C++ ではなぜ extern “C”{ #include <foo.h> } が必要なのでしょうか?
-
09-06-2019 - |
質問
なぜ使用する必要があるのですか:
extern "C" {
#include <foo.h>
}
具体的には:
いつ使用すればよいですか?
それを使用する必要があるコンパイラ/リンカー レベルで何が起こっているのでしょうか?
コンパイル/リンクの観点から、これを使用する必要がある問題はどのように解決されるのでしょうか?
解決
C と C++ は表面的には似ていますが、それぞれは非常に異なるコード セットにコンパイルされます。C++ コンパイラにヘッダー ファイルをインクルードすると、コンパイラは C++ コードを期待します。ただし、それが C ヘッダーの場合、コンパイラーはヘッダー ファイルに含まれるデータが特定の形式 (C++ 'ABI' または 'Application Binary Interface') にコンパイルされることを期待するため、リンカーが停止します。これは、C データを期待する関数に C++ データを渡すよりも望ましい方法です。
(実際の核心に入ると、C++ の ABI は通常、関数/メソッドの名前を「マングル」するため、 printf()
プロトタイプに C 関数としてフラグを設定しないと、C++ は実際に呼び出しコードを生成します。 _Zprintf
, 、最後に追加のくだらないことを追加します。)
それで:使用 extern "C" {...}
c ヘッダーをインクルードする場合、それはとても簡単です。そうしないと、コンパイルされたコードに不一致が生じ、リンカーが停止します。ただし、ほとんどのヘッダーでは、 extern
なぜなら、ほとんどのシステム C ヘッダーは、それらが C++ コードにインクルードされる可能性があるという事実をすでに考慮しており、すでに extern
彼らのコード。
他のヒント
extern "C" は、生成されたオブジェクト ファイル内のシンボルにどのような名前を付けるかを決定します。関数が extern "C" なしで宣言されている場合、オブジェクト ファイル内のシンボル名には C++ の名前マングリングが使用されます。ここに例を示します。
test.C を次のように指定すると、次のようになります。
void foo() { }
シンボルをコンパイルしてオブジェクト ファイルにリストすると、次のようになります。
$ g++ -c test.C
$ nm test.o
0000000000000000 T _Z3foov
U __gxx_personality_v0
foo 関数は実際には「_Z3foov」と呼ばれます。この文字列には、戻り値の型やパラメータなどの型情報が含まれています。代わりに次のように test.C を作成すると、次のようになります。
extern "C" {
void foo() { }
}
次にコンパイルしてシンボルを確認します。
$ g++ -c test.C
$ nm test.o
U __gxx_personality_v0
0000000000000000 T foo
Cリンケージが得られます。オブジェクト ファイル内の "foo" 関数の名前は単なる "foo" であり、名前のマングリングから得られる派手な型情報がすべて含まれているわけではありません。
通常、ヘッダーに含まれるコードが C コンパイラーでコンパイルされているが、それを C++ から呼び出そうとしている場合は、ヘッダーを extern "C" {} 内に含めます。これを行うと、ヘッダー内のすべての宣言が C リンケージを使用することをコンパイラーに伝えることになります。コードをリンクすると、.o ファイルには「_Z3foblah」ではなく「foo」への参照が含まれます。これは、リンク先のライブラリ内の内容と一致することが期待されます。
最新のライブラリのほとんどは、シンボルが正しいリンケージで宣言されるように、そのようなヘッダーの周囲にガードを置きます。例えば多くの標準ヘッダーには次のものがあります。
#ifdef __cplusplus
extern "C" {
#endif
... declarations ...
#ifdef __cplusplus
}
#endif
これにより、C++ コードにヘッダーが含まれる場合、オブジェクト ファイル内のシンボルが C ライブラリ内のシンボルと一致することが保証されます。C ヘッダーが古く、これらのガードがまだない場合は、C ヘッダーの周囲に extern "C" {} を置くだけで済みます。
C++ では、名前を共有するさまざまなエンティティを持つことができます。たとえば、すべての名前が付けられた関数のリストは次のとおりです。 ふー:
A::foo()
B::foo()
C::foo(int)
C::foo(std::string)
これらすべてを区別するために、C++ コンパイラは、名前マングリングまたは装飾と呼ばれるプロセスでそれぞれに一意の名前を作成します。C コンパイラはこれを行いません。さらに、各 C++ コンパイラはこれを異なる方法で実行する場合があります。
extern "C" は、中かっこ内のコードに対して名前のマングリングを実行しないように C++ コンパイラーに指示します。これにより、C++ 内から C 関数を呼び出すことができます。
これは、さまざまなコンパイラが名前マングリングを実行する方法に関係しています。C++ コンパイラは、C コンパイラとはまったく異なる方法でヘッダー ファイルからエクスポートされたシンボルの名前を破壊するため、リンクしようとすると、シンボルが見つからないというリンカー エラーが発生します。
これを解決するには、C++ コンパイラに「C」モードで実行するように指示します。これにより、C コンパイラと同じ方法で名前マングリングが実行されます。これにより、リンカー エラーが修正されます。
いつ使用すればよいですか?
C ライブラリを C++ オブジェクト ファイルにリンクする場合
コンパイラ/リンカーレベルで何が起こっているのか、それを使用する必要がありますか?
C と C++ では、シンボルの命名に異なるスキームが使用されます。これは、指定されたライブラリでリンクするときに C のスキームを使用するようにリンカーに指示します。
編集/リンクに関して、これは私たちがそれを使用する必要がある問題をどのように解決しますか?
C 命名スキームを使用すると、C スタイルのシンボルを参照できます。そうしないと、リンカーは C++ スタイルのシンボルを試行しますが、機能しません。
C と C++ では、シンボルの名前に関する規則が異なります。シンボルは、コンパイラによって生成された 1 つのオブジェクト ファイル内の関数 "openBankAccount" への呼び出しが、同じ (または互換性のある) ソース ファイルから生成された別のオブジェクト ファイル内で呼び出された関数 "openBankAccount" への参照であることをリンカが認識する方法です。コンパイラ。これにより、複数のソース ファイルからプログラムを作成できるため、大規模なプロジェクトで作業する場合に安心です。
C ではルールは非常に単純で、とにかくシンボルはすべて 1 つの名前空間内にあります。したがって、整数「socks」は「socks」として保存され、関数 count_socks は「count_socks」として保存されます。
リンカーは、この単純なシンボル命名規則を使用して、C および C などの他の言語用に構築されました。したがって、リンカー内のシンボルは単なる文字列です。
しかし、C++ では、この言語では、名前空間、ポリモーフィズム、およびそのような単純なルールと矛盾するその他のさまざまなものを使用できます。「add」と呼ばれる 6 つの多態性関数すべてに異なるシンボルが必要です。そうしないと、間違ったシンボルが他のオブジェクト ファイルで使用されてしまいます。これは、シンボルの名前を「マングリング」(これは専門用語です) することによって行われます。
C++ コードを C ライブラリまたはコードにリンクする場合、C ライブラリのヘッダー ファイルなど、C で記述されたものはすべて extern "C" で、これらのシンボル名がマングルされないことを C++ コンパイラに伝え、残りのシンボル名は破損しないようにする必要があります。もちろん、C++ コードは壊れる必要があります。そうしないと機能しません。
C コンパイラによってコンパイルされ、C++ ファイルで使用されるファイル内に存在する関数を定義するヘッダーを組み込む場合は、常に extern "C" を使用する必要があります。(多くの標準 C ライブラリでは、開発者にとって簡単にするためにヘッダーにこのチェックが含まれている場合があります)
たとえば、util.c、util.h、main.cpp という 3 つのファイルを含むプロジェクトがあり、.c ファイルと .cpp ファイルの両方が C++ コンパイラ (g++、cc など) でコンパイルされている場合、それは機能しません。実際には必要ではないため、リンカー エラーが発生する可能性もあります。ビルド プロセスで util.c に通常の C コンパイラを使用する場合は、util.h をインクルードするときに extern "C" を使用する必要があります。
何が起こっているのかというと、C++ は関数のパラメータをその名前にエンコードしています。これが関数のオーバーロードの仕組みです。C 関数で起こりがちなのは、名前の先頭にアンダースコア (「_」) を追加することだけです。extern "C" を使用しない場合、関数の実際の名前が _DoSomething() または単に DoSomething() である場合、リンカーは DoSomething@@int@float() という名前の関数を探します。
extern "C" を使用すると、C++ の命名規則ではなく C の命名規則に従う関数を検索するように C++ コンパイラーに指示することで、上記の問題を解決できます。
C++ コンパイラは、C コンパイラとは異なる方法でシンボル名を作成します。したがって、C コードとしてコンパイルされた C ファイル内にある関数を呼び出そうとしている場合は、解決しようとしているシンボル名がデフォルトとは異なることを C++ コンパイラに伝える必要があります。そうしないと、リンク手順が失敗します。
の extern "C" {}
コンストラクトは、中括弧内で宣言された名前に対してマングリングを実行しないようにコンパイラーに指示します。通常、C++ コンパイラは関数名を「拡張」して、引数と戻り値に関する型情報をエンコードします。これはと呼ばれます 壊れた名前. 。の extern "C"
構造が破損を防ぎます。
通常、C++ コードが C 言語ライブラリを呼び出す必要がある場合に使用されます。また、C++ 関数を (DLL などから) C クライアントに公開するときにも使用できます。
これは、名前のマングリングの問題を解決するために使用されます。extern C は、関数が「フラット」C スタイル API 内にあることを意味します。
逆コンパイルする g++
何が起こっているかを確認するために生成されたバイナリ
私はこの回答を次から移行しています。 C++ における extern "C" の効果は何ですか? その質問はこの質問と重複していると考えられたためです。
main.cpp
void f() {}
void g();
extern "C" {
void ef() {}
void eg();
}
/* Prevent g and eg from being optimized away. */
void h() { g(); eg(); }
GCC 4.8 Linux でコンパイルする 妖精 出力:
g++ -c main.cpp
シンボルテーブルを逆コンパイルします。
readelf -s main.o
出力には次の内容が含まれます。
Num: Value Size Type Bind Vis Ndx Name
8: 0000000000000000 6 FUNC GLOBAL DEFAULT 1 _Z1fv
9: 0000000000000006 6 FUNC GLOBAL DEFAULT 1 ef
10: 000000000000000c 16 FUNC GLOBAL DEFAULT 1 _Z1hv
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _Z1gv
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND eg
解釈
次のことがわかります。
ef
そしてeg
コード内と同じ名前のシンボルに保存されている他のシンボルは壊れていました。それらを解き明かしてみましょう:
$ c++filt _Z1fv f() $ c++filt _Z1hv h() $ c++filt _Z1gv g()
結論:次のシンボル タイプは両方とも ない 壊れた:
- 定義済み
- 宣言されているが未定義 (
Ndx = UND
)、リンクまたは実行時に別のオブジェクト ファイルから提供されます。
したがって、必要になります extern "C"
両方の通話時:
- C++ からの C:教えて
g++
によって生成された、マングルされていないシンボルを期待します。gcc
- C から C++:教えて
g++
分解されていないシンボルを生成するにはgcc
使用する
extern C で動作しないもの
名前のマングリングを必要とする C++ 機能は内部では動作しないことが明らかです。 extern C
:
extern "C" {
// Overloading.
// error: declaration of C function ‘void f(int)’ conflicts with
void f();
void f(int i);
// Templates.
// error: template with C linkage
template <class C> void f(C i) { }
}
C++ からの最小限の実行可能な C の例
完全を期すため、また初心者向けに、以下も参照してください。 C++ プロジェクトで C ソース ファイルを使用するにはどうすればよいですか?
C++ から C を呼び出すのは非常に簡単です。各 C 関数には、マングルされていないシンボルが 1 つしか存在しないため、追加の作業は必要ありません。
main.cpp
#include <cassert>
#include "c.h"
int main() {
assert(f() == 1);
}
ch
#ifndef C_H
#define C_H
/* This ifdef allows the header to be used from both C and C++. */
#ifdef __cplusplus
extern "C" {
#endif
int f();
#ifdef __cplusplus
}
#endif
#endif
c.c
#include "c.h"
int f(void) { return 1; }
走る:
g++ -c -o main.o -std=c++98 main.cpp
gcc -c -o c.o -std=c89 c.c
g++ -o main.out main.o c.o
./main.out
それなし extern "C"
リンクは次のように失敗します。
main.cpp:6: undefined reference to `f()'
なぜなら g++
壊れたものを見つけることを期待しています f
, 、 どれの gcc
生産しませんでした。
C からの最小限の実行可能な C++ の例
C++ を呼び出すのは少し難しくなります。公開したい各関数の非マングル バージョンを手動で作成する必要があります。
ここでは、C++ 関数のオーバーロードを C に公開する方法を説明します。
main.c
#include <assert.h>
#include "cpp.h"
int main(void) {
assert(f_int(1) == 2);
assert(f_float(1.0) == 3);
return 0;
}
cpp.h
#ifndef CPP_H
#define CPP_H
#ifdef __cplusplus
// C cannot see these overloaded prototypes, or else it would get confused.
int f(int i);
int f(float i);
extern "C" {
#endif
int f_int(int i);
int f_float(float i);
#ifdef __cplusplus
}
#endif
#endif
cpp.cpp
#include "cpp.h"
int f(int i) {
return i + 1;
}
int f(float i) {
return i + 2;
}
int f_int(int i) {
return f(i);
}
int f_float(float i) {
return f(i);
}
走る:
gcc -c -o main.o -std=c89 -Wextra main.c
g++ -c -o cpp.o -std=c++98 cpp.cpp
g++ -o main.out main.o cpp.o
./main.out
それなし extern "C"
次のように失敗します。
main.c:6: undefined reference to `f_int'
main.c:7: undefined reference to `f_float'
なぜなら g++
生成された壊れたシンボル gcc
見つけることができません。
Ubuntu 18.04でテストしました。