LoginSignup
45
30

More than 5 years have passed since last update.

cgoを使ったCとGoのリンクの裏側 (2)

Last updated at Posted at 2016-05-22

先の記事では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するように指示している。

export_example.go
package main

import (
    "C"
    "runtime"
)

//export goVersion
func goVersion() string {
    return runtime.Version()
}
use_exported.h
void print_go_version(void);
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);
}
main.go
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はこのようになっている。

ソースコードの依存関係は下図の通りである。

deps.png

ビルドプロセス

ビルドプロセス自体はimportの場合と大差ない。

  1. コード生成
    • go tool cgo export_example.go
    • go tool cgo main.go
  2. Cコードのコンパイル
    • gcc -c SOURCES
  3. Cコードのリンク
    • gcc -o _cgo_.o OBJS
  4. インポート宣言の生成
    • go tool cgo -dynimport ....
  5. Goコードのコンパイル
    • go tool compile -o example2.a -pack GO_FILES
  6. Cコードの再リンク
    • gcc -o _all.o OBJS
  7. Cオブジェクトをアーカイブに追加
    • go tool pack r example2.a _all.o
  8. アーカイブ内のオブジェクトをリンク
    • go tool link -o example2 example2.a

データフローは下図の通りである。

flow.png

これもimportの場合と概ね同じ構造であるが、下記2点が異なる。

  1. go tool cgoに掛けるソースファイルが2個に増えたので、その分生成されるファイルが増えて複雑になった
  2. use_exported.c由来のオブジェクトファイルuse_exported.oが加わった。これをコンパイルする際にgo tool cgoで生成された_cgo_export.h#includeされている。

特に後者のほうを詳しく見てみよう。

use_exported.c

まず、exportされたGoの機能を利用するCソースuse_exported.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.hgo tool cgoexport_example.gomain.goから生成された物だ。

_cgo_export.h
/* 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つの部分から構成される。

  1. import "C"の直前のコメントで書いたpreamble部のコピー

    preambleに//#include <stdio.h>とか書いた場合だと、#include <stdio.h>がこの部分にコピーされる訳だ。

  2. Goの基本型をCで表現したもの (boilerplate cgo prologue)

  3. //exportを宣言したGoの関数に対応する関数プロトタイプ

これらを利用してuse_exported.cではexport_example.go内のfunc goVersion() stringを呼び出せるわけだ。ただし、正確に言うとfunc goVersion() stringと関数プロトタイプ内のgoVersion()は同一ではない。それは次節で見てみよう。

グルーコード

まず、Cから見えるgoVersion()は生成された_cgo_export.cの中にある。これは引数を適切にpackしてGoの世界に引き渡すためのグルーコードである。

_cgo_export.c
/* 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にある。

_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の世界に制御が移る。

45
30
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
30