cgo的指針傳遞

在cgo的官方文檔中有一小節特意介紹了cgo中傳遞c語言和go語言指針之間的傳遞,因爲裏面講得比較抽象而且缺乏例子,所以經過這篇文章總結cgo指針傳遞的注意事項。函數

基本概念

在官方文檔和本篇總結中,Go指針指的是指向Go分配的內存的指針(例如使用&運算符或者調用new函數獲取的指針)。而C指針指的是C分配的內存的指針(例如調用malloc函數獲取的指針)。一個指針是Go指針仍是C指針,是根據內存如何分配判斷的,與指針的類型無關。ui

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 pointercode

傳遞指向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的指針,所以這是符合規範的調用,不會觸發panicelement

傳遞指向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 pointerio

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指針)。指針之間傳遞的規則不是絕對要遵照的,能夠經過多種方式忽視檢測,可是這每每致使沒法預料的結果。

相關文章
相關標籤/搜索