cgo callback サンプル
概要
本サンプルは、cgoを利用してCの関数からGoの関数をコールバックする方法を示します。
Goの main
関数からCで実装された c_func
を呼び出します。このとき、Goで実装されエクスポートされた export_func
の関数ポインタを引数として渡します。
C側の c_func
は、受け取った関数ポインタを利用してGoの export_func
を呼び出し、その結果を受け取ってGoの main
関数に返します。
処理の流れ
- Goの
main
関数が開始されます。
- Goの
main
関数は、Cの関数 c_func
を呼び出します。引数として、整数 x
, y
と、Goの関数 export_func
への関数ポインタを渡します。
- Cの
c_func
は、受け取った関数ポインタ fn
を使って、Goの関数 export_func(x, y)
を呼び出します(コールバック)。
- Goの
export_func
が実行され、x
と y
の乗算結果を計算し、Cの c_func
に返します。
- Cの
c_func
は、 export_func
から受け取った値を、呼び出し元であるGoの main
関数に返します。
- Goの
main
関数は、最終的な結果を受け取り、標準出力に表示します。
実行方法
go run .
実行結果
[C ] x=2, y=3
[Go] sleep 1sec
[Go] x=2, y=3, ans=6
[C ] ans=6
[Go] z=6
Goの関数ポインタの取得について
main.go
の中で、Cの関数 c_func
に渡すために export_func
の関数ポインタを取得している以下のコードは、cgo特有のテクニックです。
fn := (*[0]byte)(C.export_func)
Cのコードでは、c_func
の第3引数 fn
は int (*fn)(int, int)
という関数ポインタ型として定義されています。
このため、Go側から c_func
を呼び出す際には、Goの関数 export_func
のメモリアドレス(ポインタ)を渡す必要があります。
しかし、cgoの仕様上、C.export_func
という記述は、Goの世界では関数そのものを表しますが、直接ポインタを指すものではありません。そこで、一度 unsafe.Pointer
を経由して型変換を行う必要がありますが、より一般的に使われるイディオムが (*[0]byte)(C.export_func)
というキャストです。
これは、export_func
を「長さ0のbyte配列へのポインタ」に変換することを意味します。Goのコンパイラは、このキャストを特別に解釈し、export_func
の先頭アドレスを指すポインタを生成します。これにより、C側が期待する関数ポインタとして正しく渡すことができます。
補足: エクスポートしていないGo関数を渡す場合
//export
ディレクティブを付けていないGoの関数(プライベートな関数やメソッドなど)を、Cの関数ポインタとして直接渡すことはできません。Cから見えるシンボルとして公開されていないためです。
このようなケースでは、cgo.Handle
という仕組みを使って間接的に実現するのが一般的な方法です。これは、Goの値をC側で安全に参照するための「ハンドル」という整数値に変換する機能です。
手順の概要は以下の通りです。
- Go:
cgo.NewHandle()
を使い、コールバックさせたいGoの関数からハンドル(整数)を作成します。
- Go: Cの関数を呼び出す際に、このハンドルを
void*
などにキャストして渡します。
- C: Cの関数は、Goの関数ポインタの代わりにこのハンドルを受け取ります。
- C: コールバックが必要な場面で、仲介役となる エクスポート済みのGo関数(トランポリン関数) を呼び出し、引数としてハンドルを渡します。
- Go (トランポリン関数): 渡されたハンドルを
handle.Value()
で元のGoの関数に戻し、実行します。
- Go: ハンドルが不要になったら
handle.Delete()
で解放します。
この方法は少し複雑になりますが、Goのガベージコレクタに管理されているメモリ空間にある値をCの世界に安全に公開するための定石です。
このプロジェクトの 24.C_Using_cgo_Handle/
ディレクトリに、この cgo.Handle
を利用したサンプルコードがありますので、参考にしてください。