読者です 読者をやめる 読者になる 読者になる

C++のマングルとextern "C" {

C++とCが混在したプログラムを書くとときどき定義したはずの関数がundefinedだと言われることがある。そんなときの対処法とマングルの話。

その前にまずはC言語だけの場合を考える。例えば以下のようなCのプログラムを書いてみる。

/* main.c */
#include "foo.h"

int main(){
    func1();
    return 0;
}
/* foo.h */
#ifndef FOO_H

void func1();

#endif
/* foo.c */
#include "foo.h"
#include <stdio.h>

void func1(){
    puts("ok");
}

見てのとおり、main.cはfunc1関数を呼び出しており、foo.cはfunc1関数を定義している。当然、main.cをコンパイルしてできたmain.oもfunc1を参照し、foo.cをコンパイルしてできたfoo.oはfunc1を定義する。

オブジェクト中で使われているシンボルはnmコマンドで見ることができる。

$ gcc -c main.c
$ nm main.o
                 U func1
0000000000000000 T main
$ gcc -c foo.c
$ nm foo.o
0000000000000000 T func1
                 U puts

"U"になっているのは定義されていないもの、つまりそのシンボルを外部参照している。"T"になっているのはそのシンボルが定義されていることを意味する(正確には、"T"は「そのシンボルがテキスト(コード)セクションにある」という意味だが、細かいことは今回は関係ないので省略)。

f:id:wagavulin:20170209214327p:plain

このため、main.oとfoo.oをリンクさせてやれば、func1関数の呼び出しが可能になる。

$ gcc main.o foo.o
$ ./a.out
ok

C++の場合

C++の場合もCと同じような方法でシンボルの解決を行うが、Cとは違ってソースコード中の関数/変数名をそのままオブジェクトファイル中のシンボルにすることはできない。というのは、C++では同名の関数を複数作ることができるからだ。引数の型が異なれば同じ名前を使えるし(オーバーロード)、クラスや名前空間が異なればやはり同じ名前を使える。以下のような場合を考えよう。

// main.cpp
#include "foo.h"

int main(){
    func1();
    func1(10);
    Foo::func1();
}
// foo.h
#ifndef FOO_H
#define FOO_H

void func1();
void func1(int);

class Foo {
public:
    static void func1();
};

#endif // FOO_H
// foo.cpp
#include "foo.h"

void func1(){}

void func1(int){}

void Foo::func1(){}

このソースでは、func1という名前の関数が3つ存在する。そのため、func1という名前だけでは同定することができず、正しくリンクできなくなってしまう。そのため、C++コンパイラは、引数の型やクラス・名前空間名を使って修飾された一意な名前を作成し、オブジェクトファイルにはその修飾された名前を使う。これをマングル(mangle)と呼ぶ。

実際にコンパイルして見て見ると以下のようになる。

$ g++ -c main.cpp
$ nm main.o
                 U _Z5func1i
                 U _Z5func1v
                 U _ZN3Foo5func1Ev
0000000000000000 T main

$ g++ -c foo.cpp
$ nm foo.o
0000000000000007 T _Z5func1i
0000000000000000 T _Z5func1v
0000000000000012 T _ZN3Foo5func1Ev

f:id:wagavulin:20170209214328p:plain

なにやら複雑な名前になっているが、よく見ると"func1"や"Foo"という文字が見える。なお、マングルされた名前から元に戻すことをデマングルと呼び、nmコマンドでは--demangleでデマングルできる。

$ nm --demangle main.o
                 U func1(int)
                 U func1()
                 U Foo::func1()
0000000000000000 T main
$ nm --demangle foo.o
0000000000000007 T func1(int)
0000000000000000 T func1()
0000000000000012 T Foo::func1()

C++からCの関数を呼ぶ場合

C++からCの関数を呼ぶ場合、このマングル処理が問題になる。以下の例を考えよう。

// main.cpp
#include "foo.h"

int main(){
    func1();
}
/* foo.h */
#ifndef FOO_H
#define FOO_H

void func1();

#endif // FOO_H
/* foo.c */
#include "foo.h"

void func1(){}

main.cppはC++、foo.cはCで書かれている。これをビルドしてみよう。

$ g++ -c main.cpp
$ gcc -c foo.c
$ g++ main.o foo.o
main.o: In function `main':
main.cpp:(.text+0x5): undefined reference to `func1()'
collect2: error: ld returned 1 exit status

と、こんな風にエラーになってしまった。エラーメッセージによれば、func1()が見つからないらしいが、それはfoo.oにあるはずだ。

リンクできない理由はnmを使えば分かる。

$ nm main.o
                 U _Z5func1v
0000000000000000 T main
$ nm foo.o
0000000000000000 T func1

main.oはC++コンパイラが作ったので関数名がマングル,され"_Z5func1v"を参照しているが、foo.oはCコンパイラが作ったのでマングルされず、"func1"になっているのだ。

f:id:wagavulin:20170209214329p:plain

これを解決するには、C++コンパイラに対して、「func1はCの関数だからマングルしない名前で参照せよ」と命じる必要がある。これを行うのがextern "C" { ... }だ。これで囲まれた中で宣言された関数はCの関数とみなされ、マングルされずに使われる。従って、foo.hにある宣言をexter "C" { ... }で囲めば良い。

ただし、この構文はCコンパイラは理解できずエラーになる。そのため、__cplusplusマクロを使い、C++コンパイラから読まればときだけ有効になるようにする。

/* foo.h */
#ifndef FOO_H
#define FOO_H

#ifdef __cplusplus
extern "C" {
#endif

void func1();

#ifdef __cplusplus
}
#endif

#endif // FOO_H

これでようやくビルドできる。このextern "C" { ... }とインクルードガードはCのヘッダには必ず入れるようにしよう。

Cのヘッダにextern "C" {}がない場合

Cのヘッダにextern "C" {}が書かれておらず、かつそれが他人の作ったやつで変更できないような場合は、include文を囲むことで回避できる。上の例でfoo.hがC++対応していない場合、main.cppのinclude文を

extern "C" {
#include "foo.h"
}

とすれば良い。