Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cgo编译的中间环节:Go是如何找到C方法 #2

Open
xpzouying opened this issue Aug 17, 2020 · 0 comments
Open

cgo编译的中间环节:Go是如何找到C方法 #2

xpzouying opened this issue Aug 17, 2020 · 0 comments
Labels

Comments

@xpzouying
Copy link
Owner

cgo编译的中间环节:Go是如何找到C方法

相关源码:源码

环境介绍

编译环境:

➜  learning_golang ✗ go version       
go version go1.14.3 darwin/amd64

基本概念

对于操作系统来说,无论是C、Go编写的函数,无非是一堆指令,最终对于操作系统来说都是一堆指令。CPU根据指令顺序执行。对于函数的调用,简单的理解为跳到某个指令段,然后开始顺序执行对应的指令。

图片参考:How CPU WORKS

在Go中,调用函数就只需要告诉Go:

  1. 哪一个函数:函数的地址
  2. 参数的地址:由于多个参数也是连续存放的,所以取参数的首地址。

Go通过cgo也是跳不出这套规则。那么,cgo对C的函数是如何处理的,可以让Go调用起来C函数呢?

代码分析

CGO DEMO

从cgo demo开始,具体代码如下:

package main

/*
typedef struct person {
	char* name;
	int score1;
	int score2;
} person;

person get_person() {
	person zy;
	zy.name = "zouying";
	zy.score1 = 100;
	zy.score2 = 100;

	return zy;
}

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

func SayHello() { println("hello ZOUYING") }

func main() {

	SayHello()

	p := C.get_person()
	log.Printf("%#v, size of person: %d", p, C.sizeof_struct_person)

	value := C.sum(p.score1, p.score2)
	println("score=", value)
}

在代码中,

  • C语言部分:

    • 定义了1个结构体person
    • 定义了2个函数:其中一个有输入参数,一个没有输入参数
    • C相关的代码必须定义在注释中,可以包含:函数、变量的声明和定义。
    • 函数、变量名可以想象成被定义在一个叫"C"的package中。
  • Go语言部分:

    • import "C"跟上面C的代码不能有空行。否则会报错:./main.go:18:2: could not determine kind of name for C.get_person
    • 使用import "C"。"C"并不是Go真实的一个package,是让Go能调用C的符号,比如C.intC.sum()等。
    • 定义了1个Go函数。

更多的cgo规范细节可以参考官方文档:

运行

hello ZOUYING
2020/07/26 19:05:06 main._Ctype_struct_person{name:(*main._Ctype_char)(0x416cebc), score1:100, score2:100}, size of person: 16
score= 200                                                                                                                    

从结果中,通过对C.get_person()返回值打印的日志,可以看到C.person类型变成了main._Ctype_struct_person,那么在此过程中Go编译器又做了哪些工作呢?

生成中间文件

运行命令go tool cgo main.go生成cgo中间文件。在当前文件夹中会生成_obj的文件夹,保存cgo的中间文件。具体的生成过程可以参考$GOROOT/src/cmd/cgo/doc.go,在此先不赘述。

(base) ➜  demo (how-go-call-c-func-by-cgo) ✗ ls -lh _obj 
total 64
-rw-r--r--  1 zouying  staff   3.3K Jul 26 17:44 _cgo_.o
-rw-r--r--  1 zouying  staff   605B Jul 26 17:44 _cgo_export.c
-rw-r--r--  1 zouying  staff   1.5K Jul 26 17:44 _cgo_export.h
-rw-r--r--  1 zouying  staff    13B Jul 26 17:44 _cgo_flags
-rw-r--r--  1 zouying  staff   1.8K Jul 26 17:44 _cgo_gotypes.go
-rw-r--r--  1 zouying  staff   416B Jul 26 17:44 _cgo_main.c
-rw-r--r--  1 zouying  staff   421B Jul 26 17:44 main.cgo1.go
-rw-r--r--  1 zouying  staff   2.7K Jul 26 17:44 main.cgo2.c

生成中间文件的流程

Go在产生这些中间文件时,会经过一系列工作。主要流程包括,

  1. 分析C的代码:借助gcc分析当前cgo的所有标识符、判断是否存在错误等等。
  2. 将C翻译成Go的代码:根据main.go的内容转换成中间的go文件,即产生了上面列表中的.go、.c、.h文件。
  3. 其他:在此过程中还会生成一些链接库,在此就先跳过,详细的细节可以参考cgo/doc.go

分析中间文件

分析一下_obj这个临时文件夹中的文件。

首先,main.go会被拷贝成main.cgo1.go文件,并且在此过程中会把对应的C函数进行翻译。

该文件删除大部分注释后,源码如下,

package main

import _ "unsafe"

import "log"

func SayHello() { println("hello ZOUYING") }

func main() {

	SayHello()

	p, err := ( /*line :31:12*/_C2func_get_person /*line :31:23*/)()
	log.Printf("%#v, size of person: %d, err=%v", p, ( /*line :32:51*/_Ciconst_sizeof_struct_person /*line :32:72*/), err)

	value := ( /*line :34:11*/_Cfunc_sum /*line :34:15*/)(p.score1, p.score2)
	println("score=", value)
}

