1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-27 23:12:20 +00:00

144 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 2.5. 内部机制
对于刚刚接触CGO用户来说CGO的很多特性类似魔法。CGO特性主要是通过一个叫cgo的命令行工具来辅助输出Go和C之间的桥接代码。本节我们尝试从生成的代码分析Go语言和C语言函数直接相互调用的流程。
## CGO生成的中间文件
要了解CGO技术的底层秘密首先需要了解CGO生成了哪些中间文件。我们可以在构建一个cgo包时增加一个`-work`输出中间生成文件所在的目录并且在构建完成时保留中间文件。如果是比较简单的cgo代码我们也可以直接通过手工调用`go tool cgo`命令来查看生成的中间文件。
在一个Go源文件中如果出现了`import "C"`指令则表示将调用cgo命令生成对应的中间文件。下图是cgo生成的中间文件的简单示意图
![](../images/ch2-cgo-generated-files.dot.png)
包中有4个Go文件其中nocgo开头的文件中没有`import "C"`指令其它的2个文件则包含了cgo代码。cgo命令会为每个包含了cgo代码的Go文件创建2个中间文件比如 main.go 会分别创建 main.cgo1.go 和 main.cgo2.c 两个中间文件。然后会为整个包创建一个 `_cgo_gotypes.go` Go文件其中包含Go语言部分辅助代码。此外还会创建一个 `_cgo_export.h``_cgo_export.c` 文件对应Go语言导出到C语言的类型和函数。
## 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)
其中`runtime.cgocall`函数是实现Go语言到C语言函数跨界调用的关键。更详细的细节可以参考 https://golang.org/src/cmd/cgo/doc.go 内部的代码注释和 `runtime.cgocall` 函数的实现。
## C调用Go函数
![](../images/ch2-call-c-sum-v2.uml.png)
TODO