cgo 系列文章之内部原理 (三)

内部机制

CGO 生成的中间文件

在构建一个 cgo 包时增加一个 -work 输出中间生成所在目录并且在构建完成时保留中间文件.

对于比较简单的 cgo 代码可以直接手工调用 go tool cgo 命令来查看生成的中间文件.

在一个 Go 源文件当中, 如果出现 import "C" 指令则表示将调用 cgo 命令生成对应的中间文件. 下面是生成的中间文件的简单
示意图:

在这里插入图片描述

包含有 4 个 Go 文件, 其中 nocgo 开头的文件中没有 import "C" 指令, 其他的 2 个文件则包含了 cgo 代码. cgo 命令会为每个包含 cgo 代码的 Go 文件创建 2 个中间文件, 比如 main.go 会分别创建 main.cgo1.gomain.cgo2.c 两个中间文件, cgo当中是 Go 代码部分和 C 代码部分.

然后会为整个包创建一个 _cgo_gotypes.go Go 文件, 其中包含 Go 语言部分辅助代码. 此外还创建一个 _cgo_export.h_cgo_export.c 文件, 对应 Go 语言导出到 C 语言的类型和函数.

package main

/*
int sum(int a, int b) { 
  return a+b; 
}
*/
import "C"

func main() {
    println(C.sum(1, 1))
}

使用 go tool cgo main.go 生成的中间文件如下:

_cgo_export.c
_cgo_export.h
_cgo_flags
_cgo_gotypes.go
_cgo_main.c
_cgo_.o
main.cgo1.go
main.cgo2.c

其中 main.cgo1.go [1] 的代码如下:

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 [2] 文件当中定义:

//go:cgo_import_static _cgo_7b5139e7c7da_Cfunc_sum
//go:linkname __cgofn__cgo_7b5139e7c7da_Cfunc_sum _cgo_7b5139e7c7da_Cfunc_sum
var __cgofn__cgo_7b5139e7c7da_Cfunc_sum byte
var _cgo_7b5139e7c7da_Cfunc_sum = unsafe.Pointer(&__cgofn__cgo_7b5139e7c7da_Cfunc_sum)


//go:cgo_unsafe_args
func _Cfunc_sum(p0 _Ctype_int, p1 _Ctype_int) (r1 _Ctype_int) {
	_cgo_runtime_cgocall(_cgo_7b5139e7c7da_Cfunc_sum, uintptr(unsafe.Pointer(&p0)))
	if _Cgo_always_false {
		_Cgo_use(p0)
		_Cgo_use(p1)
	}
	return
}

_Cfunc_sum 函数的参数和返回值 _Ctype_int 类型对应 C.int 类型, 命名的规则和 _C_func_xxx 类似, 不同的前缀用于区分函数和类型.

_cgo_runtime_cgocall 对应 runtime.cgocall 函数, 声明如下:

func runtime.cgocall(fn, arg unsafe.Pointer) int32

第一个参数是 C 语言函数的地址
第二个参数是存储 C 语言函数对应的参数结构体(参数和返回值)的地址

在此例当中, 被传入C语言函数 _cgo_7b5139e7c7da_Cfunc_sum 也是 cgo 生成的中间函数. 函数定义在main.cgo2.c [3] 当中.

void _cgo_7b5139e7c7da_Cfunc_sum(void *v)
{
	struct {
		int p0;
		int p1;
		int r;
		char __pad12[4];
	} __attribute__((__packed__, __gcc_struct__)) *_cgo_a = v;
	char *_cgo_stktop = _cgo_topofstack();
	__typeof__(_cgo_a->r) _cgo_r;
	_cgo_tsan_acquire();
	_cgo_r = sum(_cgo_a->p0, _cgo_a->p1);
	_cgo_tsan_release();
	_cgo_a = (void*)((char*)_cgo_a + (_cgo_topofstack() - _cgo_stktop));
	_cgo_a->r = _cgo_r;
	_cgo_msan_write(&_cgo_a->r, sizeof(_cgo_a->r));
}

此函数参数只有一个 void* 的指针, 函数没有返回值. 真实的 sum 函数的函数参数和返回值通过唯一的参数指针类实现.

struct {
		int p0;
		int p1;
		int r;
		char __pad12[4];
} __attribute__((__packed__, __gcc_struct__)) *_cgo_a = v;

其中, p0, p1分别对应 sum 的第一个和第二个参数, r 对应 sum 的返回值. _pad12 用于填充结构体保证对齐CPU机器字的整数倍.

然后从参数执行的结构体获取参数后开始调用真实的C语言版sum函数, 并且将返回值保存到结构体的返回值对应的成员.

因为 Go 语言和 C 语言有着不同的内存模型和函数调用规范, 其中 _cgo_topofstack 函数相关的代码用于 C 函数调用后恢复调用栈. _cgo_tsan_acquire_cgo_tsan_release 则是用于扫描 CGO 相关函数的指针总相关检查.

调用链:

内部机制

CGO 生成的中间文件

