0%

从编译器视角看 cgo

经过持续厚脸皮的 ping,Michael 大佬终于开始 review 那个 cgo 优化的 PR 了。只不过,大佬不喜欢 copy 几百行汇编的方案,希望用 hack 的方式。

好吧,这么大个改动也不早说,谁让咱朝中无人呢,只能这么任人摆布了。

这两天居家隔离,正好也就搞下了。虽然搞得头大,不过也对 cgo 的认识更深了一些,趁着热乎,简单记录一下的。

之前从运行时,调度策略等方面分享过 cgo,今儿就从编译器视角,来介绍下 cgo 的。

简单说

常规编译 Go 代码,就是编译 + 链接,这两步。

cgo 的主要区别是:

  1. 多了预编译这一步,用来插入一些包裹代码
  2. 链接的时候,需要链接 C 和 Go 的两种产物

三步走

预编译

将原始的 Go 预编译,生成中间的 Go + C 代码

比如,这么一个 Go 函数

1
2
3
4
//export AddFromGo
func AddFromGo(a int64, b int64) int64 {
return a + b
}

会编译为两份,其中,Go 代码:

1
2
3
4
5
6
7
8
9
10
//go:cgo_export_dynamic AddFromGo
//go:linkname _cgoexp_83bc3e2136d8_AddFromGo _cgoexp_83bc3e2136d8_AddFromGo
//go:cgo_export_static _cgoexp_83bc3e2136d8_AddFromGo
func _cgoexp_83bc3e2136d8_AddFromGo(a *struct {
p0 int64
p1 int64
r0 int64
}) {
a.r0 = AddFromGo(a.p0, a.p1)
}

这里的 Go 函数,接受的参数是一个结构体指针,结构体中存了真实的函数参数。

以及 C 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
extern void _cgoexp_83bc3e2136d8_AddFromGo(void *);

CGO_NO_SANITIZE_THREAD
GoInt64 AddFromGo(GoInt64 a, GoInt64 b)
{
size_t _cgo_ctxt = _cgo_wait_runtime_init_done();
typedef struct {
GoInt64 p0;
GoInt64 p1;
GoInt64 r0;
} __attribute__((__packed__, __gcc_struct__)) _cgo_argtype;
static _cgo_argtype _cgo_zero;
_cgo_argtype _cgo_a = _cgo_zero;
_cgo_a.p0 = a;
_cgo_a.p1 = b;
_cgo_tsan_release();
crosscall2(_cgoexp_83bc3e2136d8_AddFromGo, &_cgo_a, 24, _cgo_ctxt);
_cgo_tsan_acquire();
_cgo_release_context(_cgo_ctxt);
return _cgo_a.r0;
}

这里的 AddFromGo 就变成了一个标准的 C 函数了,可以被其他的 C 无缝调用了。
同时,也可以看到,上面 Go 函数需要的结构体,是如何封装的了。

这里的 crosscall2,咱们就不展开了,感兴趣可以翻看 以前的分享

编译

这个环节很普通,就是常规编译器的套路

C 代码用 gcc/clang 编译,Go 用 go compiler 编译。

链接

本质上,这里的链接也是常规套路,只是链接的来源,有 C 和 Go 的两种目标产物。

此时,有个小问题是,这两个目标产物,需要如何链接。

这里就需要用到第一步预编译中,产生的编译指令了。

比如,Go 中这两行

1
2
//go:linkname _cgoexp_83bc3e2136d8_AddFromGo _cgoexp_83bc3e2136d8_AddFromGo
//go:cgo_export_static _cgoexp_83bc3e2136d8_AddFromGo

其作用是,导出 _cgoexp_83bc3e2136d8_AddFromGo 这个 Go 函数

然后在 C 里面,刚好通过 extern 指定引入了这个函数。

1
extern void _cgoexp_83bc3e2136d8_AddFromGo(void *);

这样就完成了 C 和 Go 之间的配置。

这里的示例,是从 Go 函数导出给 C,如果是 Go 引用 C 函数,则会用到 cgo_import_static 这样的编译指令。

有啥用呢

整个编译过程,其实也没多少信息量,主要是知道了,Go 提供的这些 编译指令

可以通过这些编译指令,来更底层的控制 C 和 Go 之间的符号链接。 某些时候,可以更方便做 C 和 G 之间的交互了。

比如,C 里面搞个全局变量,Go 里面通过 cgo_import_import/dynamic 就可以链接到 C 变量的地址,从而直接读写 C 变量了。
(这个理论上 go 已经能做到了,go 内部自己就有这么用的,只是没有暴露对外,加了一些使用上的限制)

当然,如果是作为普通 cgo 的使用者,这些个编译指令,hack 的玩法,通常是用不着的。

以后,有需要再试试的。