「cgo 不是银弹」,cgo 是连接 Go 与 C (乃至其他任何语言)之间的桥梁。 cgo 性能远不及原生 Go 程序的性能,执行一个 cgo 调用的代价很大。 下图展示了 cgo, go, c 之间的性能差异(网络 I/O 场景):
图1: cgo v.s. Co v.s. C,图取自 changkun/cgo-benchmarks
本文则具体研究 cgo 在运行时中的实现方式。
先来编写一个最简单的 cgo 程序:
package main
/*
#include "stdio.h"
void print() {
printf("hellow, cgo");
}
*/
import "C"
func main() {
C.print()
}
我们先观察一下汇编的结果:
TEXT main._Cfunc_print(SB) _cgo_gotypes.go
_cgo_gotypes.go:40 0x40503c0 65488b0c2530000000 MOVQ GS:0x30, CX
(...)
_cgo_gotypes.go:41 0x40503e7 488b053aca0600 MOVQ main._cgo_fd63072f180f_Cfunc_print(SB), AX
_cgo_gotypes.go:41 0x40503ee 48890424 MOVQ AX, 0(SP)
_cgo_gotypes.go:41 0x40503f2 488b442418 MOVQ 0x18(SP), AX
_cgo_gotypes.go:41 0x40503f7 4889442408 MOVQ AX, 0x8(SP)
_cgo_gotypes.go:41 0x40503fc e8ff3bfbff CALL runtime.cgocall(SB)
(...)
TEXT main.main(SB) /Users/changkun/dev/go-under-the-hood/demo/10-cgo/main.go
main.go:11 0x4050420 65488b0c2530000000 MOVQ GS:0x30, CX
(...)
main.go:12 0x405043b e880ffffff CALL main._Cfunc_print(SB)
(...)
说明 Go 代码在进入 C 代码前,最终以用编译器配合的形式,进入了运行时的 runtime.cgocall
。
再来看一下整个编译过程中的临时文件,临时文件中的入口文件为 main.cgo1.go
:
// Code generated by cmd/cgo; DO NOT EDIT.
//line main.go:1:1
package main
/*
#include "stdio.h"
void print() {
printf("hellow, cgo");
}
*/
import _ "unsafe"
func main() {
(_Cfunc_print)()
}
可以看到 Go 编译器会将我们原有的 cgo 调用替换为:_Cfunc_print
。
我们可以在 _cgo_gotypes.go
中看到这个函数的定义:
//go:cgo_unsafe_args
func _Cfunc_print() (r1 _Ctype_void) {
// 调用 _cgo_runtime_cgocall 传递 C 函数的入口地址以及相关参数
_cgo_runtime_cgocall(_cgo_222b4724d882_Cfunc_print, uintptr(unsafe.Pointer(&r1)))
if _Cgo_always_false {
}
return
}
而 _cgo_runtime_cgocall
的定义:
//go:linkname _cgo_runtime_cgocall runtime.cgocall
func _cgo_runtime_cgocall(unsafe.Pointer, uintptr) int32
可以看到编译器通过编译标志 go:linkname
将这个调用链接为了 runtime.cgocall
。
因此,从 Go 进入 C 空间的 cgo 调用,以 Go 程序为主体(运行时依然存在),通过编译器的配合,
当需要调用 C 代码时,会向运行时传递 C 函数的入口地址及所需传递的参数。
那么剩下的工作就是去分析 runtime.cgocall
这个调用如何与 Go 运行时进行交互了。
TODO:
Go under the hood | CC-BY-NC-ND 4.0 & MIT © changkun