在cgo的官方文档中有一小节特地介绍了cgo中传递c语言和go语言指针之间的传递,由于里面讲得比较抽象并且缺少例子,因此通过这篇文章总结cgo指针传递的注意事项。
基本概念
在官方文档和本篇总结中,Go指针指的是指向Go分配的内存的指针(例如使用&
运算符或者调用new
函数获取的指针)。而C指针指的是C分配的内存的指针(例如调用malloc
函数获取的指针)。一个指针是Go指针还是C指针,是根据内存如何分配判断的,与指针的类型无关。
Go调用C
传递指向Go Memory的指针
Go调用C Code时,Go传递给C Code的Go指针所指的Go Memory中不能包含任何指向Go Memory的Pointer。
值得注意的是,Go是可以传递给C Code的Go指针的,但是这个指针里面不能包含任何指向Go Memory的Pointer。
package main /* #include <stdio.h> struct Foo { int a; int *p; }; void plusOne(struct Foo *f) { (f->a)++; *(f->p)++; } */ import "C" import "unsafe" import "fmt" func main() { f := &C.struct_Foo{} f.a = 5 f.p = (*C.int)((unsafe.Pointer)(new(int))) // f.p = &f.a C.plusOne(f) fmt.Println(int(f.a)) }
在以上代码可以看出,Go Code向C Code传递了一个指向Go Memory(Go分配的)指针f,但f指向的Go Memory中有一个指针p指向了另一处Go Memory:new(int)
。当使用go build
编译这个文件时,是可以通过编译的,然后在运行时会发生如下报错:panic runtime error: cgo argument has Go pointer to Go pointer
。
传递指向struc field的指针
Go调用C Code时,如果传递的是一个指向struct field的指针,那么“Go Memory”专指这个field所占用的内存,即便struct中有其他field指向其他Go Memory也没关系。
将上面例子改为只传入指向struct field的指针。如下:
package main /* #include <stdio.h> struct Foo { int a; int *p; }; void plusOne(int *i) { (*i)++; } */ import "C" import ( "fmt" "unsafe" ) func main() { f := &C.struct_Foo{} f.a = 5 f.p = (*C.int)((unsafe.Pointer)(new(int)) C.plusOne(&f.a) fmt.Println(int(f.a)) }
直接指向go run
,打印结果为6
。可以看出,因为这次调用只传递单个field指针,指向这个field所占用的内存,而这个field也没有嵌套其他指向Go Memory的指针,因此这是符合规范的调用,不会触发panic。
传递指向slice或array中的element指针
和传递struct field不同,传递一个指向slice或者array中的element指针时,需要考虑的Go Memory的范围不仅仅是这个element,而是整个array或这个slice背后的underlying array所占用的内存区域,要保证整个区域内不包含任何指向Go Memory的指针。
package main /* #include <stdio.h> void plusOne(int **i) { (**i)++; } */ import "C" import ( "fmt" "unsafe" ) func main() { s1 := make([]*int,5) var a int = 5 s1[1] = &a C.plusOne((**C.int)((unsafe.Pointer)(&s1[0]))) fmt.Println(s1[0]) }
从以上代码可以看出,传递给C的是slice第一个element的地址,并不包括指向Go Memory的指针,但由于第二个element保存了另外一块Go Memory的地址(&a),当运行go run
时,获得报错:panic runtime error: cgo argument has Go pointer to Go pointer
。
C调用Go
返回指向Go分配的内存的指针
C调用的Go函数不能返回指向Go分配的内存的指针。
package main // extern int* goAdd(int,int); // // int cAdd(int a,int b) { // int *i = goAdd(a,b); // return *i; // } import "C" import "fmt" // export goAdd func goAdd(a,b C.int) { c := a + b return &c } func main() { var a,b int = 5,6 i := C.cAdd(C.int(a),C.int(b)) fmt.Println(int(i)) }
上面代码中,goAdd这个Go函数返回了一个指向Go分配的内存(&c)的指针。运行上述代码,结果如下:panic runtime error: cgo result has Go pointer
。
在C分配的内存中存储指向Go分配的内存的指针
Go Code不能在C分配的内存中存储指向Go分配的内存的指针。
package main // #include <stdlib.h> // extern void goFoo(int**); // // void cFoo() { // int **p = malloc(sizeof(int*)); // goFoo(p); // } import "C" //export goFoo func goFoo(p **C.int) { *p = new(C.int) } func main() { C.cFoo() }
针对此例,默认的GODEBUG=cgocheck=1是正常运行的,将GODEBUG=cgocheck=2则会发生报错:fatal error: Go pointer stored into non-Go memory
。
检测控制
以上规则会在运行时动态检测,可以通过设置GODEBUG环境变量修改检测程度,默认值是GODEBUG=cgocheck=1,可以通过设置为0取消这些检测,也可以通过设置为2来提高检测标准,但这会牺牲运行的效率。
此外,也可以通过使用unsafe
包来逃脱这些限制,而且C语言方面也没法使用什么特殊的机制来限制调用Go。尽管如此,如果程序打破了上面的限制,很可能会以一种无法预料的方式调用失败。
小结
cgo中,Go与C的内存应该保持着相对独立,指针之间的传递应该尽量避免嵌套不同内存的指针(如C中保存Go指针)。指针之间传递的规则不是绝对要遵守的,可以通过多种方式忽视检测,但是这往往导致无法预料的结果。
原文链接:https://www.f2er.com/go/187339.html