先の記事ではGoコードからCの関数を呼び出す(import)場合について見た。
この記事では逆にCの関数からGoのコードを呼び出す、つまりGoの関数をCにexportする場合を扱う。
ただし、ここで言うのはあくまでも「Goのパッケージの一部をCで実装するにあたって、CコードがGoの機能を利用できる」ということだ。CプログラムにGoライブラリを埋め込む話(-buildmode=c-archive
, -buildmode=c-shared
)とは別で、そっちの話は別途扱う予定だ。
今回の話では、プログラム全体はあくまでもGoで書かれてる前提で、前回と同様にそのごく一部だけをCで書くことを想定している。
サンプルコード
今度は次のような4つのファイルからなるパッケージ github.com/yugui/cgo-explained/example2を考える。
//export
ディレクティブでgoVersion
をCにexportするように指示している。
package main
import (
"C"
"runtime"
)
//export goVersion
func goVersion() string {
return runtime.Version()
}
void print_go_version(void);
#include <stdio.h>
#include "_cgo_export.h"
void print_go_version(void) {
const GoString version = goVersion();
printf("%.*s\n", (int)version.n, version.p);
}
package main
// #include "use_exported.h"
import "C"
func main() {
C.print_go_version()
}
exported.go
の機能をuse_exported.c
が利用しているのがこの記事で主に扱う部分である。ただし、プログラム自体はGoで書かねばならないため、Goのfunc main
がどこかでuse_exported.c
の機能を更にimportしていないとuse_exported.c
の存在する意味がない。このため、main.go
はこのようになっている。
ソースコードの依存関係は下図の通りである。
ビルドプロセス
ビルドプロセス自体はimportの場合と大差ない。
- コード生成
go tool cgo export_example.go
go tool cgo main.go
- Cコードのコンパイル
gcc -c SOURCES
- Cコードのリンク
gcc -o _cgo_.o OBJS
- インポート宣言の生成
go tool cgo -dynimport ....
- Goコードのコンパイル
go tool compile -o example2.a -pack GO_FILES
- Cコードの再リンク
gcc -o _all.o OBJS
- Cオブジェクトをアーカイブに追加
go tool pack r example2.a _all.o
- アーカイブ内のオブジェクトをリンク
go tool link -o example2 example2.a
データフローは下図の通りである。
これもimportの場合と概ね同じ構造であるが、下記2点が異なる。
-
go tool cgo
に掛けるソースファイルが2個に増えたので、その分生成されるファイルが増えて複雑になった -
use_exported.c
由来のオブジェクトファイルuse_exported.o
が加わった。これをコンパイルする際にgo tool cgo
で生成された_cgo_export.h
が#include
されている。
特に後者のほうを詳しく見てみよう。
use_exported.c
まず、exportされたGoの機能を利用するCソースuse_exported.c
は次のようになっている(再掲)。
#include <stdio.h>
#include "_cgo_export.h"
void print_go_version(void) {
const GoString version = goVersion();
printf("%.*s\n", (int)version.n, version.p);
}
ここで#include
している_cgo_export.h
はgo tool cgo
でexport_example.go
とmain.go
から生成された物だ。
/* Created by "go tool cgo" - DO NOT EDIT. */
/* (中略) */
/* Start of preamble from import "C" comments. */
/* End of preamble from import "C" comments. */
/* Start of boilerplate cgo prologue. */
/* (中略) */
typedef signed char GoInt8;
/* (中略) */
typedef struct { const char *p; GoInt n; } GoString;
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
/* (中略) */
extern GoString goVersion();
このファイルは主に3つの部分から構成される。
-
import "C"
の直前のコメントで書いたpreamble部のコピーpreambleに
//#include <stdio.h>
とか書いた場合だと、#include <stdio.h>
がこの部分にコピーされる訳だ。 -
Goの基本型をCで表現したもの (boilerplate cgo prologue)
-
//export
を宣言したGoの関数に対応する関数プロトタイプ
これらを利用してuse_exported.c
ではexport_example.go
内のfunc goVersion() string
を呼び出せるわけだ。ただし、正確に言うとfunc goVersion() string
と関数プロトタイプ内のgoVersion()
は同一ではない。それは次節で見てみよう。
グルーコード
まず、Cから見えるgoVersion()
は生成された_cgo_export.c
の中にある。これは引数を適切にpackしてGoの世界に引き渡すためのグルーコードである。
/* Created by cgo - DO NOT EDIT. */
#include "_cgo_export.h"
extern void crosscall2(void (*fn)(void *, int), void *, int);
extern void _cgo_wait_runtime_init_done();
extern void _cgoexp_5e3e4b09c83e_goVersion(void *, int);
GoString goVersion()
{
_cgo_wait_runtime_init_done();
struct {
GoString r0;
} __attribute__((__packed__)) a;
crosscall2(_cgoexp_5e3e4b09c83e_goVersion, &a, 16);
return a.r0;
}
crosscall2
の実体はruntime.cgo.crosscall2
でレジスタ待避などをやっている。_cgo_wait_runtime_init_done
はその名の通りruntime
パッケージの初期化が終わるまでブロックする。
そして、ここで呼び出されている_cgoexp_5e3e4b09c83e_goVersion
は_cgo_gotypes.go
にある。
/* (略) */
//go:linkname _cgo_runtime_cgocallback runtime.cgocallback
func _cgo_runtime_cgocallback(unsafe.Pointer, unsafe.Pointer, uintptr)
/* (略) */
//go:cgo_export_dynamic goVersion
//go:linkname _cgoexp_5e3e4b09c83e_goVersion _cgoexp_5e3e4b09c83e_goVersion
//go:cgo_export_static _cgoexp_5e3e4b09c83e_goVersion
//go:nosplit
//go:norace
func _cgoexp_5e3e4b09c83e_goVersion(a unsafe.Pointer, n int32) {
fn := _cgoexpwrap_5e3e4b09c83e_goVersion
_cgo_runtime_cgocallback(**(**unsafe.Pointer)(unsafe.Pointer(&fn)), a, uintptr(n));
}
func _cgoexpwrap_5e3e4b09c83e_goVersion() (r0 string) {
defer func() {
_cgoCheckResult(r0)
}()
return goVersion()
}
runtime.cgocallback
がスタック周りを調整しつつ、最終的にgoVersion
を呼び出している。
以上のグルーコードによってGoで定義した関数goVersion
をCの関数print_go_version
から呼び出すことができた。
まとめ
exportの場合もimportの場合とビルド仮定は大差ない。importの場合は割愛した_cgo_export.[ch]
が重要になるだけである。
実行時は_cgo_export.c
と_cgo_gotypes.go
にそれぞれ定義されている2段階のグルーコードを経てCの世界からGoの世界に制御が移る。