cgoでGolangとC++ライブラリをリンクするとき、何が起きているのか

関東も秋が深まり、「紅葉を見にいこうよう」と言ってスベるシーズンがやってきました。みなさんいかがお過ごしでしょうか、れもんです。

さて、自社サイトでGoやるよって発表したので最近はずっとGoを書いているのですが、ついに難題がやってきました。C++で書かれたライブラリをGoで使うというやつです。今日は、GoからC++のライブラリを使おうとするときに何が起きているのかという話と、それゆえにこのオプションを指定するとドツボにはまるのでやめた方がいいよという話です。

GoからC++を使うときの基本的な考え方はRubyとかPerlでC++のライブラリを使うときと同じです。なので、いつものセオリーでやってみることにしました。まぁC++なら素直にSWIG使えよって話もあるんですが、何事も最初は挑戦だってことで手で書きます。

そのいつものセオリーとは何かというと、C++のライブラリをCインタフェースで使えるブリッジを書いてCライブラリとして別言語から使う、というやつですね。基本的にC++でライブラリを作ると、明示しない限りリンケージはC++になっていますので、別言語で使おうとするとマングリング問題で呼ぶのが大変です。なので、 extern "C" したCスタイルの関数でC++オブジェクトのポインタを返して、オブジェクトのメンバ関数を呼ぶのは第一引数をオブジェクトのポインタにしたC関数経由にする…というやつです。WindowsのCOMと似たような感じですね。

## 2分でわかるマングリング

マングリング is 何という人のために簡単に説明します。分かってる人は読み飛ばしてオッケーです。簡単のために、クラスのメンバ関数じゃなくて普通のグローバルな関数で void Hoge(int, const char*) という関数を考えます。

<code class="cpp">#include &lt;cstdio&gt;

void Hoge(int i, const char* str) {
    std::printf("i is %d, str is %s", i, str);
}
</code>```

これを `gcc -c test.cpp -o test.o` でコンパイルして中間ファイルを生成して、中のシンボルをみる `nm` コマンドで調べてみます。

$ nm test.o 0000000000000000 T _Z4HogeiPKc U printf```

Hoge 関数が _Z4HogeiPKc というシンボル名で格納されています。これが、C++のマングリング(名前修飾)です。なぜこうなるかというと、C++は名前同じで引数が違う関数を許容するので名前がシンボル名になってるとリンクする関数を解決できないからですね。このよくわからん _Z4HogeiPKc という文字列は、デマングルすることで可読性のある文字列に戻すことができます。それが c++filt コマンドです。

$ nm test.o | c++filt
0000000000000000 T Hoge(int, char const*)
                 U printf```

こんな感じで読めるようになりました。ドヤッ!

で、別言語から呼ぶには_Z4HogeiPKcという関数名で呼べばいいんですが、さすがにマングリング規則を覚えてマングリングした関数名をすぐ書ける人はほとんどいないはずです。そこで使うのが `extern "C"` というリンケージ方法の変更です。さっきのソースコードをこう変更します。

#include <cstdio>

extern “C” {

void Hoge(int i, const char* str) { std::printf(“i is %d, str is %s”, i, str); }

}```

これをもう一度コンパイルしてnmコマンドにかけると、シンボル名が変わります。

$ nm test.o
0000000000000000 T Hoge
                 U printf```

この状態なら、他の言語から `Hoge` という関数名で呼び出しが可能です。

</div>
さてやっと本題です。そんな感じでC++のライブラリをCでラップしたのを書いたのですが、これをGoから呼び出すところで1日ほどハマりました。golangでC/C++のライブラリを使うときには[cgo](http://golang.org/cmd/cgo/)という仕組みでライブラリをリンクします。

このとき、Goのソースコードで `import "C"` する前にコメントでCヘッダと `#cgo` ディレクティブを書くわけですが、これを書くと絶対失敗するので書いてはいけないオプションというのがあるのでそれのご紹介、というのが今日の本題です。

それは何かというと、**リンクするライブラリがC++だからといって、 `#cgo CFLAGS` で `-x c++` を指定してはいけない**という点です。

逆にこれを指定するとどういう悲劇が起こるかを知るには、cgo(gc)のビルドプロセスを知る必要があります。今回は最新のgo 1.3.3と、gcc 4.9.1を使っています(なお、go 1.2.2, gcc 4.4.7を使っても同じです)。

Goのビルドプロセスは `go [build|run]` に `-x` オプションをつけて実行すると実際に実行しているコマンドが見えます。長いのでここに掲載するのは省略しますが、その処理の中身を見ていくと、こんな感じです(x86-64環境なので6c/6lを使います)。

1.  `import "C"` しているgoのソースにcgoコマンドを実行して_objディレクトリにGoとCの間の呼び出しをブリッジするCソースコードを生成
1. 6c, 6gコマンドで、cgoが生成したソースをコンパイルして.6ファイルを生成
1. gccコマンドで、cgoが生成したソースをコンパイルして.oファイルを生成
1. go tool packで.6ファイルと.oファイルを連結してgo形式の静的リンクファイル( `command-line-arguments.a` )を生成
1. go形式の静的リンクファイルを6lコマンドにかけてネイティブ実行ファイルを生成

で、問題はCコンパイラ向けのCFLAGSに `-x c++` を指定したときに何が起こるかという話です。

こんな感じのGoとCヘッダをを書きます。前提として、別途Makefileでgccを使ってコンパイルする `bridge.cpp` が別にあって、そこから `bridge.a` を生成しています。で、それを静的ファイルとしてGoが吐き出すバイナリに静的リンクするように `#cgo LDFLAGS` で指定しています。

