经过持续厚脸皮的 ping,Michael 大佬终于开始 review 那个 cgo 优化的 PR 了。只不过,大佬不喜欢 copy 几百行汇编的方案,希望用 hack 的方式。
好吧,这么大个改动也不早说,谁让咱朝中无人呢,只能这么任人摆布了。
这两天居家隔离,正好也就搞下了。虽然搞得头大,不过也对 cgo 的认识更深了一些,趁着热乎,简单记录一下的。
之前从运行时,调度策略等方面分享过 cgo,今儿就从编译器视角,来介绍下 cgo 的。
简单说
常规编译 Go 代码,就是编译 + 链接,这两步。
cgo 的主要区别是:
- 多了预编译这一步,用来插入一些包裹代码
- 链接的时候,需要链接 C 和 Go 的两种产物
三步走
预编译
将原始的 Go 预编译,生成中间的 Go + C 代码
比如,这么一个 Go 函数
1 | //export AddFromGo |
会编译为两份,其中,Go 代码:
1 | //go:cgo_export_dynamic AddFromGo |
这里的 Go 函数,接受的参数是一个结构体指针,结构体中存了真实的函数参数。
以及 C 代码:
1 | extern void _cgoexp_83bc3e2136d8_AddFromGo(void *); |
这里的 AddFromGo
就变成了一个标准的 C 函数了,可以被其他的 C 无缝调用了。
同时,也可以看到,上面 Go 函数需要的结构体,是如何封装的了。
这里的 crosscall2
,咱们就不展开了,感兴趣可以翻看 以前的分享
编译
这个环节很普通,就是常规编译器的套路
C 代码用 gcc/clang 编译,Go 用 go compiler 编译。
链接
本质上,这里的链接也是常规套路,只是链接的来源,有 C 和 Go 的两种目标产物。
此时,有个小问题是,这两个目标产物,需要如何链接。
这里就需要用到第一步预编译中,产生的编译指令了。
比如,Go 中这两行
1 | //go:linkname _cgoexp_83bc3e2136d8_AddFromGo _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 的玩法,通常是用不着的。
以后,有需要再试试的。