2014/10/22

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*)という関数を考えます。

#include <cstdio>

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

これを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という関数名で呼び出しが可能です。

さてやっと本題です。そんな感じでC++のライブラリをCでラップしたのを書いたのですが、これをGoから呼び出すところで1日ほどハマりました。golangでC/C++のライブラリを使うときには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ソースコードを生成
  2. 6c, 6gコマンドで、cgoが生成したソースをコンパイルして.6ファイルを生成
  3. gccコマンドで、cgoが生成したソースをコンパイルして.oファイルを生成
  4. go tool packで.6ファイルと.oファイルを連結してgo形式の静的リンクファイル(command-line-arguments.a)を生成
  5. 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
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
}

すると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*)::*’ [-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*)::*’ [-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
#ifdef __cplusplus
extern "C" {
#endif

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

#ifdef __cplusplus
}
#endif

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

うされもん @acidlemonについて

|'-')/ acidlemonです。鎌倉市在住で、鎌倉で働く普通のITエンジニアです。

30年弱住んだ北海道を離れ、鎌倉でまったりぽわぽわしています。

外部サイト情報

  • twitter
  • github
  • facebook
  • instagram
  • work on kayac