diff --git a/ch2-cgo/ch2-05-internal.md b/ch2-cgo/ch2-05-internal.md index 5108165..42796e7 100644 --- a/ch2-cgo/ch2-05-internal.md +++ b/ch2-cgo/ch2-05-internal.md @@ -14,9 +14,127 @@ ## Go调用C函数 +Go调用C函数是CGO最常见的应用场景,我们将从最简单的例子入手分析Go调用C函数的详细流程。 + +具体代码如下(main.go): + +```go +package main + +//int sum(int a, int b) { return a+b; } +import "C" + +func main() { + println(C.sum(1, 1)) +} +``` + +首先构建并运行该例子没有错误。然后通过cgo命令行工具在_obj目录生成中间文件: + +``` +$ go tool cgo main.go +``` + +查看_obj目录生成中间文件: + +``` +$ ls _obj | awk '{print $NF}' +_cgo_.o +_cgo_export.c +_cgo_export.h +_cgo_flags +_cgo_gotypes.go +_cgo_main.c +main.cgo1.go +main.cgo2.c +``` + +其中`_cgo_.o`、`_cgo_flags`和`_cgo_main.c`文件和我们的代码没有直接的逻辑关联,可以暂时忽略。 + +我们先查看`main.cgo1.go`文件,它是main.go文件展开虚拟C包相关函数和变量后的Go代码: + +```go +package main + +//int sum(int a, int b) { return a+b; } +import _ "unsafe" + +func main() { + println((_Cfunc_sum)(1, 1)) +} +``` + +其中`C.sum(1, 1)`函数调用被替换成了`(_Cfunc_sum)(1, 1)`。每一个`C.xxx`形式的函数都会被替换为`_Cfunc_xxx`格式的纯Go函数,其中前缀`_Cfunc_`表示这是一个C函数,对应一个私有的Go桥接函数。 + +`_Cfunc_sum`函数在cgo生成的`_cgo_gotypes.go`文件中定义: + +```go +//go:cgo_unsafe_args +func _Cfunc_sum(p0 _Ctype_int, p1 _Ctype_int) (r1 _Ctype_int) { + _cgo_runtime_cgocall(_cgo_506f45f9fa85_Cfunc_sum, uintptr(unsafe.Pointer(&p0))) + if _Cgo_always_false { + _Cgo_use(p0) + _Cgo_use(p1) + } + return +} +``` + +`_Cfunc_sum`函数的参数和返回值`_Ctype_int`类型对应`C.int`类型,命名的规则和`_Cfunc_xxx`类似,不同的前缀用于区分函数和类型。 + +其中`_cgo_runtime_cgocall`对应`runtime.cgocall`函数,函数的声明如下: + +```go +func runtime.cgocall(fn, arg unsafe.Pointer) int32 +``` + +第一个参数是C语言函数的地址,第二个参数是存放C语言函数对应的参数结构体的地址。 + +在这个例子中,被传入C语言函数`_cgo_506f45f9fa85_Cfunc_sum`也是cgo生成的中间函数。函数在`main.cgo2.c`定义: + +```c +void _cgo_506f45f9fa85_Cfunc_sum(void *v) { + struct { + int p0; + int p1; + int r; + char __pad12[4]; + } __attribute__((__packed__)) *a = v; + char *stktop = _cgo_topofstack(); + __typeof__(a->r) r; + _cgo_tsan_acquire(); + r = sum(a->p0, a->p1); + _cgo_tsan_release(); + a = (void*)((char*)a + (_cgo_topofstack() - stktop)); + a->r = r; +} +``` + +这个函数参数只有一个void范型的指针,函数没有返回值。真实的sum函数的函数参数和返回值均通过唯一的参数指针类实现。 + +`_cgo_506f45f9fa85_Cfunc_sum`函数的指针指向的结构为: + +```c + struct { + int p0; + int p1; + int r; + char __pad12[4]; + } __attribute__((__packed__)) *a = v; +``` + +其中p0成员对应sum的第一个参数,p1成员对应sum的第二个参数,r成员,`__pad12`用于填充结构体保证对齐CPU机器字的整倍数。 + +然后从参数指向的结构体获取调用参数后开始调用真实的C语言版sum函数,并且将返回值保持到结构体内返回值对应的成员。 + +因为Go语言和C语言有着不同的内存模型和函数调用规范。其中`_cgo_topofstack`函数相关的代码用于C函数调用后恢复调用栈。`_cgo_tsan_acquire`和`_cgo_tsan_release`则是用于扫描CGO相关的函数则是对CGO相关函数的指针做相关检查。 + + +`C.sum`的整个调用流程图如下: + ![](../images/ch2-call-c-sum-v1.uml.png) -TODO +其中`runtime.cgocall`函数是实现Go语言到C语言函数跨界调用的关键。更详细的细节可以参考 https://golang.org/src/cmd/cgo/doc.go 内部的代码注释和 `runtime.cgocall` 函数的实现。 ## C调用Go函数