GO调C基本原理
CGO是实现Go与C互操作的方式,包括Go调C和C调Go两个过程。其中Go调C的过程比较简单。对于一个在C中定义的函数add3,在Go中调用时需要显式的使用C.add3调用。其中C是在程序中引入的一个伪包。示例代码如下所示:
图一:CGO使用示例代码
代码中的import “C”即为在Go中使用的伪包。这个包并不真实存在,也不会被Go的compile组件见到,它会在编译前被CGO工具捕捉到,并做一些代码的改写和桩文件的生成。
下面是CGO工具产生的目录树(可以使用go tool cgo在本地目录生成这些桩文件):
图二:CGO产生的桩文件目录树
其中main.cgo1.go为主要文件,是用户代码main.go被cgo改写之后的文件:
图三:CGO改写之后的用户代码
这个文件才是Go的compile组件真正看到的用户代码。可以看到原来文件中的import “C”被去掉,而用户写的http://C.int被改写为_Ctype_int,C.add3被改写为_Cfunc_add3。关于这个特性有两个点需要注意。一是在有import “C”的文件中,用户的注释信息全部丢失,使用的一些progma也不例外。二是在testing套件中import “C”不允许使用,表现为testing不支持CGO。但并不是没有办法在testing中使用CGO,可以利用上面的特性,在另外一个独立的Go文件中定义C函数,并使用import “C”;但是在使用testing的Go文件中直接使用_Cfunc_add3函数即可。_Cfunc_add3用户虽然没有显示定义,但是CGO自动产生了这一函数的定义。上面一系列的//line编译制导语句用做关联生成的Go与原来的用户代码的行号信息。
再次回到_Cfunc_add3函数,并不是C中的add3函数,是CGO产生的一个Go函数。它的定义在CGO产生的桩文件_cgo_gotypes.go中:
图四:_Cfunc_add3的定义
_Cfunc_add3的参数传递与正常的函数有些不同,其参数并不在栈上,而是在堆上。函数中的_Cgo_use,其实是runtime.cgoUse,用来告诉编译器要把p0, p1, p2逃逸到堆上去,这样才能较为安全的把参数传递到C的程序中去。
图五:参数逃逸到堆中
函数中的__cgo_79f22807c129_Cfunc_add3是一个变量,记录了一个C函数的地址(注意,这并不是实际要调用add3函数),是一个真正定义在C程序中的函数。在Go中,通过编译制导语句//go:cgo_import_static在链接时拿到C中函数__cgo_79f22807c129_Cfunc_add3的地址,然后通过编译制导语句//go:linkname把这个函数地址与Go中的byte型变量__cgofn_cgo_79f22807c129_Cfunc_add3的地址对齐在一起。之后再利用一个新的变量__cgo_79f22807c129_Cfunc_add3记录这个byte型变量的地址。从而可以实现在Go中拿到C中函数的地址。做完,这些之后把C的函数地址和参数地址传给cgocall函数,进行Go与C之间call ABI操作。当然,cgocall里面会做一些调度相关的准备动作,后面有详细说明。
__cgo_79f22807c129_Cfunc_add3如上文所述,是定义在main.cgo2.c中的一个函数,其定义如下:
图六:__cgo_79f22807c129_Cfunc_add3的定义
在这个函数的定义中,并没有显式的参数拷贝;而是利用类型强转,在C中直接操作Go传递过来的参数地址。在这个函数中真正调用了用户定义的add3函数。
cgocall即_Cfunc_add3中的_cgo_runtime_cgocall函数,是runtime中的一个从Go调C的关键函数。这个函数里面做了一些调度相关的安排。之所以有这样的设计,是因为Go调入C之后,程序的运行不受Go的runtime的管控。一个正常的Go函数是需要runtime的管控的,即函数的运行时间过长会导致goroutine的抢占,以及GC的执行会导致所有的goroutine被拉齐。C程序的执行,限制了Go的runtime的调度行为。为此,Go的runtime会在进入到C程序之后,会标记这个运行C的线程排除在runtime的调度之后,以减少这个线程对Go的调度的影响。此外,由于正常的Go程序运行在一个2K的栈上,而C程序需要一个无穷大的栈。这样的设计会导致在Go的栈上执行C函数会导致栈的溢出,因此在进去C函数之前需要把当前线程的栈从2K的栈切换到线程本身的系统栈上。栈切换发生在asmcgocall中,而线程的状态标记发生在cgocall中。其具体原理示意图如下所示:
图八:Go调C原理图