👮

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

2020/12/03に公開

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 _1cBh6Fi_v _1cBh6Fic_v _1cBh6F_v
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" {, }を有効にする。ということになります。

GitHubで編集を提案

Discussion