在构建一个 cgo 包时增加一个 -work 输出中间生成所在目录并且在构建完成时保留中间文件.

对于比较简单的 cgo 代码可以直接手工调用 go tool cgo 命令来查看生成的中间文件.

在一个 Go 源文件当中, 如果出现 import "C" 指令则表示将调用 cgo 命令生成对应的中间文件. 下面是生成的中间文件的简单示意图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4l8PmTju-1594449275938)(/images/cgo_mid.png)]

包含有 4 个 Go 文件, 其中 nocgo 开头的文件中没有 import "C" 指令, 其他的 2 个文件则包含了 cgo 代码. cgo 命令会为每个包含 cgo 代码的 Go 文件创建 2 个中间文件, 比如 main.go 会分别创建 main.cgo1.gomain.cgo2.c 两个中间文件, cgo当中是 Go 代码部分和 C 代码部分.

然后会为整个包创建一个 _cgo_gotypes.go Go 文件, 其中包含 Go 语言部分辅助代码. 此外还创建一个 _cgo_export.h_cgo_export.c 文件, 对应 Go 语言导出到 C 语言的类型和函数.

package main

/*
int sum(int a, int b) { 
  return a+b; 
}
*/
import "C"

func main() {
    println(C.sum(1, 1))
}

使用 go tool cgo main.go 生成的中间文件如下:

_cgo_export.c
_cgo_export.h
_cgo_flags
_cgo_gotypes.go
_cgo_main.c
_cgo_.o
main.cgo1.go
main.cgo2.c

其中 main.cgo1.go [1] 的代码如下:

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 [2] 文件当中定义:

//go:cgo_import_static _cgo_7b5139e7c7da_Cfunc_sum
//go:linkname __cgofn__cgo_7b5139e7c7da_Cfunc_sum _cgo_7b5139e7c7da_Cfunc_sum
var __cgofn__cgo_7b5139e7c7da_Cfunc_sum byte
var _cgo_7b5139e7c7da_Cfunc_sum = unsafe.Pointer(&__cgofn__cgo_7b5139e7c7da_Cfunc_sum)


//go:cgo_unsafe_args
func _Cfunc_sum(p0 _Ctype_int, p1 _Ctype_int) (r1 _Ctype_int) {
	_cgo_runtime_cgocall(_cgo_7b5139e7c7da_Cfunc_sum, uintptr(unsafe.Pointer(&p0)))
	if _Cgo_always_false {
		_Cgo_use(p0)
		_Cgo_use(p1)
	}
	return
}

_Cfunc_sum 函数的参数和返回值 _Ctype_int 类型对应 C.int 类型, 命名的规则和 _C_func_xxx 类似, 不同的前缀用于区分函数和类型.

_cgo_runtime_cgocall 对应 runtime.cgocall 函数, 声明如下:

func runtime.cgocall(fn, arg unsafe.Pointer) int32

第一个参数是 C 语言函数的地址
第二个参数是存储 C 语言函数对应的参数结构体(参数和返回值)的地址

在此例当中, 被传入C语言函数 _cgo_7b5139e7c7da_Cfunc_sum 也是 cgo 生成的中间函数. 函数定义在main.cgo2.c [3] 当中.

void _cgo_7b5139e7c7da_Cfunc_sum(void *v)
{
	struct {
		int p0;
		int p1;
		int r;
		char __pad12[4];
	} __attribute__((__packed__, __gcc_struct__)) *_cgo_a = v;
	char *_cgo_stktop = _cgo_topofstack();
	__typeof__(_cgo_a->r) _cgo_r;
	_cgo_tsan_acquire();
	_cgo_r = sum(_cgo_a->p0, _cgo_a->p1);
	_cgo_tsan_release();
	_cgo_a = (void*)((char*)_cgo_a + (_cgo_topofstack() - _cgo_stktop));
	_cgo_a->r = _cgo_r;
	_cgo_msan_write(&_cgo_a->r, sizeof(_cgo_a->r));
}

此函数参数只有一个 void* 的指针, 函数没有返回值. 真实的 sum 函数的函数参数和返回值通过唯一的参数指针类实现.

struct {
		int p0;
		int p1;
		int r;
		char __pad12[4];
} __attribute__((__packed__, __gcc_struct__)) *_cgo_a = v;

其中, p0, p1分别对应 sum 的第一个和第二个参数, r 对应 sum 的返回值. _pad12 用于填充结构体保证对齐CPU机器字的整数倍.

然后从参数执行的结构体获取参数后开始调用真实的C语言版sum函数, 并且将返回值保存到结构体的返回值对应的成员.

因为 Go 语言和 C 语言有着不同的内存模型和函数调用规范, 其中 _cgo_topofstack 函数相关的代码用于 C 函数调用后恢复调用栈. _cgo_tsan_acquire_cgo_tsan_release 则是用于扫描 CGO 相关函数的指针总相关检查.

调用链:

文件链:

main.go -> main.cgo1.go -> _cgo_gotypes.go -> main.cgo2.c
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值