【Free Style】CGO: Go与C互操作技术(一):Go调C基本原理

举报
赵志强 发表于 2017/11/13 17:51:07 2017/11/13
【摘要】 GO调C基本原理CGO是实现Go与C互操作的方式,包括Go调C和C调Go两个过程。其中Go调C的过程比较简单。对于一个在C中定义的函数add3,在Go中调用时需要显式的使用C.add3调用。其中C是在程序中引入的一个伪包。示例代码如下所示:图一:CGO使用示例代码 代码中的import “C”即为在Go中使用的伪包。这个包并不真实存在,也不会被Go的compile组件见到,它

GO调C基本原理

CGO是实现GoC互操作的方式,包括GoCCGo两个过程。其中GoC的过程比较简单。对于一个在C中定义的函数add3,在Go中调用时需要显式的使用C.add3调用。其中C是在程序中引入的一个伪包。示例代码如下所示:

image.png

图一:CGO使用示例代码

            代码中的import “C”即为在Go中使用的伪包。这个包并不真实存在,也不会被Gocompile组件见到,它会在编译前被CGO工具捕捉到,并做一些代码的改写和桩文件的生成。

下面是CGO工具产生的目录树(可以使用go tool cgo在本地目录生成这些桩文件):

image.png

图二:CGO产生的桩文件目录树

            其中main.cgo1.go为主要文件,是用户代码main.gocgo改写之后的文件:

image.png

图三:CGO改写之后的用户代码

这个文件才是Gocompile组件真正看到的用户代码。可以看到原来文件中的import “C”被去掉,而用户写的C.int被改写为_Ctype_intC.add3被改写为_Cfunc_add3。关于这个特性有两个点需要注意。一是在有import “C”的文件中,用户的注释信息全部丢失,使用的一些progma也不例外。二是在testing套件中import “C”不允许使用,表现为testing不支持CGO。但并不是没有办法在testing中使用CGO,可以利用上面的特性,在另外一个独立的Go文件中定义C函数,并使用import “C”;但是在使用testingGo文件中直接使用_Cfunc_add3函数即可。_Cfunc_add3用户虽然没有显示定义,但是CGO自动产生了这一函数的定义。上面一系列的//line编译制导语句用做关联生成的Go与原来的用户代码的行号信息。

再次回到_Cfunc_add3函数,并不是C中的add3函数,是CGO产生的一个Go函数。它的定义在CGO产生的桩文件_cgo_gotypes.go中:

image.png

图四:_Cfunc_add3的定义

            _Cfunc_add3的参数传递与正常的函数有些不同,其参数并不在栈上,而是在堆上。函数中的_Cgo_use,其实是runtime.cgoUse,用来告诉编译器要把p0, p1, p2逃逸到堆上去,这样才能较为安全的把参数传递到C的程序中去。

image.png

图五:参数逃逸到堆中

函数中的__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函数,进行GoC之间call ABI操作。当然,cgocall里面会做一些调度相关的准备动作,后面有详细说明。

__cgo_79f22807c129_Cfunc_add3如上文所述,是定义在main.cgo2.c中的一个函数,其定义如下:

image.png

图六:__cgo_79f22807c129_Cfunc_add3的定义

         在这个函数的定义中,并没有显式的参数拷贝;而是利用类型强转,在C中直接操作Go传递过来的参数地址。在这个函数中真正调用了用户定义的add3函数。

            cgocall_Cfunc_add3中的_cgo_runtime_cgocall函数,是runtime中的一个从GoC的关键函数。这个函数里面做了一些调度相关的安排。之所以有这样的设计,是因为Go调入C之后,程序的运行不受Goruntime的管控。一个正常的Go函数是需要runtime的管控的,即函数的运行时间过长会导致goroutine的抢占,以及GC的执行会导致所有的goroutine被拉齐。C程序的执行,限制了Goruntime的调度行为。为此,Goruntime会在进入到C程序之后,会标记这个运行C的线程排除在runtime的调度之后,以减少这个线程对Go的调度的影响。此外,由于正常的Go程序运行在一个2K的栈上,而C程序需要一个无穷大的栈。这样的设计会导致在Go的栈上执行C函数会导致栈的溢出,因此在进去C函数之前需要把当前线程的栈从2K的栈切换到线程本身的系统栈上。栈切换发生在asmcgocall中,而线程的状态标记发生在cgocall中。其具体原理示意图如下所示:

image.png 

image.png

image.png

图八:GoC原理图


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。