Cの広い互換性と実装の自由度を併せ持つC++は、システム連携や長期運用の現場で強力な選択肢になります。
本稿では、C言語から安全に呼び出せるC++コードの書き方を、extern “C”、ABI、ヘッダ設計、ビルド・テストまで一貫して解説します。
具体例とコードで、実践的なベストプラクティスを掴んでいただけます。
C言語互換の前提とユースケース:C++でC APIを提供する意義
どんな場面でC言語互換が必要か(組込み・プラグイン・FFI)
組込み・OSカーネル周辺
多くの組込み環境やOSの低層はC ABIが標準で、C++ランタイムや例外機構を前提にできません。
ドライバ、ミドルウェア、RTOS向けライブラリはC関数としての提供が安全です。
プラグインと動的ロード
アプリケーションがdlsym
やGetProcAddress
で関数を解決するプラグイン方式では、名前修飾がないCシンボルが前提となることが多く、C ABIを守ることで互換性が高まります。
他言語FFI(Python、Rust、Go、Javaなど)
多くのFFIはC ABIを基準に設計されており、Cシンボルと安定したレイアウトのデータだけを前提にします。
Cで公開し内部でC++を活用すれば、他言語から容易に呼び出せます。
C言語互換の基本方針と制約の整理
公開境界は「C」、内部実装は「C++」
公開ヘッダはCコンパイラでそのまま通るように設計し、内部ではC++のクラス、テンプレート、RAII、スマートポインタを最大限活用します。
橋渡しは薄いCラッパーが担います。
互換性を壊しやすい要素の排除
例外、RTTI、関数オーバーロード、デフォルト引数、参照型(T&)、テンプレートの公開、std系コンテナやstringの公開などはABIや言語互換を壊すため、Cの公開APIには持ち込まない前提で設計します。
extern “C” の基礎:名前修飾(name mangling)抑止とリンケージ
ヘッダでのextern “C”ガード(C/C++両対応の記述例)
C++は関数名にシグネチャを埋め込む名前修飾を行います。
Cからリンク可能にするには、公開関数をextern "C"
で囲みます。
ヘッダはC/C++両対応に記述します。
// CコンパイラでもC++コンパイラでもインクルード可能な公開ヘッダ
#ifndef MYLIB_H
#define MYLIB_H
// バージョンマクロ
#define MYLIB_VERSION_MAJOR 1
#define MYLIB_VERSION_MINOR 0
#define MYLIB_VERSION_PATCH 0
// 可視性と呼び出し規約
#if defined(_WIN32) || defined(__CYGWIN__)
#if defined(MYLIB_SHARED)
#if defined(MYLIB_BUILD)
#define MYLIB_API __declspec(dllexport)
#else
#define MYLIB_API __declspec(dllimport)
#endif
#else
#define MYLIB_API
#endif
// 既定は__cdecl。必要ならビルド時にMYLIB_USE_STDCALLを定義
#ifdef MYLIB_USE_STDCALL
#define MYLIB_CALL __stdcall
#else
#define MYLIB_CALL __cdecl
#endif
#else
#if defined(MYLIB_SHARED)
#define MYLIB_API __attribute__((visibility("default")))
#else
#define MYLIB_API
#endif
#define MYLIB_CALL
#endif
#include <stddef.h> // size_t
#include <stdint.h> // int32_t, int64_t
#ifdef __cplusplus
extern "C" {
#endif
// 不完全型で実装を隠す
typedef struct mylib_context mylib_context;
// エラーコード(例外は使わない)
typedef enum mylib_status {
MYLIB_OK = 0,
MYLIB_EINVAL = 1,
MYLIB_EOOM = 2, // out-of-memory
MYLIB_EINTERNAL = 3 // その他内部エラー
} mylib_status;
// コンテキスト生成・破棄(所有権は呼び出し側が持つ)
MYLIB_API mylib_status MYLIB_CALL mylib_create(mylib_context** out_ctx);
MYLIB_API void MYLIB_CALL mylib_destroy(mylib_context* ctx);
// 設定と計算(in/out契約を明確化)
MYLIB_API mylib_status MYLIB_CALL mylib_set_scale(mylib_context* ctx, int32_t scale);
MYLIB_API mylib_status MYLIB_CALL mylib_sum_scaled(
mylib_context* ctx,
const int32_t* data, size_t n,
int64_t* out_sum);
#ifdef __cplusplus
} // extern "C"
#endif
#endif // MYLIB_H
ポイント
extern "C"
はヘッダ側で宣言に適用し、C++でもCリンケージになるようにします。- シンボル可視性と呼び出し規約はマクロにまとめ、プラットフォームとビルド形態(静的/共有)に応じて切り替えます。
実装側の注意点:C++からCシンボルをエクスポートする
C++実装では公開関数の定義もextern "C"
で囲みます。
例外は境界を越えないよう必ず捕捉し、エラーコードへ変換します。
// C++で実装しCリンケージでエクスポート
#include "mylib.h"
#include <memory>
#include <vector>
#include <new> // std::bad_alloc
#include <exception>
// 内部実装はC++の型で自由に設計
namespace mylib_detail {
class ContextImpl {
public:
void set_scale(int32_t s) { scale_ = s; }
int64_t sum_scaled(const int32_t* data, size_t n) const {
int64_t acc = 0;
for (size_t i = 0; i < n; ++i) {
acc += static_cast<int64_t>(data[i]) * static_cast<int64_t>(scale_);
}
return acc;
}
private:
int32_t scale_ = 1;
};
static mylib_status to_status_from_exception() noexcept {
try {
throw; // 直前の例外を再スローして型を判定
} catch (const std::bad_alloc&) {
return MYLIB_EOOM;
} catch (...) {
return MYLIB_EINTERNAL;
}
}
}
// Opaque pointerの実体(サイズやレイアウトは非公開)
struct mylib_context {
std::unique_ptr<mylib_detail::ContextImpl> pimpl;
};
extern "C" {
MYLIB_API mylib_status MYLIB_CALL mylib_create(mylib_context** out_ctx) {
if (!out_ctx) return MYLIB_EINVAL;
try {
// 例外安全:unique_ptrで確実に解放
std::unique_ptr<mylib_context> ctx(new mylib_context);
ctx->pimpl = std::make_unique<mylib_detail::ContextImpl>();
*out_ctx = ctx.release();
return MYLIB_OK;
} catch (...) {
return mylib_detail::to_status_from_exception();
}
}
MYLIB_API void MYLIB_CALL mylib_destroy(mylib_context* ctx) {
// 例外を投げないこと(noexcept違反はstd::terminate)
delete ctx; // unique_ptrごと破棄され実装も解放
}
MYLIB_API mylib_status MYLIB_CALL mylib_set_scale(mylib_context* ctx, int32_t scale) {
if (!ctx) return MYLIB_EINVAL;
try {
ctx->pimpl->set_scale(scale);
return MYLIB_OK;
} catch (...) {
return mylib_detail::to_status_from_exception();
}
}
MYLIB_API mylib_status MYLIB_CALL mylib_sum_scaled(
mylib_context* ctx, const int32_t* data, size_t n, int64_t* out_sum) {
if (!ctx || (!data && n != 0) || !out_sum) return MYLIB_EINVAL;
try {
*out_sum = ctx->pimpl->sum_scaled(data, n);
return MYLIB_OK;
} catch (...) {
return mylib_detail::to_status_from_exception();
}
}
} // extern "C"
ABIと呼び出し規約:バイナリ互換を壊さないために
呼び出し規約(cdecl/stdcall)とプラットフォーム差(Windows/Unix)
呼び出し規約の選択
Windowsでは__cdecl
(呼び出し側がスタック片付け)や__stdcall
(呼び出され側が片付け)などの呼び出し規約がABIに影響します。
不一致はクラッシュの原因になります。
公開ヘッダではMYLIB_CALL
のようなマクロに閉じ込め、ビルドや利用側で一貫させます。
Unix系(ELF)では一般に呼び出し規約が固定で、明示が不要なことが多いです。
実務的な勧め
- WindowsのC APIは既定で
__cdecl
を採用し、必要時のみ__stdcall
をサポートする分岐を用意します。 - 共有ライブラリと静的ライブラリでエクスポート属性を切り替えるマクロを用意します(上記
MYLIB_API
)。
シンボル可視性・バージョニング・安定したABI設計
可視性の最小化
ELFでは-fvisibility=hidden
を既定にし、公開関数だけvisibility("default")
で明示します。
Windowsでは__declspec(dllexport)
か.def
ファイルで公開関数のみをエクスポートします。
内部シンボル露出を抑えると、衝突回避・最適化・起動時間に有利です。
バージョニングと後方互換
- セマンティックバージョニング(MAJOR.MINOR.PATCH)に従い、ABI破壊はMAJORでのみ行う。
- ELFではリンカ版スクリプトでシンボルバージョンを付け、古いシンボルも残すことで段階的移行が可能です。
/* ELF向けシンボルバージョン */
MYLIB_1.0 {
global:
mylib_create;
mylib_destroy;
mylib_set_scale;
mylib_sum_scaled;
local:
*;
};
g++ -shared -fvisibility=hidden -Wl,--version-script=mylib.map -o libmylib.so mylib.cpp
; mylib.def — エクスポートシンボルの明示
LIBRARY "mylib"
EXPORTS
mylib_create
mylib_destroy
mylib_set_scale
mylib_sum_scaled
C互換APIのヘッダ設計ベストプラクティス
POD/固定幅整数(stdint.h)/列挙・構造体のレイアウト規約
データ型の選定
公開APIにはstdint.h
の固定幅整数(int32_t
/uint64_t
)とsize_t
を使い、long
のようにプラットフォームで幅が変わる型を避けます。
構造体を公開する場合はPOD(Plain Old Data)に限定し、アラインメントやパディングを意識します。
#pragma pack
の乱用は避け、バイナリ互換維持が難しくなる設計は控えます。
列挙体
CとC++でサイズが異なる可能性を踏まえ、ABI境界に生の列挙値を渡すだけにします。
サイズ固定が必要ならint32_t
などの実整数型を明示します。
Opaque pointer(不完全型)で内部実装を隠蔽する
不完全型の利点
typedef struct X X;
のような不完全型を公開し、実体はC++側に隠します。
これにより内部レイアウト変更がABIに波及しません。
上のmylib_context
が典型例です。
例外・RTTI・関数オーバーロード・デフォルト引数の禁止
禁止事項の理由
- 例外やRTTIはABIとランタイムに依存し、Cから扱えません。
- 関数オーバーロードやデフォルト引数はCにはなく、名前解決やABIを複雑化します。
- 参照型(
T&
)やC++専用の型(std::string
、std::vector
)を公開境界に出さないでください。
必要に応じて、C風の関数名にプレフィックスを付け(mylib_
)、明確な1関数1責務を徹底します。
メモリ割り当てと所有権(create/destroy, in/outバッファ)の明確化
所有権ポリシー
- 「作る側が壊す」を原則に、
create/destroy
ペアを提供します。 - バッファは「呼び出し側が確保、ライブラリが書き込む」か「ライブラリが確保、専用のfree関数で解放」を関数ごとに明記します。
- サイズ問い合わせ関数(
*_get_required_size
)を設けると安全です。
本稿の例では、コンテキストはライブラリが確保して返し、破棄はmylib_destroy
で行うという明確な契約にしています。
C++実装の活用とCラッパー:橋渡しパターン
C++内部(クラス/テンプレート/スマートポインタ)をC APIで包む
橋渡しの要諦
内部の高度なロジックはC++の表現力を活かし、境界で型消去(opaque pointer)してC関数に落とします。
ポインタ所有権はstd::unique_ptr
で厳密に管理し、C関数では生ポインタに限定して公開します。
エラーハンドリング:例外を外へ出さずエラーコード/errnoで返す
例外→ステータス変換
C関数は決して例外を投げず、失敗は戻り値で返します。
別途mylib_last_error()
のようなスレッドローカル詳細エラー取得APIを用意するパターンもあります。
ここでは典型的なenum
のステータスで簡潔に表現しています。
スレッド安全性と再入可能性の確保
状態の局所化
グローバル可変状態を避け、状態はコンテキストに閉じ込めます。
関数は可能な限り再入可能(reentrant)にし、共有資源を使う場合は内部でミューテックスなどを管理します。
スレッド安全性はドキュメントに明記し、APIごとに「スレッドセーフ」「コンテキスト間は独立」などの契約を提示します。
ビルドとテスト:移植性と互換性の検証
コンパイラ差(MSVC/GCC/Clang)・リンカ設定・.def/visibility制御
クロスコンパイラの注意
- MSVCは例外仕様や警告の既定が異なるため、
/EHsc
や/W4
を設定し、公開関数はnoexcept
相当を守る実装にします。 - GCC/Clangでは
-fvisibility=hidden
とバージョンスクリプト、MSVCでは__declspec(dllexport)
か.def
で公開範囲を最小化します。
Cコンパイル通過テストとABI互換テスト(nm/objdump/ABIチェック)
Cとしてビルドが通ることの検証
公開ヘッダが「本当にC互換か」をCIで常時検証すると事故を防げます。
# Cとしてヘッダと最小プログラムをビルド
gcc -std=c11 -Wall -Wextra -Werror -c test.c -o test.o
Cのテストプログラム例と実行結果:
// CからC++実装のライブラリを利用
#include "mylib.h"
#include <stdio.h>
#include <inttypes.h>
int main(void) {
mylib_context* ctx = NULL;
if (mylib_create(&ctx) != MYLIB_OK) {
fprintf(stderr, "create failed\n");
return 1;
}
mylib_set_scale(ctx, 2);
int32_t data[] = {1, 2, 3};
int64_t sum = 0;
if (mylib_sum_scaled(ctx, data, 3, &sum) != MYLIB_OK) {
fprintf(stderr, "sum_scaled failed\n");
mylib_destroy(ctx);
return 2;
}
printf("sum_scaled = %" PRId64 "\n", sum);
mylib_destroy(ctx);
return 0;
}
sum_scaled = 12
シンボルのmangling抑止を確認
ELFの場合の確認例です。
nm -D --defined-only libmylib.so | grep mylib_
0000000000001120 T mylib_create
0000000000001160 T mylib_destroy
00000000000011a0 T mylib_set_scale
00000000000011f0 T mylib_sum_scaled
ABIドリフトの自動チェック
- Linux: abi-dumper + abi-compliance-checkerでバージョン間のABI互換性をCI検証します。
- Windows: 公開関数セット(.def)やヘッダの差分を監視し、呼び出し規約と引数型の不一致を検出します。
パッケージングと配布(pkg-config、バージョンポリシー)
pkg-configファイル
ビルドフラグを配布すれば利用側が簡単に設定できます。
# mylib.pc
prefix=/usr
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include
Name: mylib
Description: Example C-compatible C++ library
Version: 1.0.0
Libs: -L${libdir} -lmylib
Cflags: -I${includedir}
バージョンとsoname
ELFではlibmylib.so.1
のようにMAJORをsonameに反映し、MINOR/PATCHは後方互換の範囲で増やします。
互換性を破る変更はMAJORを上げ、古いアプリが誤ってリンクしないようにします。
まとめ
C言語互換を保ちながらC++で実装する最短ルートは、公開境界を「純C」、内部実装を「C++」と明確に分離することです。
ヘッダではextern "C"
ガード、固定幅整数とPOD、opaque pointer、明確な所有権とエラーコードといった原則を徹底します。
実装では例外を境界外に出さず、スマートポインタとRAIIで安全性と保守性を高めます。
ABI面では呼び出し規約・可視性・バージョニングを統一し、CIで「Cとしてコンパイルできること」と「ABI互換」を継続的に検証します。
これらのベストプラクティスを押さえることで、他言語FFI・プラグイン・組込みなど多様な現場に長く耐えるライブラリを、C++の表現力を損なわずに提供できます。