cgo的指针传递
p了个c · · 3357 次点击 · · 开始浏览在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指针)。指针之间传递的规则不是绝对要遵守的,可以通过多种方式忽视检测,但是这往往导致无法预料的结果。
有疑问加站长微信联系(非本文作者)
入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889
关注微信- 请尽量让自己的回复能够对别人有帮助
- 支持 Markdown 格式, **粗体**、~~删除线~~、
`单行代码` - 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
- 图片支持拖拽、截图粘贴等方式上传
收入到我管理的专栏 新建专栏
在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指针)。指针之间传递的规则不是绝对要遵守的,可以通过多种方式忽视检测,但是这往往导致无法预料的结果。