C言語で作られたライブラリをC++から安全に呼び出すには、リンク時の振る舞いを正しく理解し、宣言の書き方を丁寧に整える必要があります。
本記事では、extern “C”の意味と効果、CヘッダをC++側で扱う際の注意点、そして初心者がつまずきやすいエラーの原因と回避策までを、実用的なサンプルとともに詳しく解説します。
なぜ互換性にextern “C”が必要か
CとC++の名前修飾(name mangling)の違い
C++は関数オーバーロードなどの機能を実現するため、コンパイル後のシンボル名を名前修飾(name mangling)で一意化します。
一方、Cは引数型の情報をシンボル名に含めません。
この違いにより、Cでビルドされた関数(foo
)をC++からそのまま呼ぶと、C++側では_Z3fooi
のように別名で探しに行き、リンクエラーが発生します。
つまり、Cで提供されるシンボル名とC++が期待するシンボル名が一致しないことが互換性問題の核です。
以下は概念図です(実際の名前はコンパイラやABIに依存します)。
言語 | ソース上の宣言 | 代表的なリンク名の例 |
---|---|---|
C | int foo(int); | foo |
C++ | int foo(int); | _Z3fooi (GCC系/Itanium ABIの例) |
C++ | int foo(double); | _Z3food |
C++ with extern “C” | extern “C” int foo(int); | foo |
extern “C”で関数名をC形式に固定
C++側でextern "C"
を付けて宣言すると、その宣言に対するリンク名がCと同じ形式(名前修飾なし)に固定されます。
これにより、Cでビルドされたライブラリの関数シンボルと一致し、リンクが成功します。
extern “C”は「呼び出し規約」を変えません。
Windows環境などで__cdecl
や__stdcall
が関わる場合は、別途呼び出し規約の整合も必要です。
リンク名固定の最小例
// C++側
extern "C" int foo(int); // CでビルドされたfooをCリンク名で参照する
int main() {
return foo(42);
}
標準Cライブラリでextern “C”が不要な理由
標準Cライブラリの関数(printf, strlen, mallocなど)は、C++標準が定めるヘッダ実装側で適切にCリンケージが付与されています。
具体的には以下のいずれかの形で提供されます。
- C++用の
<cstdio>
<cstring>
などのc頭ヘッダでは、宣言がstd
名前空間に入りつつCリンケージが確保されます。 - 互換のための
<stdio.h>
などでも、多くの実装がextern "C"
ブロックで囲っています。
そのため、ユーザーが標準Cライブラリに対して独自にextern “C”を書く必要は基本的にありません。
ただし、非標準のCライブラリや自作のCヘッダは、以降で解説する方法で明示的に対応します。
extern “C”の基本的な使い方
個別の関数宣言に付ける書き方
単一の関数だけCリンケージにしたい場合は、宣言に直接付けます。
// C++ソース内でCの関数を1つだけ使う例
extern "C" int add(int a, int b); // Cのaddを呼びたい
int main() {
int r = add(3, 4);
return r == 7 ? 0 : 1;
}
複数の宣言をまとめるブロック
複数の関数や型をまとめる場合は、ブロック形式が読みやすいです。
// C++ソース内で複数宣言をまとめる例
extern "C" {
int add(int a, int b);
void log_message(const char* msg);
// グローバル変数の参照も可能(避けるべきですが、やむを得ない場合)
extern int g_counter;
}
ヘッダ側にだけ書くのが安全
宣言は常にヘッダ側に集約し、C++からもCからも同じヘッダをインクルードするのが安全です。
CソースはCコンパイラでビルドされるため、extern "C"
の記法をそのまま書くとエラーになります。
そこで__cplusplus
マクロでガードします。
CとC++の両対応ヘッダの基本形
/* mylib.h: CとC++両対応のヘッダ */
#ifndef MYLIB_H
#define MYLIB_H
#ifdef __cplusplus
extern "C" { /* C++でインクルードされたときだけCリンケージにする */
#endif
/* ここはCの有効な宣言だけを書く */
int add(int a, int b);
#ifdef __cplusplus
} /* extern "C" の終わり */
#endif
#endif /* MYLIB_H */
最小サンプル(宣言→呼び出し)
Cで実装された関数add
を、C++から呼び出す最小の流れを示します。
/* mylib.c: C言語でビルドされる実装 */
#include "mylib.h"
int add(int a, int b) {
return a + b; /* シンプルな加算 */
}
/* mylib.h: 前掲のヘッダと同じ内容 */
#ifndef MYLIB_H
#define MYLIB_H
#ifdef __cplusplus
extern "C" {
#endif
int add(int a, int b);
#ifdef __cplusplus
}
#endif
#endif
// main.cpp: C++からCの関数を呼ぶ
#include <iostream>
#include "mylib.h"
int main() {
int a = 12;
int b = 30;
int s = add(a, b); // Cのaddを呼ぶ
std::cout << "add(" << a << "," << b << ") = " << s << std::endl;
return 0;
}
add(12,30) = 42
ビルド例(参考):
# 1) CファイルはCコンパイラで
gcc -std=c11 -c mylib.c -o mylib.o
# 2) C++ファイルはC++コンパイラで
g++ -std=c++17 main.cpp mylib.o -o app
# 3) 実行
./app
CヘッダとライブラリをC++で使う
Cヘッダを安全にインクルードする方法
常に公式ヘッダを経由し、ソース側で独自の関数プロトタイプを重ねて書かないのが基本です。
自作Cライブラリなら、先ほどの__cplusplus
ガード付きヘッダを用意し、C++ソースからそのヘッダをインクルードします。
標準Cライブラリは<cstdio>
などのC++版ヘッダの利用が無難です。
ヘッダを書かずにC++ソース内で手書き宣言すると、シグネチャ不一致やextern “C”の付け漏れが起きやすく、リンクや実行時に問題化します。
__cplusplusガードの定番パターン
次の雛形はほぼ定番です。
これに沿っていれば、CでもC++でも同一ヘッダが使えます。
/* example.h: 定番の__cplusplusガード */
#ifndef EXAMPLE_H
#define EXAMPLE_H
#ifdef __cplusplus
extern "C" {
#endif
/* Cから見ても有効な宣言だけを書く */
typedef struct Point {
int x;
int y;
} Point;
void move(Point* p, int dx, int dy);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* EXAMPLE_H */
関数のプロトタイプと型を一致させる
CとC++では型の別名やサイズがプラットフォームで微妙に異なることがあります。
例えばsize_t
はLP64系とLLP64系で実体が異なります。
必ずヘッダの宣言を唯一の真実としてソースコードはそれに従い、勝手にunsigned long
などに置き換えないでください。
不一致の例:
// NG例: Cヘッダでは size_t write(const void*, size_t) なのに...
extern "C" unsigned long write(const void* p, unsigned long n); // プラットフォームにより不一致で未定義動作の恐れ
正しい例は、Cヘッダをインクルードしてそのまま使うことです。
#include <cstddef> // size_t
extern "C" {
#include "clib_write.h" // 本物の宣言を含む
}
ポインタのconst修飾や構造体の前方宣言の違いでもABIが変わることがあるため、独自に宣言を「推測」しないことが重要です。
よくあるトラブルと回避策
未定義参照(リンクエラー)の原因
典型的には、Cの実装に対してC++側の宣言がC++リンケージのままになっているケースです。
リンク時に「修飾済みの名前」を探しに行き、Cオブジェクト内の「素の名前」と一致しないため失敗します。
エラーメッセージ例(GCC系):
/usr/bin/ld: main.o: in function `main':
main.cpp:(.text+0x1d): undefined reference to `add(int, int)'
collect2: error: ld returned 1 exit status
回避策は、ヘッダにextern “C”ガードを正しく書くことです。
ソース側で手書き宣言を増やさないことも重要です。
宣言の付け忘れ・スペルミス
関数名の1文字違いやextern "C"
の付け忘れは、ビルドが通っても実行時のクラッシュやリンク失敗につながります。
常に公式ヘッダをインクルードし、宣言は1か所に集約しましょう。
オーバーロード不可に注意
extern “C”指定の関数はオーバーロードできません。
Cリンケージは名前修飾を行わないため、同名異引数の区別ができないからです。
extern "C" int api(int);
// 次はエラー: Cリンケージの同名関数は定義できない
extern "C" int api(const char*); // error: conflicting C linkage declaration
異なる機能を提供したい場合は関数名を変える、あるいはC++側に薄いラッパーを用意します。
例外をC関数に渡さない
例外はC言語の世界に存在しません。
Cから呼ばれる可能性がある関数、またはC関数ポインタを経由する境界では、例外を外に出さないようにします。
C++で例外を使うなら、境界で捕捉してエラーコードに変換します。
// Cから呼び出されるCリンケージ関数。例外を外に出さない。
extern "C" int do_work_c(int x) {
try {
// C++内部実装(例外を投げる可能性あり)
// ここではダミーの例
if (x < 0) throw std::runtime_error("negative");
return x * 2;
} catch (...) {
// 例外はここで止め、エラーコードを返す
return -1;
}
}
境界での責任分担を明確にし、例外や所有権、スレッド安全性などのモデルを混同しないことが安全運用の鍵です。
まとめ
C言語の関数をC++から安全に呼ぶには、extern “C”でリンク名をC形式に固定し、__cplusplusガード付きヘッダを唯一の宣言源として共有するのが基本です。
標準Cライブラリは実装側で対応済みのため通常はextern “C”不要ですが、自作やサードパーティのCライブラリではヘッダ側にextern “C”を忘れないことが重要です。
オーバーロード不可や例外の扱いなど、言語間のモデル差も境界で吸収しましょう。
最後に繰り返しますが、「公式ヘッダを信頼し、ソースに独自宣言を書かない」ことが最も効果的なトラブル回避策です。