$ cat bridge.h extern “C” {

typedef void* App; App CreateApp(); void Run(App);

}```

$ cat main.go
<code class="golang">package main
/*
#cgo CFLAGS: -x c++ -fpermissive
#cgo LDFLAGS: -lrt -lstdc++ bridge.a
#include "bridge.h"
*/
import "C"
func main() {
    app := C.CreateApp()   // create C++ app object
    C.Run(app)             // enter main loop
}
</code>```

するとcgoはこんな感じのグルーコードを生成します。このコードは `go build -x` で出てきたコマンドを1個ずつ手で実行しないとお目にかかれません。

$ cat main.cgo2.c #line 2 “(…)/src/main.go”

#include “bridge.h”

// Usual nonsense: if x and y are not equal, the type will be invalid // (have a negative array count) and an inscrutable error will come // out of the compiler and hopefully mention “name”. #define __cgo_compile_assert_eq(x, y, name) typedef char name[(x-y)(x-y)-2+1];

// Check at compile time that the sizes we use match our expectations. #define __cgo_size_assert(t, n) __cgo_compile_assert_eq(sizeof(t), n, cgo_sizeof##t##is_not##n)

__cgo_size_assert(char, 1) __cgo_size_assert(short, 2) __cgo_size_assert(int, 4) typedef long long __cgo_long_long; __cgo_size_assert(__cgo_long_long, 8) __cgo_size_assert(float, 4) __cgo_size_assert(double, 8)

#include <errno.h> #include <string.h>

void _cgo_32105cabc2a6_Cfunc_CreateApp(void *v) { struct { App r; } attribute((packed, gcc_struct)) *a = v; a->r = CreateApp(); }

void _cgo_32105cabc2a6_Cfunc_Run(void *v) { struct { App p0; } attribute((packed, gcc_struct)) *a = v; Run(a->p0); } ```

で、この生成された main.cgo2.c をgccでコンパイルするときのオプションは #cgo CFLAGS で指定したものも使われるので、ここで -x c++ を指定してしまうと、このCファイルがC++としてコンパイルされてしまい、リンケージがC++になってしまうわけです。ついでにいうと、C++的にはよくないキャストが含まれているので、-fpermissiveを指定しないとコンパイルが通りません。

ということで、これを実行するとこんな感じになります。

$ go run main.go
# command-line-arguments
./echo_client.go: In function ‘void _cgo_32105cabc2a6_Cfunc_CreateApp(void*)’:
./echo_client.go:36:53: warning: invalid conversion from ‘void*’ to ‘_cgo_32105cabc2a6_Cfunc_PrepareApp(void*)::<anonymous struct>*’ [-fpermissive]

                                                     ^
./echo_client.go: In function ‘void _cgo_32105cabc2a6_Cfunc_Run(void*)’:
./echo_client.go:46:53: warning: invalid conversion from ‘void*’ to ‘_cgo_32105cabc2a6_Cfunc_Run(void*)::<anonymous struct>*’ [-fpermissive]

                                                     ^
# command-line-arguments
/var/tmp/go-link-sVLoaz/go.o: In function `main._Cfunc_Run':
(.text+0x39a): undefined reference to `_cgo_32105cabc2a6_Cfunc_Run'
collect2: error: ld returned 1 exit status
/usr/local/go/pkg/tool/linux_amd64/6l: running gcc failed: unsuccessful exit status 0x100

いやいや、ちゃんと自動生成したCソースに _cgo_32105cabc2a6_Cfunc_Run あるしそれエラーになるわけないじゃない…と昨日1日ずーっと悩みました。で今朝電車でゆらゆらしながらずーっと考えてたんですが、よく考えたらcgoのCFLAGS指定はすべてのgcc呼び出しに付加されているので、自動生成したソースコードをC++としてコンパイルさせてしまっていたのでした。そりゃリンケージがC++になるからメソッド見つからなくなるわ! という話ですね。

では、最初になぜ -x c++ -fpermissive をつけてドツボにハマリにいってしまったかというと、ヘッダにcgoのヘッダ部分に extern "C" を書いてその状態でコンパイルが通る様にするためだったんですね。Cコンパイラは extern "C" を解釈できません。ということは、CじゃなくてC++としてコンパイルしないとダメなんだなーということでcgoディレクティブを追加したのですが、そもそもcgoの吐くソースはCとしてコンパイルしないとダメなので、これは筋が悪かったというところです。

ではどうすればよかったというと、 bridge.hextern "C" するのをちゃんと #ifdef __cplusplus のときだけに絞るという、当たり前のことを当たり前のようにやればよい、ということでした。

$ cat bridge.h
<code class="cpp">#ifdef __cplusplus
extern "C" {
#endif

typedef void* App;
App CreateApp();
void Run(App);

#ifdef __cplusplus
}
#endif
</code>```

ということで、今日はGoがcgoを使ってネイティブコンパイルするときの流れと、指定すると絶対失敗するのでやめたほうがよい `CFLAGS` のオプションについてご紹介しました。ここ2日くらいこれが解決する気配なくて本当につらかった…。

この記事をシェア

acidlemonのアバター

acidlemon

|'-') れもんです。鎌倉市在住で、鎌倉で働く普通のITエンジニアです。 30年弱住んだ北海道を離れ、鎌倉でまったりぽわぽわしています。

こういうのも読む?