在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指针)。指针之间传递的规则不是绝对要遵守的,可以通过多种方式忽视检测,但是这往往导致无法预料的结果。
有疑问加站长微信联系(非本文作者)