GO调C基本原理CGO是实现Go与C互操作的方式,包括Go调C和C调Go两个过程。其中Go调C的过程比较简单。对于一个在C中定义的函数add3,在Go中调用时需要显式的使用C.add3调用。其中C是在程序中引入的一个伪包
代码中的import “C”即为在Go中使用的伪包。这个包并不真实存在,也不会被Go的compile组件见到,它会在编译前被CGO工具捕捉到,并做一些代码的改写和桩文件的生成。
CGO 提供了 golang 和 C 语言相互调用的机制。某些第三方库可能只有 C/C++ 的实现,完全用纯 golang 的实现可能工程浩大,这时候 CGO 就派上用场了。可以通 CGO 在 golang 在调用 C 的接口,C++ 的接口可以用 C 包装一下提供给 golang 调用。被调用的 C 代码可以直接以源代码形式提供或者打包静态库或动态库在编译时链接。推荐使用静态库的方式,这样方便代码隔离,编译的二进制也没有动态库依赖方便发布也符合 golang 的哲学。
基本数值类型
golang 的基本数值类型内存模型和 C 语言一样,就是连续的几个字节(1 / 2 / 4 / 8 字节)。因此传递数值类型时可以直接将 golang 的基本数值类型转换成对应的 CGO 类型然后传递给 C 函数调用,反之亦然:
package main
/*
#include
static int32_t add(int32_t a, int32_t b) {
return a + b;
}
*/
import “C”
import “fmt”
func main() {
var a, b int32 = 1, 2
var c int32 = int32(C.add(C.int32_t(a), C.int32_t(b)))
fmt.Println(c) // 3
}
golang 和 C 的基本数值类型转换对照表如下:
C语言类型 CGO类型 Go语言类型
char C.char byte
singed char C.schar int8
unsigned char C.uchar uint8
short C.short int16
unsigned short C.ushort uint16
int C.int int32
unsigned int C.uint uint32
long C.long int32
unsigned long C.ulong uint32
long long int C.longlong int64
unsigned long long int C.ulonglong uint64
float C.float float32
double C.double float64
size_t C.size_t uint
注意 C 中的整形比如 int 在标准中是没有定义具体字长的,但一般默认认为是 4 字节,对应 CGO 类型中 C.int 则明确定义了字长是 4 ,但 golang 中的 int 字长则是 8 ,因此对应的 golang 类型不是 int 而是 int32 。为了避免误用,C 代码最好使用 C99 标准的数值类型
golang 中切片用起来有点像 C 中的数组,但实际的内存模型还是有点区别的。C 中的数组就是一段连续的内存,数组的值实际上就是这段内存的首地址。golang 切片的内存模型如下所示(参考源码 $GOROOT/src/runtime/chan.go
array len cap
|
v
data
由于底层内存模型的差异,不能直接将 golang 切片的指针传给 C 函数调用,而是需要将存储切片数据的内部缓冲区的首地址及切片长度取出传传递:
package main
/*
#include
static void fill_255(char* buf, int32_t len) {
int32_t i;
for (i = 0; i < len; i++) {
buf[i] = 255;
}
}
*/
import “C”
import (
“fmt”
“unsafe”
)
func main() {
b := make([]byte, 5)
fmt.Println(b) // [0 0 0 0 0]
C.fill_255((*C.char)(unsafe.Pointer(&b[0])), C.int32_t(len(b)))
fmt.Println(b) // [255 255 255 255 255]
}
字符串
golang 的字符串和 C 中的字符串在底层的内存模型也是不一样的:
golang 字串符串并没有用 ‘\0’ 终止符标识字符串的结束,因此直接将 golang 字符串底层数据指针传递给 C 函数是不行的。一种方案类似切片的传递一样将字符串数据指针和长度传递给 C 函数后,C 函数实现中自行申请一段内存拷贝字符串数据然后加上未层终止符后再使用。更好的方案是使用标准库提供的 C.CString() 将 golang 的字符串转换成 C 字符串然后传递给 C 函数调用:
package main
/*
#include
#include
#include
static char* cat(char* str1, char* str2) {
static char buf[256];
strcpy(buf, str1);
strcat(buf, str2);
return buf; } */ import "C" import (
"fmt"
"unsafe" )
func main() {
str1, str2 := “hello”, “ world”
// golang string -> c string
cstr1, cstr2 := C.CString(str1), C.CString(str2)
defer C.free(unsafe.Pointer(cstr1)) // must call
defer C.free(unsafe.Pointer(cstr2))
cstr3 := C.cat(cstr1, cstr2)
// c string -> golang string
str3 := C.GoString(cstr3)
fmt.Println(str3) // “hello world”
}
需要注意的是 C.CString() 返回的 C 字符串是在堆上新创建的并且不受 GC 的管理,使用完后需要自行调用 C.free() 释放,否则会造成内存泄露,而且这种内存泄露用前文中介绍的 pprof 也定位不出来。
其他类型
golang 中其他类型(比如 map) 在 C/C++ 中并没有对等的类型或者内存模型也不一样。传递的时候需要了解 golang 类型的底层内存模型,然后进行比较精细的内存拷贝操作。传递 map 的一种方案是可以把 map 的所有键值对放到切片里,然后把切片传递给 C++ 函数,C++ 函数再还原成 C++ 标准库的 map 。由于使用场景比较少,这里就不赘述了。
总结
本文主要介绍了在 golang 中使用 CGO 调用 C/C++ 接口涉及的一些细节问题。C/C++ 比较底层的语言,需要自己管理内存。使用 CGO 时需要对 golang 底层的内存模型有所了解。另外 goroutine 通过 CGO 进入到 C 接口的执行阶段后,已经脱离了 golang 运行时的调度并且会独占线程,此时实际上变成了多线程同步的编程模型。如果 C 接口里有阻塞操作,这时候可能会导致所有线程都处于阻塞状态,其他 goroutine 没有机会得到调度,最终导致整个系统的性能大大较低。总的来说,只有在第三方库没有 golang 的实现并且实现起来成本比较高的情况下才需要考虑使用 CGO ,否则慎用。
可以使用go tool cgo在本地目录生成这些桩文件
$go tool cgo main.go
.
|__obj
| |cgo_.o
| |cgo_export.c
| |cgo_export.h
| |cgo_flags
| |cgo_gotypes.go
| |cgo_main.c
| |main.cgo1.go
| |main.cgo2.c
|_main.go
其中main.cgo1.go为主要文件,是用户代码main.go被cgo改写之后的文件:
$cat _obj/main.cgo1.go
// Created by cgo - DO NOT EDIT
//line /Users/didi/goLang/src/github.com/xiazemin/cgo/exp1/main.go:1
package main
//line /Users/didi/goLang/src/github.com/xiazemin/cgo/exp1/main.go:11
import “fmt”
func main() {
var a, b int32 = 1, 2
var c int32 = int32(_Cfunc_add(_Ctype_int32_t(a), _Ctype_int32_t(b)))
fmt.Println(c)
}
这个文件才是Go的compile组件真正看到的用户代码。可以看到原来文件中的import “C”被去掉,而用户写的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中
$cat _obj/_cgo_gotypes.go
// Created by cgo - DO NOT EDIT
package main
import “unsafe”
import _ “runtime/cgo”
import “syscall”
var _ syscall.Errno
func _Cgo_ptr(ptr unsafe.Pointer) unsafe.Pointer { return ptr }
//go:linkname _Cgo_always_false runtime.cgoAlwaysFalse
var _Cgo_always_false bool
//go:linkname _Cgo_use runtime.cgoUse
func _Cgo_use(interface{})
type _Ctype_int int32
type _Ctype_int32_t _Ctype_int
type _Ctype_void [0]byte
//go:linkname _cgo_runtime_cgocall runtime.cgocall
func _cgo_runtime_cgocall(unsafe.Pointer, uintptr) int32
//go:linkname _cgo_runtime_cgocallback runtime.cgocallback
func _cgo_runtime_cgocallback(unsafe.Pointer, unsafe.Pointer, uintptr, uintptr)
//go:linkname _cgoCheckPointer runtime.cgoCheckPointer
func _cgoCheckPointer(interface{}, …interface{})
//go:linkname _cgoCheckResult runtime.cgoCheckResult
func _cgoCheckResult(interface{})
//go:cgo_import_static _cgo_3a42ad434848_Cfunc_add
//go:linkname __cgofn__cgo_3a42ad434848_Cfunc_add _cgo_3a42ad434848_Cfunc_add
var __cgofn__cgo_3a42ad434848_Cfunc_add byte
var _cgo_3a42ad434848_Cfunc_add = unsafe.Pointer(&__cgofn__cgo_3a42ad434848_Cfunc_add)
//go:cgo_unsafe_args
func _Cfunc_add(p0 _Ctype_int32_t, p1 _Ctype_int32_t) (r1 _Ctype_int32_t) {
_cgo_runtime_cgocall(_cgo_3a42ad434848_Cfunc_add, uintptr(unsafe.Pointer(&p0)))
if _Cgo_always_false {
_Cgo_use(p0)
_Cgo_use(p1)
}
return
}
_Cfunc_add3的参数传递与正常的函数有些不同,其参数并不在栈上,而是在堆上。函数中的_Cgo_use,其实是runtime.cgoUse,用来告诉编译器要把p0, p1, p2逃逸到堆上去,这样才能较为安全的把参数传递到C的程序中去。(因为go是动态栈不安全)
$ go build -gcflags “-m” main.go
/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:14:6: can inline _Cgo_ptr
/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:14:35: leaking param: ptr to result ~r1 level=0
/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:27:6: _cgo_runtime_cgocall assuming arg#2 is unsafe uintptr
/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:30:6: _cgo_runtime_cgocallback assuming arg#3 is unsafe uintptr
/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:30:6: _cgo_runtime_cgocallback assuming arg#4 is unsafe uintptr
/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:47:11: p0 escapes to heap
/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:48:11: p1 escapes to heap
/var/folders/r9/35q9g3d56_d9g0v59w9x2l9w0000gn/T/go-build789689150/command-line-arguments/_obj/_cgo_gotypes.go:45:75: _Cfunc_add &p0 does not escape
./main.go:16: c escapes to heap
./main.go:16: main … argument does not escape
函数中的__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_NO_SANITIZE_THREAD
void
_cgo_3a42ad434848_Cfunc_add(void v)
{
struct {
int32_t p0;
int32_t p1;
int32_t r;
char pad12[4];
} __attribute((packed)) *a = v;
char *stktop = _cgo_topofstack();
typeof(a->r) r;
_cgo_tsan_acquire();
r = add(a->p0, a->p1);
_cgo_tsan_release();
a = (void)((char*)a + (_cgo_topofstack() - stktop));
a->r = r;
}
在这个函数的定义中,并没有显式的参数拷贝;而是利用类型强转,在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中。
<img src="https://xiazemin.github.io/MyBlog/img/cgo.png"/>
<img src="https://xiazemin.github.io/MyBlog/img/cgo_add.png"/>
<img src="https://xiazemin.github.io/MyBlog/img/cgo_add_addr.png"/> 也可以#include头文件的方式 package main
// #include
// #include
/*
void print(char *str) {
printf("%s\n", str);
}
*/
import "C"
import “unsafe”
func main() {
s := “Hello Cgo”
cs := C.CString(s)
C.print(cs)
C.free(unsafe.Pointer(cs))
}
与“正常”的go代码相比,上述代码有几处“特殊”的地方:
在开头的注释中出现了c语言头文件的include字样
在注释中定义了c语言函数print
import了一个名为C的“包”
在main函数中调用了上述定义的c语言函数print
首先,go源码文件中的c语言代码是需要用注释包裹的,就像上面的include头文件以及print函数定义;其次,import “C”这个语句是必须的,而且其与上面的c代码之间不能用空行分隔,必须紧密相连。这里的”C“不是包名,而是一种类似名字空间的概念,或可以理解为伪包,c语言所有语法元素均在该伪包下面;最后,访问c语法元素时都要在其前面加上伪包前缀,比如C.uint和上面代码中的C.print、C.free等。
在上面的例子中,c语言是内嵌在go代码中的,如果代码量更大更复杂的话,这显然是很不”专业“的。那么,是否可以将c语言代码从go代码中分离出去,单独定义呢?答案是肯定的,可以通过共享库的方式实现。
cgo提供了#cgo指示符可以指定go源码在编译后与哪些共享库进行链接。例子如下:
// hello.go
package main
// #cgo LDFLAGS: -L ./ -lhello
// #include
// #include
// #include "hello.h"
import "C"
func main() {
C.hello()
}
// hello.c
#include “hello.h”
void hello()
{
printf(“hello, go\n”);
}
// hello.h
extern void hello();
其中在hello.go中,#cgo指示符后面添加LDFLAGS: -L ./ -lhello,作用是在go代码编译时,指定在当前目录查找so库并进行链接。
因此,只需要把hello.c编译成动态库,再编译go代码,即可在运行go代码的时候调用共享库中的c语言函数。指令如下:
gcc -fPIC -o libhello.so hello.c
go build -o hello
./hello
示例代码的逻辑为:在Go中定义一个add3函数,然后把这个函数export给C使用;在C中定义一个add3_c函数,并调用这个Go中的add3;在Go的主程序中再调用C中的add3_c函数。
c语言调用go语言
与在go中调用c源码相比,在c中使用go函数的场合较少。因为一般来说,采用高级语言作为粘合剂调用低级语言能充分发挥各自的特点,而用低级语言调用高级语言反而有可能降低低级语言的性能优势,在go中,可以使用”export + 函数名“来导出go函数为c代码所用,看一个简单的例子:
// hello.go
package main
import “C”
import “fmt”
// export Go2C
func Go2C() {
fmt.Println(“hello, C”)
}
func main(){
}
可通过go build的编译选项,将go代码编译成共享库以供c代码调用。注意,编译so库时必须存在main及main函数(即使main函数为空,否则报错,# command-line-arguments
runtime.main_main·f: relocation target main.main not defined
runtime.main_main·f: undefined: “main.main”
)。编译指令如下:go build -v -x -buildmode=c-shared -o libhello.so hello.go。
编译成功后,只需在c代码中引入新生成的头文件及编译时链接动态库即可实现go函数的调用。代码如下:
// hello.c
#include
#include "libhello.h"
int main()
{
Go2C();
return 0;
}
通过gcc -o hello -L. -lhello hello.c,即可编译成可执行程序。
$ ./hello
hello, C
注意,运行前必须确定共享库运行时查找路径中存在需要链接的共享库,可通过将so库路径放到/usr/lib或者修改环境变量LD_LIBRARY_PATH。
小结
go语言可以通过内嵌c代码的形式调用c语言,也可以通过调用共享库函数的方式实现;至于c语言调用go函数,则可以通过go build将go代码编译成共享库提供给c代码使用。注意,本文中的共享库均为动态共享库,至于静态共享库则未曾实验
$ go help buildmode
The ‘go build’ and ‘go install’ commands take a -buildmode argument which
indicates which kind of object file is to be built. Currently supported values
are:
-buildmode=archive
Build the listed non-main packages into .a files. Packages named
main are ignored.
-buildmode=c-archive
Build the listed main package, plus all packages it imports,
into a C archive file. The only callable symbols will be those
functions exported using a cgo //export comment. Requires
exactly one main package to be listed.
-buildmode=c-shared
Build the listed main package, plus all packages it imports,
into a C shared library. The only callable symbols will
be those functions exported using a cgo //export comment.
Requires exactly one main package to be listed.
-buildmode=default
Listed main packages are built into executables and listed
non-main packages are built into .a files (the default
behavior).
-buildmode=shared
Combine all the listed non-main packages into a single shared
library that will be used when building with the -linkshared
option. Packages named main are ignored.
-buildmode=exe
Build the listed main packages and everything they import into
executables. Packages not named main are ignored.
-buildmode=pie
Build the listed main packages and everything they import into
position independent executables (PIE). Packages not named
main are ignored.
-buildmode=plugin
Build the listed main packages, plus all packages that they
import, into a Go plugin. Packages not named main are ignored.
go build -x -v -ldflags “-s -w” -buildmode=c-shared -o libhello.so main.go
mv $WORK/command-line-arguments/_obj/_cgo_install.h libhello.h
mv $WORK/command-line-arguments/_obj/exe/libhello.so libhello.so
$ tree ./
./
├── build.sh
├── libhello.h
├── libhello.so
├── main.c
└── main.go
执行完毕的目录结构到多了libhello.h 和 libhello.so
使用下面的命令编译C文件.(-L指定动态链接库目录 -l指定动态链接库文件 -I指定头文件目录)
gcc -I./ -L./ -lhello -o main main.c
$ tree ./
./
├── build.sh
├── libhello.h
├── libhello.so
├── main
├── main.c
└── main.go
$ ./main
export Test
Helloargp=casp1casp2casp3falsefaultgcingpanicstart (MB)
addr= code= ctxt: curg= list= m->p= p->m= prev= span= varp=(…)
, not SCHED typesefenceerrno etypesobjectsignalstatussweep (scan (scan) MB in dying= locks= m->g0= s=nil
, goid=, sys: GODEBUGSignal defer value=cs fs gctracego1.9.2gs panic: r10 r11 r12 r13 r14 r15 r8 r9 rax rbp rbx rcx rdi rdx rflags rip rsi rsp runningsyscallunknownwaiting goalΔ= helpgc= is not mcount= minutes nalloc= newval= nfreed= packed= pointer stack=[, idle: [signal
—–
这里有个注意的地方就是main.go文件中的//export Hello 和 //export Test这个是有作用的,没有这个就不会生成对应的头文件方法.也就是此方法就不会被导出.
只会生成.so 文件;不会生成.h文件,导出不成功
cgo_export.c
关注上述main.go中的代码,add3即为用于被C调用的函数。在函数上方的//export add3即为告诉cgo在编译时生成一个用于C调用的add3函数。这个又cgo生成add3函数,才是C程序正真调用的函数。
前面的博客介绍了Go调C的原理,所以在本文中默认读者已经清楚了Go是如何调用进入C的。这作为本文的基本出发点,即从add3_c函数开始,如何通过调用上面定义的add3函数,进而调入到Go中定义的add3函数。上面cgo产生的这个add3函数,只是一个桩函数。这个函数的执行,需要等待runtime初始化之后。然后在这里面定义了一个按照Go函数的入参规则定义的一个结构体。与Go调C不同,这里面发生一次显式的参数拷贝。_cgoexp_3f63814d8a5f_add3为Go中定义的桩函数,与Go调C类似,只是这个是一个实实在在的Go函数,在链接的时候暴露到C中。这个函数的细节在后面继续深入。在知道了这个代表的意义之后,就可以理解crosscall2的作用。
crosscall2类似于asmcgocall,它是从C直接通过ABI call进入Go的函数。接收Go中的函数地址,以及参数地址和参数的大小。经过这个函数,已经开始进入Go程序之中,在执行Go函数,但并不是用户代码。在进入用户代码之前,还有很长的路要走。它的函数声明为:
通过这些编译制导语句,在链接时把这个函数的地址暴露给C程序。以此为入口,由C进入Go之中。这个函数中的_cgoexpwrap_3f63814d8a5f_add3又是一个接口函数。这个函数已经是很接近用户定义的函数了。
C调Go的主要故事发生在_cgo_runtime_cgocallback函数之后,即runtime.cgocallback函数。
cgocallback
在介绍cgocallback之前,我们再次考虑Go函数与C函数的不同。在Go中运行的用户代码,必须受runtime的管控,这是基本出发点。当程序运行从C进入Go之中,同样要遵守这样的规则。即需要给Go函数准备必须的G,M,P。如果此时的C程序,是从Go中进入的,那此时C调Go即可直接使用原来的G即可。如果,此时的C程序运行在一个原生的C线程上。那这个纯粹的pthread是没有M的概念的。为让Go程序有一个M的环境执行,runtime需要给这个原生的C线程安装一些东西,把它伪装成一个M。此外,此时的运行仍然处于系统调用状态,以及线程栈上。还需要退出系统调用状态和切换到普通的G栈上。
这里cgocallback函数只是一个跳板函数。之后进入cgocallback_gofunc。
cgocallback_gofunc
这个函数有三个任务:
判断当前线程是否为Go的线程,如果不是则把它伪装一下
把栈从线程栈切换到G栈
把函数地地址、参数地址等信息入栈,并记录栈顶地址
这个函数中需要判断出当前的线程是从Go调C的状态,还是一个原生的C线程状态。如果是原生的C线程,在这个函数中会做一些操作把当前线程伪装成一个Go的线程。这个情况在后面再进一步讨论。下面讨论是Go的线程的状态。cgocallback_gofunc在确认当前的线程是Go的线程之后,把栈从线程栈切换到G栈,然后把函数地址和参数地址入栈,栈顶信息记录到G中,后进入到cgocallbackg中。
cgocallbackg
这个函数实现线程系统调用状态的退出,此时程序运行在G栈上。进入cgocallbackg1函数。
这个函数,首先会判断当前是否需要补充extrem用于补给原生的C线程伪装成Go线程所用的组件。这个在后面介绍原生的C线程调用Go时,会再次提到。之后,这个函数会重新拿到传入函数的地址和参数地址,并经过reflectcall函数给所执行函数选择合适frame的执行函数。
CALLFN
这是一个plan 9的宏定义,实现了callXXXXX等一系列函数。在这个函数根据传递进来的参数地址,把参数值从C的内存中传递到Go的内存,并负责把在Go中计算的结果传会C的内存。
至此终于进入了_cgoexpwrap_3f63814d8a5f_add3函数。
纯C的线程的情况
对于一个纯C的线程,需要一个extram的结构负责把该线程伪装成一个Go的线程。当然,这又要分主程序是C和Go两种情况。先考虑主程序是Go的情况。在一个Go程序初始化时,在使用cgo的情况下,就会为这样的情况准备了一个extram,在mstart1中
extram是一个全局链表,记录着所有的extram。extram就是一个M,只是这个M没有一个绑定的线程而已。除此之外,runtime中的M全是和线程绑定在一起的。另外,这个extram,在创建时就处于系统调用状态,并且不仅有一个g0还有一个g,这都是为C调Go准备的。
在一个纯C的线程拿走一个extram之后,此时系统并没有为这个链表补充新的。等到拿走extram的线程执行到cgocallbackg1时,才会为这个链表补充一个新的extram,留待其他的线程使用。
对于主程序是C的情况
这种情况下,Go一般是便以为c-shared/c-archive的库给C调用。C函数调入进Go,必须按照Go的规则执行。这个规则是不会变的,所以当主程序是C调用Go时,也同样有一个Go的runtime与C程序并行执行。这个runtime的初始化在对应的c-shared的库加载时就会执行。因此,在一开始就有两个线程执行,一个C的,一个是Go的。此时,Go的初始化入口为_rt0_amd64_linux_lib,这是在链接时写入的。