Edited at
C++Day 5

CとC++が混在したプログラムでの注意点


C++ Advent Calendar 2018

このカレンダーの作成者(Gacchoさん)に招待されたので書かせていただきます😇

C/C++を専門としてる者ではないので、周知されていることを書いてしまいそうですが、ご了承ください...。


前後の記事

前日の記事は、@niinaさんの「メンバ関数をもっと分ける」でした。

次日の記事は、@h6akhさんの「macOS High SierraでC++ REST SDKの導入に苦労したのでメモを残す」です。


CとC++の混在は難しい

なんせ自分もそうだったんですが、CとC++が混在しているプログラムを分割コンパイルし、オブジェクトファイルから実行ファイルを作る時に、時々定義したはずの関数が undefined となりコンパイルに失敗することがありました。


C++コンパイラ

よくよく調べていると、C++コンパイラでは名前マングリングと呼ばれる処理を行なっているようです。これは名前修飾とも呼ばれているようで、


名前修飾(なまえしゅうしょく、name mangling)は、現代的なコンピュータプログラミング言語処理系で用いられている手法で、サブルーチン(関数)名などに対する内部名を、その表層的な名前だけではなく、関数であればその引数の型や返戻値の型などといった意味的な情報を含めて修飾した(manglingした)名前とするものである。


つまりは、ソース上で書いた関数名をそのままシンボル名にするのではなく、「引数の型」「返り値の型」など、その関数に関連する情報も追加したものを「シンボル名」にするということです。

なのでCとC++でのコンパイルした際の、シンボル名の違いを確認してみます。


test.cpp

#include <iostream>


void example_function();

int main ( void ) {
example_function();
return 0;
}



test.c

#include <stdio.h>


void example_function();

int main ( void ) {
example_function();
return 0;
}


それぞれををgccでコンパイルして、nmコマンドを使ってシンボルテーブルを確認してみます。


nmコマンド


nm は、UNIXや類似のオペレーティングシステムに存在するコマンドであり、バイナリファイル(ライブラリ、実行ファイル、オブジェクトファイル)の中身を調べ、そこに格納されているシンボルテーブルなどの情報を表示する。デバッグに使われることが多く、識別子の名前の衝突問題やC++の名前修飾の問題を解決する際に補助として用いられる。

GNUプロジェクトでは、高機能の nm プログラムを GNU Binutils パッケージの一部として提供している。この nm コマンドは他のツールと同様に特定のコンピュータ・アーキテクチャとバイナリフォーマット向けにコンパイルされているので、セキュリティ専門家は疑わしいバイナリファイルを調査するためにネイティブでない nm コマンドを事前に取り揃えておくことが多い。


$ gcc -c test.c

$ nm c.o
U _example_function
0000000000000000 T _main

$ gcc -c test.cpp
$ nm c.o
U __Z16example_functionv
0000000000000000 T _main

このように、Cの方では、シンボル名 = 定義した関数名ですが、C++ではシンボル名 ≠ 定義した関数名です。これがコンパイラによって情報を付加されたシンボル名です。付加される情報はコンパイラに依存します。

コンパイラ
void h(int)
void h(int, char)
void h(void)

GNU GCC 3.x
_Z1hi
_Z1hic
_Z1hv

GNU GCC 2.9x
h__Fi
h__Fic
h__Fv

Intel C++ 8.0 for Linux
_Z1hi
_Z1hic
_Z1hv

Microsoft VC++ v6/v7
?h@@YAXH@Z
?h@@YAXHD@Z
?h@@YAXXZ

Borland C++ v3.1
@h$qi
@h$qizc
@h$qv

OpenVMS C++ V6.5 (ARM mode)
H__XI
H__XIC
H__XV

OpenVMS C++ V6.5 (ANSI mode)
CXX$__7H__FI0ARG51T
CXX$__7H__FIC26CDH77
CXX$__7H__FV2CB06E8

OpenVMS C++ X7.1 IA-64
CXX$_Z1HI2DSQ26A
CXX$_Z1HIC2NP3LI4
CXX$_Z1HV0BCA19V

Digital Mars C++
?h@@YAXH@Z
?h@@YAXHD@Z
?h@@YAXXZ

SunPro CC
_1cBh6Fiv_
_1cBh6Ficv_
_1cBh6Fv_

HP aC++ A.05.55 IA-64
_Z1hi
_Z1hic
_Z1hv

HP aC++ A.03.45 PA-RISC
h__Fi
h__Fic
h__Fv

Tru64 C++ V6.5 (ARM mode)
h__Xi
h__Xic
h__Xv

Tru64 C++ V6.5 (ANSI mode)
__7h__Fi
__7h__Fic

このコンパイラによって変換されたシンボル名をデマングルします。c++filtコマンドを使うことによってデマングルすることができます。


c++filt


test.cpp

#include <iostream>


void test_function ();
void test_function2 ( int val );
void test_function3 ( double val );
int test_function4 ( float val );

int main ( void ) {
test_function ();
test_function2 (1);
test_function3 (1.0);
test_function4 (1.0);
return 0;
}


$ gcc -c test.cpp

$ nm test.o
U __Z13test_functionv
U __Z14test_function2i
U __Z14test_function3d
U __Z14test_function4f
0000000000000000 T _main

