cgoを用いるとCのライブラリをGoバイナリにリンクしたり、Goパッケージの一部をCで書いたりできる。更にGo 1.5以降では、GoのパッケージをC用の静的ライブラリまたは動的ライブラリにまとめておいて、Cからリンクすることもできる。
これらの機能はすべてgo build
コマンドに統合されているので、普段は特にcgoを使っていることを意識することは少ない。しかし、pure goのコードのビルドにしたところでその裏側ではコンパイラ、アセンブラ、リンカが走っているわけである。ではcgoの場合をこの水準で見るとどのような処理が行われているのだろうか。
要は、gcc(1)の裏ではcc1, as, collect2なんかが走ってるよね、cgoではどうなってるの? という話が本稿の話題である。
なお、Goのオブジェクトファイルがプラットフォーム独立な(ELFとかではない)フォーマットであることや、Goのパッケージをビルドすると独自メタデータ付きのarアーカイブにまとめられることなどは既知とする。
まず今回はCライブラリをGoにリンクする場合について見てみよう。なお検証はGo 1.6で行っている。
サンプルコード
次のような2つのファイルから成るGoパッケージgithub.com/yugui/cgo-explained/example1
を考える。
この程度であれば実際にはファイルを分ける必要はないが、あとのリンクプロセスを見るために意図的にcgoを含まないgoファイルmain.go
を作った。
package main
import (
//#cgo LDFLAGS: -lm
//#include <math.h>
"C"
"fmt"
)
func printSqrt(n int) {
fmt.Println(C.sqrt(C.double(n)))
}
package main
func main() {
printSqrt(10)
}
ビルドプロセス
上のパッケージをgo build github.com/yugui/cgo-explained/example1
とビルドする場合、次のようなプロセスが走る。
各ステップの詳細については後述する。
- コード生成
go tool cgo import_example.go
- Cコードのコンパイル
gcc -c SOURCES
- Cコードのリンク
gcc -o _cgo_.o OBJS
- インポート宣言の生成
go tool cgo -dynimport ....
- Goコードのコンパイル
go tool compile -o example1.a -pack GO_FILES
- Cコードの再リンク
gcc -o _all.o OBJS
- Cオブジェクトをアーカイブに追加
go tool pack r example1.a _all.o
- アーカイブ内のオブジェクトをリンク
go tool link -o example example1.a
データフローは次のようになっている。各ファイルについては追々見ていく。
コード生成
まずgo build
はパッケージ内の.go
ファイルのうちcgoを利用しているファイルのみをcgo
コマンドに渡す。
上例では対象はimport_example.go
だけで、概ね次のようなコマンドが走る。
複数のファイルがcgoを利用している場合はすべてまとめてコマンドライン引数として引き渡す。
$ env CGO_LDFLAGS=-lm go tool cgo -objdir $tmpobjdir \
> -importpath github.com/yugui/cgo-explained/example1 \
> import_example.go
このコマンドは$tmpobjdir
ディレクトリに次のファイルを生成する。
-
_cgo_export.c
: 後で登場するが、今回は大した情報は入っていない。 -
_cgo_export.h
: 同上 -
_cgo_flags
: 環境変数から収集したCGO_LDFLAGS
などを保存する。cgoのstraceやソースを読む限り、保存しただけで誰も使ってない気がひしひしする。 -
_cgo_gotypes.go
: Cの関数を呼び出すためのthunkが入っている。 -
_cgo_main.c
: 後述の_cgo_.o
をリンクするためのdummyに見える。 -
import_example.cgo1.go
:
元のimport_example.go
からGo部分を抜き出したもの。Cの型や関数は_cgo_gotypes.go
で定義されたものに置き換えられている。 -
import_example.cgo2.c
:
_cgo_gotypes.go
に対応するC側のラッパーがある。
元のimport_example.go
でimport "C"
の前にコメントとして書いたCコード片もここに来るらしい。
なお、このcgo
コマンドは内部で更にCプリプロセッサを呼び出してimport "C"
のところのCコード片を処理する。これによってGo側で呼び出した関数やGo側で参照した型が実際にはマクロであるケースや、それらが#if
節内で宣言されていてもきちんと処理できるわけである。
_CGO_CFLAGS=
_CGO_LDFLAGS=-lm
//go:cgo_ldflag "-lm"
// Created by cgo - DO NOT EDIT
package main
import "unsafe"
import _ "runtime/cgo"
import "syscall"
var _ syscall.Errno
func _Cgo_ptr(ptr unsafe.Pointer) unsafe.Pointer { return ptr }
//go:linkname _Cgo_always_false runtime.cgoAlwaysFalse
var _Cgo_always_false bool
//go:linkname _Cgo_use runtime.cgoUse
func _Cgo_use(interface{})
type _Ctype_double float64
type _Ctype_void [0]byte
//go:linkname _cgo_runtime_cgocall runtime.cgocall
func _cgo_runtime_cgocall(unsafe.Pointer, uintptr) int32
/* 中略 */
//go:cgo_import_static _cgo_5df64109ab52_Cfunc_sqrt
//go:linkname __cgofn__cgo_5df64109ab52_Cfunc_sqrt _cgo_5df64109ab52_Cfunc_sqrt
var __cgofn__cgo_5df64109ab52_Cfunc_sqrt byte
var _cgo_5df64109ab52_Cfunc_sqrt = unsafe.Pointer(&__cgofn__cgo_5df64109ab52_Cfunc_sqrt)
func _Cfunc_sqrt(p0 _Ctype_double) (r1 _Ctype_double) {
_cgo_runtime_cgocall(_cgo_5df64109ab52_Cfunc_sqrt, uintptr(unsafe.Pointer(&p0)))
if _Cgo_always_false {
_Cgo_use(p0)
}
return
}
int main() { return 0; }
void crosscall2(void(*fn)(void*, int), void *a, int c) { }
void _cgo_wait_runtime_init_done() { }
char* _cgo_topofstack(void) { return (char*)0; }
void _cgo_allocate(void *a, int c) { }
void _cgo_panic(void *a, int c) { }
void _cgo_reginit(void) { }
// Created by cgo - DO NOT EDIT
//line /home/vagrant/go/src/github.com/yugui/cgo-explained/example1/import_example.go:1
package main
//line /home/vagrant/go/src/github.com/yugui/cgo-explained/example1/import_example.go:4
//line /home/vagrant/go/src/github.com/yugui/cgo-explained/example1/import_example.go:3
import (
//line /home/vagrant/go/src/github.com/yugui/cgo-explained/example1/import_example.go:7
"fmt"
)
//line /home/vagrant/go/src/github.com/yugui/cgo-explained/example1/import_example.go:11
//line /home/vagrant/go/src/github.com/yugui/cgo-explained/example1/import_example.go:10
func printSqrt(n int) {
fmt.Println(_Cfunc_sqrt(_Ctype_double(n)))
}
#line 4 "/home/vagrant/go/src/github.com/yugui/cgo-explained/example1/import_example.go"
#include <math.h>
/* (中略) */
void
_cgo_5df64109ab52_Cfunc_sqrt(void *v)
{
struct {
double p0;
double r;
} __attribute__((__packed__, __gcc_struct__)) *a = v;
char *stktop = _cgo_topofstack();
__typeof__(a->r) r = sqrt(a->p0);
a = (void*)((char*)a + (_cgo_topofstack() - stktop));
a->r = r;
}
Cコードのコンパイル/リンク
これは通常通りの手順で、先に生成された_cgo_export.c
, _cgo_main.c
, import_example.cgo2.c
を各々コンパイルした後、リンクして実行ファイル_cgo_.o
を生成する。import_example.go
で指定したLDFLAGS
の-lm
はここでgcc
に引き渡されているので、無事にsqrt(3)
を解決できる。
ここで重要なのはCのリンカにシンボルの解決を丸投げしてローダブルなオブジェクトを作ることである。実行ファイルをビルドしているのはその手段にすぎない。要は一度リンクさせてみてからldd
的なことをしたいのだろう。
インポート宣言の生成
上の_cgo_.o
を入力として次のようにcgo
コマンドを呼び出している。
$ go tool cgo -objdir $tmpobjdir -dynpackage main \
> -dynimport $tmpobjdir/_cgo_.o -dynout $tmpobjdir/_cgo_import.go
生成されたファイルは次のようである。
どうもGoのリンカに渡すディレクティブのようだ。
package main
//go:cgo_import_dynamic __libc_start_main __libc_start_main#GLIBC_2.2.5 "libc.so.6"
//go:cgo_import_dynamic sqrt sqrt#GLIBC_2.2.5 "libm.so.6"
//go:cgo_import_dynamic _ _ "libm.so.6"
//go:cgo_import_dynamic _ _ "libc.so.6"
Goコードのコンパイル
パッケージ内でcgoを含まない.goファイルと、最初のステップで生成されたGoコードと、先ほど作った_cgo_import.go
をまとめてコンパイルする。
$ go tool compile -o example1.a \
> -p github.com/yugui/cgo-explained/example1 -pack \
> _cgo_gotypes.go import_example.cgo1.go _cgo_import.go main.go
この時点ではexample1.a
の中身は下記の通り
$ ar x example1.a
__.PKGDEF
_go_.o
Cコードの再リンク
先ほどコンパイルしたCのオブジェクトファイルをリンクし直して別のファイル_all.o
を作る。
先ほどの_cgo_.o
との違いは_cgo_main.o
や依存ライブラリは一切リンクしないことだ。-Wl,-r
を指定しているので、ほぼ_cgo_export.o
とimport_example.cgo2.o
を1つのファイルにまとめているだけである。
$ gcc -nostdlib -o _all.o _cgo_export.o import_example.cgo2.o -Wl,-r
Cオブジェクトをアーカイブに追加
_all.o
をexample.a
に追加する。
他のステップと違ってgo build
はサブプロセスを呼び出してはいないようだが、概ね次のようなコマンドに相当するだろう。
$ go tool pack r example1.a $tmpobjdir/_all.o
アーカイブ内のオブジェクトをリンク
さて、最後にアーカイブ内のオブジェクトや依存パッケージのオブジェクトをリンクする。
$ go tool link -o example1 example1.a
このGoリンカはcgoを使っていない場合は自分で最後までバイナリをリンクする。
しかし、今回はC由来の_all.o
をリンクするために一度ELFに落としてからgccに丸投げする。
結果として次のようなサブプロセスが走る。
$ gcc -m64 -gdwarf-2 -o example1 -rdynamic \
> /tmp/go-link-978656076/go.o /tmp/go-link-978656076/000000.o \
> /tmp/go-link-978656076/000001.o -lm -g -O2 -lpthread
まとめ
先のデータフローをもう一度見てみよう。
go buildはうまくCコンパイラツールチェインを利用していることが分かる。
まずグルーコードを生成する。それからCの世界はCコンパイラ/リンカに任せてすべての依存関係を解決させ、_cgo_.o
を作る。次に、_cgo_.o
を解析して_cgo_import.go
のためのデータを抽出する。
さらに、その後もGoとCは分離したまま、それぞれ1つずつのオブジェクトファイルにまとめる。そして最後に実行ファイルを作る段になって両者をリンクする。ここでもlibm
みたいなCの世界とやりとりする必要があるのでライブラリのロードパスやらはすべてCのリンカに丸投げである。
次回予告
以上でめでたく実行ファイルexample1
を得られた。次回はGoの関数をCのソースコードが利用する場合について見てみたい。