這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
在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 goAddfunc 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 goFoofunc 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指標)。指標之間傳遞的規則不是絕對要遵守的,可以通過多種方式忽視檢測,但是這往往導致無法預料的結果。