main.cgo1.go中可以看到部分的翻译迹象,类似于:C.get_person变成了_C2func_get_personC.sum变成了_Cfunc_sum等。这些翻译后的定义在_cgo_gotypes.go文件中。

C.person的定义:

C.get_person()。该方法没有输入,只有返回值。该函数有2个返回值,一个是C.person,一个是error,其中第二个返回值是可选的,编译器会根据是否需要第二个返回值然后定义翻译后方法的返回值个数,可以对比C.get_personC.sum翻译后的区别。

//go:cgo_unsafe_args
func _C2func_get_person() (r1 _Ctype_struct_person, r2 error) {
	errno := _cgo_runtime_cgocall(_cgo_299c25848d85_C2func_get_person, uintptr(unsafe.Pointer(&r1)))
	if errno != 0 { r2 = syscall.Errno(errno) }
	if _Cgo_always_false {
	}
	return
}

该函数其实就是通过_cgo_runtime_cgocall对函数进行调用。该函数的定义为:

//go:linkname _cgo_runtime_cgocall runtime.cgocall
func _cgo_runtime_cgocall(unsafe.Pointer, uintptr) int32

//go:xxx表示编译器指令,编译器接收注释形式的指令。注意,为了区分与正常注释,//go之间不能有空格。

go:linkname编译标志会将_cgo_runtime_cgocall链接到runtime.cgocall,输入为1、函数地址,2、参数首地址,返回值为调用编号,返回0表示调用成功。runtime.cgocall后续文章再详细分析,再此先不展开。

传入参数包括:函数名_cgo_299c25848d85_C2func_get_person和返回值的地址。对于通用的函数调用时,输入应该是函数和args列表的首地址,在此直接取返回值的地址是由于没有输入参数。

其中函数的定义为:

//go:cgo_import_static _cgo_299c25848d85_C2func_get_person
//go:linkname __cgofn__cgo_299c25848d85_C2func_get_person _cgo_299c25848d85_C2func_get_person
var __cgofn__cgo_299c25848d85_C2func_get_person byte
var _cgo_299c25848d85_C2func_get_person = unsafe.Pointer(&__cgofn__cgo_299c25848d85_C2func_get_person)

_cgo_299c25848d85_C2func_get_person被定义为取__cgofn__cgo_299c25848d85_C2func_get_person的地址,而该方法又被编译标志链接到_cgo_299c25848d85_C2func_get_person方法,该方法的定义在main.cgo2.c中,定义如下,

CGO_NO_SANITIZE_THREAD
int
_cgo_299c25848d85_C2func_get_person(void *v)
{
	int _cgo_errno;
	struct {
		person r;
	} __attribute__((__packed__)) *_cgo_a = v;
	char *_cgo_stktop = _cgo_topofstack();
	__typeof__(_cgo_a->r) _cgo_r;
	_cgo_tsan_acquire();
	errno = 0;
	_cgo_r = get_person();
	_cgo_errno = errno;
	_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));
	return _cgo_errno;
}

该方法中会最终实际调用到我们定义的C方法:get_person(),而get_person函数的定义同样被复制在main.cgo2.c中。

C.sum的定义:

//go:cgo_import_static _cgo_299c25848d85_Cfunc_sum
//go:linkname __cgofn__cgo_299c25848d85_Cfunc_sum _cgo_299c25848d85_Cfunc_sum
var __cgofn__cgo_299c25848d85_Cfunc_sum byte
var _cgo_299c25848d85_Cfunc_sum = unsafe.Pointer(&__cgofn__cgo_299c25848d85_Cfunc_sum)

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

该函数大致流程都与C.person方法一致,不同的在于有对应的输入、输出参数,所以在对其调用的时候,是把第一个入参的地址作为_cgo_runtime_cgocall的第2输入参数传入。

总结

总结一下cgo的调用关系:

@xpzouying xpzouying added the cgo label Aug 17, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant