C++を用いてソフトウェア開発を行う際、過去の資産であるC言語のライブラリを活用したり、C言語で記述されたシステムと連携したりする場面は非常に多くあります。
その際に必ずと言っていいほど登場するのがextern “C”という宣言です。
C++はC言語との高い互換性を持っていますが、コンパイル後の「名前」の扱いが異なるため、単純に結合しようとするとリンクエラーが発生してしまいます。
この記事では、extern “C”の正確な意味から、名前修飾 (ネームマングリング) の仕組み、そして具体的な実装パターンまでを詳しく解説します。
extern “C” が必要とされる背景
C++はC言語を拡張して作られた言語ですが、コンパイルのプロセスにおいて決定的な違いがあります。
それは、関数や変数の名前をリンカーがどのように識別するかという点です。
C++では関数オーバーロード (同じ名前で引数が異なる関数を複数定義できる機能) をサポートするために、コンパイラが関数名に引数の型情報などを付加して、内部的な識別名を生成します。
一方、C言語には関数オーバーロードが存在しないため、ソースコード上の関数名がほぼそのままオブジェクトファイル内のシンボル名として使用されます。
この「言語による名前の扱いの違い」が、C++からC言語の関数を呼び出す際、あるいはその逆を行う際の大きな障壁となります。
この差異を埋め、C言語の形式でリンケージを行うことをコンパイラに指示するのが、extern “C”の役割です。
名前修飾 (Name Mangling) とは何か
extern “C”を理解する上で避けて通れないのが名前修飾 (Name Mangling)という概念です。
これは、C++コンパイラがソースコード上の識別子を、リンカーが解釈可能な一意のの名前に変換するプロセスのことです。
C言語の場合の名前
C言語では、ソースコードで void func(int a); と定義した場合、オブジェクトファイル内でも _func や func といった単純な名前で管理されます。
そのため、同じ名前の関数はプロジェクト内に一つしか存在できません。
C++の場合の名前
C++では、以下のようなオーバーロードが可能です。
void func(int a);
void func(double b);
もしこれらが両方とも func という名前でオブジェクトファイルに書き込まれてしまうと、リンカーはどちらを呼び出すべきか判断できません。
そこでC++コンパイラは、以下のように名前を加工します (具体的な形式はコンパイラによって異なります)。
func(int)→__Z4funcifunc(double)→__Z4funcd
このように、関数名に引数の情報をエンコードして付与する仕組みが名前修飾です。
C++からC言語で書かれた関数を呼び出そうとすると、C++側は __Z4funci を探そうとしますが、C言語のオブジェクトファイルには func しか存在しないため、「未定義の参照 (undefined reference)」というエラーが発生します。
extern “C” の基本的な意味と効果
extern “C” は、指定された範囲のコードに対して「C言語のリンケージ規約を使用せよ」とコンパイラに命じる宣言です。
具体的には、その範囲内の関数名に対してC++特有の名前修飾を行わず、C言語と同じ形式でシンボルを生成・参照するようにします。
これにより、C++コンパイラでコンパイルされたコードであっても、C言語で書かれたバイナリや他のC言語コンパイラと正しくリンクすることが可能になります。
extern “C” の具体的な構文
extern “C” には、単一の宣言に適用する方法と、ブロック全体に適用する方法の2種類があります。
単一宣言への適用
特定の関数一つだけをC言語形式で扱いたい場合は、以下のように記述します。
extern "C" void my_c_function(int x);
ブロックへの適用
複数の関数を一括して指定したい場合は、波括弧 {} を使用します。
extern "C" {
void func1();
int func2(double d);
extern int global_var;
}
このブロック内で宣言されたものはすべて、C++の名前修飾を受けずに処理されます。
ヘッダーファイルにおける標準的な書き方
実務において最も一般的な使い方は、C言語とC++の両方から参照される可能性のあるヘッダーファイルでの記述です。
しかし、extern “C” はC++独自のキーワードであるため、C言語コンパイラがこれを読み込むと構文エラーになってしまいます。
この問題を解決するために、プリプロセッサマクロ __cplusplus を使用するのが定石です。
互換性を考慮したヘッダーの例
#ifndef MY_LIBRARY_H
#define MY_LIBRARY_H
/* C++の場合のみ extern "C" { を開始する */
#ifdef __cplusplus
extern "C" {
#endif
/* ここに共通の関数プロトタイプ宣言を記述 */
void process_data(int value);
int get_status();
#ifdef __cplusplus
}
#endif
#endif /* MY_LIBRARY_H */
この記述方法により、以下のような挙動が実現します。
- C++コンパイラで読み込んだ場合:
__cplusplusが定義されているため、extern “C” が有効になり、名前修飾が無効化されます。 - Cコンパイラで読み込んだ場合:
__cplusplusは定義されていないため、extern “C” は無視され、通常のC言語の宣言として処理されます。
C++からC言語の関数を呼び出す手順
実際にC++プログラムからC言語で書かれたソースファイルを呼び出す流れを、サンプルコードを用いて解説します。
1. C言語のソースファイル (sample.c)
#include <stdio.h>
/* C言語で実装された関数 */
void hello_from_c() {
printf("Hello from C language!\n");
}
2. C++のメインファイル (main.cpp)
#include <iostream>
/* C言語の関数を宣言。extern "C" がないとリンクエラーになる */
extern "C" {
void hello_from_c();
}
int main() {
std::cout << "Starting C++ program..." << std::endl;
/* C言語の関数を呼び出し */
hello_from_c();
return 0;
}
3. 実行結果の例
コンパイルおよびリンクが正しく行われると、以下の出力が得られます。
Starting C++ program...
Hello from C language!
もし extern "C" を記述しなかった場合、リンカーは _Z13hello_from_cv (環境により異なる) のような名前を探そうとしますが、sample.o 内には hello_from_c しか存在しないため、undefined reference to ‘hello_from_c()’ というエラーで失敗します。
C言語からC++の関数を呼び出す手順
逆のパターンとして、C言語のプロジェクトからC++で書かれたロジックを利用したい場合もあります。
この場合、C++側で「名前修飾をしない」ように関数を定義する必要があります。
1. C++側の実装 (logic.cpp)
#include <iostream>
/* C言語から見えるように extern "C" を付けて定義 */
extern "C" void cpp_logic(int n) {
std::cout << "C++ logic processed: " << n * 2 << std::endl;
}
2. C言語側のメインファイル (main.c)
C言語側では extern “C” は使えませんので、単にプロトタイプ宣言を行います。
#include <stdio.h>
/* プロトタイプ宣言 (C++側で extern "C" されている前提) */
void cpp_logic(int n);
int main() {
printf("Calling C++ function from C...\n");
cpp_logic(50);
return 0;
}
3. 実行結果
Calling C++ function from C...
C++ logic processed: 100
この際、C++特有の機能 (クラスやテンプレート) を直接C言語に渡すことはできません。
そのため、ラッパー関数を用意して、C言語でも扱える基本型やポインタを介してやり取りするのが一般的です。
extern “C” の制限事項と注意点
非常に便利な extern “C” ですが、万能ではありません。
C++の機能をCリンケージで扱う際には、いくつかの制約があります。
1. 関数オーバーロードは不可
extern “C” ブロック内では、同じ名前の関数を複数定義することはできません。
名前修飾を無効化するということは、シンボル名が関数名そのものになるということであり、C言語と同様の「名前の重複禁止」ルールが適用されます。
extern "C" {
void func(int a);
void func(double b); // エラー:Cリンケージではオーバーロードできない
}
2. クラスのメンバ関数には適用不可
クラスのメソッド (メンバ関数) は、本質的にそのクラスのスコープや this ポインタを持つため、C言語の関数形式とは互換性がありません。
メンバ関数を extern “C” にすることはできず、どうしても呼び出したい場合は、そのクラスのインスタンスを受け取ってメンバ関数を呼び出す「非メンバのラッパー関数」を extern “C” で作成する必要があります。
3. テンプレート関数には適用不可
テンプレートはコンパイル時に型に応じて実体を生成する仕組みであり、固定の名前を持つC言語のシンボルとは相容れません。
テンプレート関数を extern “C” で宣言することはできません。
4. 引数や戻り値の型
extern “C” を指定しても、その関数の引数にC++特有の型 (参照型や std::string など) を使うことは文法上可能ですが、それをC言語側から呼び出すことはできません。
C言語との連携が目的であれば、引数や戻り値はC言語でも解釈可能な型 (プリミティブ型や構造体のポインタなど)に限定すべきです。
応用:共有ライブラリ (DLL/so) と extern “C”
WindowsのDLLやLinuxの共有オブジェクト (.so) を作成し、それを動的にロード (dlopen や LoadLibrary) して関数を取得する場合、extern “C” は極めて重要です。
動的なロードでは、文字列で関数名を指定してそのアドレスを取得します。
もし C++ の名前修飾がかかっていると、"my_function" という名前で検索しても見つからず、"_Z11my_functionv" のような複雑な名前を指定しなければならなくなります。
これでは汎用性が著しく低下するため、外部公開するAPIには必ず extern “C” を付与するのが鉄則です。
よくあるトラブルと解決策
開発現場でよく遭遇する extern “C” 関連のエラーとその対処法をまとめました。
| 現象 | 原因 | 解決策 |
|---|---|---|
| リンクエラー (undefined reference) | C++からCの関数を呼ぶ際、宣言に extern “C” がない | ヘッダーを extern “C” で囲むか、宣言に付加する |
| Cコンパイルエラー (syntax error) | C用のヘッダーに直接 extern “C” と書いてしまった | #ifdef __cplusplus でガードする |
| コンパイルエラー (conflicting declaration) | 同じ名前の関数を extern “C” 内で複数定義した | オーバーロードをやめるか、別名にする |
| 実行時エラー/セグメンテーションフォールト | C++の例外が extern “C” の境界を越えてしまった | extern “C” 関数内では必ず try-catch で例外を捕捉する |
特に最後の「例外の送出」には注意が必要です。
C言語はC++の例外処理を知りません。
extern “C” で宣言された関数からC++例外をスローし、それをC言語のコードを介してさらに上位のC++層でキャッチしようとする動作は、未定義動作を引き起こす可能性があります。
連携用の関数内では例外を適切に処理し、エラーコードを返す形式にするのが安全です。
まとめ
extern “C” は、C++とC言語という異なるリンケージ規約を持つ世界を繋ぐ「架け橋」のような存在です。
- 名前修飾 (Name Mangling) を防ぎ、C言語形式のシンボル名を生成する。
- C言語のライブラリをC++で利用する際、あるいはその逆で必須となる。
__cplusplusマクロを用いて、C/C++両対応のヘッダーを作成するのが一般的。- オーバーロードやクラスメンバ、テンプレートには適用できないという制限がある。
現代のC++開発においても、OSに近いレイヤーや高性能な数値計算ライブラリ、埋め込みシステムなど、C言語との連携シーンは枚挙にいとまがありません。
extern “C” の仕組みを正しく理解しておくことは、トラブルの少ない堅牢なシステムを構築するための第一歩と言えるでしょう。
この記事が、C++におけるリンケージの問題を解決する一助となれば幸いです。
