cgo的指针传递

0

在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指针)。指针之间传递的规则不是绝对要遵守的,可以通过多种方式忽视检测,但是这往往导致无法预料的结果。

你可能感兴趣的

载入中...