$ nm test.o | c++filt
U test_function()
U test_function2(int)
U test_function3(double)
U test_function4(float)
0000000000000000 T _main

c++filtのオプションでコンパイラタイプを選択できるので、もし、うまくデマングルできない場合には--formatで明示的に指定してください。

Usage: c++filt [options] [mangled names]

Options are:
[-_|--strip-underscore] Ignore first leading underscore (default)
[-n|--no-strip-underscore] Do not ignore a leading underscore
[-p|--no-params] Do not display function arguments
[-i|--no-verbose] Do not show implementation details (if any)
[-t|--types] Also attempt to demangle type encodings
[-s|--format {none,auto,gnu,lucid,arm,hp,edg,gnu-v3,java,gnat}]
[@<file>] Read extra options from <file>
[-h|--help] Display this information
[-v|--version] Show the version information
Demangled names are displayed to stdout.
If a name cannot be demangled it is just echoed to stdout.
If no names are provided on the command line, stdin is read.

ただ、nmコマンドには--demangleってオプションがあるので、c++filtを使う必要はないのですが :(


CのモジュールをC++から呼び出す場合に起きる問題

さてここで、本題に戻りますが、C/C++でこのような違いがある中で、それぞれの言語を混在させてコンパイルをした場合どうなるのでしょうか。マングリング自体はコンパイラが良かれと思ってしてくれるものとして、実際にこのマングリングが問題になるケースとしてあげられるのは「Cのモジュール(関数)をC++側から呼び出す」場合である。

C++からCのモジュールを呼び出した場合、CのオブジェクトファイルやライブラリをC++のオブジェクトファイルにリンクする必要があります。

元の(C++側の)オブジェクトファイルがC++なので、扱う範囲(名前空間というかリンケージ)はC++のものが適用されます。そのため、リンカにはC++対応(g++)などを使う必要があります。

リンク時に、リンカであるg++はマングリングされたシンボルを期待する。しかし、CのモジュールがCのソースであり、ソースがgccによって生成された場合には、Cソースのオブジェクトファイルのシンボルテーブルには生のシンボル(関数名)が登録されているため、以下のようにリンクを行うタイミングでビルドが失敗することになる。

g++ -Wall    run.cc libhello.a   -o run

/tmp/ccrKBE8s.o: In function `main':
run.cc:(.text+0x10): undefined reference to `C_SOURCE_FUNCTION()'

collect2: error: ld returned 1 exit status
make: *** [run] Error 1


C++から呼び出されるモジュールを"C"リンケージに登録する

Cでは「Cリンケージ」がありますが、Cを含むC++では「C++リンケージ/Cリンケージ」の2つが存在することになります。リンケージを指定しない場合には、デフォルトで「C++リンケージ」にシンボルが登録されるようになっています。そのため「extern "C" { ... }」を用いて、C++から呼び出される関数を"C"リンケージに登録する必要があります。この「extern "C"」を使うことにより、シンボル名がマングルングされる前の名前になり、C/C++混合のシステムでも正常にリンク処理を行うことができます。


externなしのコード

#include <iostream>


void test_function ();
void test_function2 ( int val );
void test_function3 ( double val );
int test_function4 ( float val );
void test_function5 ( int a );

int main ( void ) {
test_function ();
test_function2 (1);
test_function3 (1.0);
test_function4 (1.0);
test_function5 (1);
return 0;
}



externなし

$ gcc -c test.cpp

$ nm test.o
U __Z13test_functionv
U __Z14test_function2i
U __Z14test_function3d
U __Z14test_function4f
U __Z14test_function5i
0000000000000000 T _main


externありのコード

#include <iostream>


extern "C" void test_function ();
extern "C" void test_function2 ( int val );
void test_function3 ( double val );
int test_function4 ( float val );
extern void test_function5 ( int a );

int main ( void ) {
test_function ();
test_function2 (1);
test_function3 (1.0);
test_function4 (1.0);
test_function5 (1);
return 0;
}



externあり

$ gcc -c extern_test.cpp

$ nm extern_test.o
U __Z14test_function3d
U __Z14test_function4f
U __Z14test_function5i
0000000000000000 T _main
U _test_function
U _test_function2

このように「extern "C"」を指定した関数のみ、マングリングされる前の名前がシンボル名に指定されていることが確認できます。


C++からCモジュールを呼び出すときのまとめ

結局のところ、C++からCのモジュールを呼び出す際にはextern "C"を用いるということです。なぜなら、C++コンパイラでは、マングリングによって、シンボル名が関数名ではなくなり、Cコンパイラとの互換性がなくなってしまうからです。


一般的な使い方?

#includeするときに、そのソースごとextern "C"をかけます。つまりはこう

extern "C" {

#include "aaaaaa.h"
#include "bbbbbb.h"
}

もしくは、C++コンパイラに、Cの関数でありマングリングをさせないようにしたければ、以下のようにもできます。

#ifdef __cplusplus

extern "C" {
#endif

void func1();

#ifdef __cplusplus
}
#endif

つまりは、__cplusplusが定義されている(C++コンパイラの)場合のみ、extern "C" {, }を有効にする。ということになります。


おわりに

なんか拙い記事ですいません...^^;

ブログやってます!よかったら覗いて行ってください!!

http://nomunomu.hateblo.jp/