Go切片与C数组转换

Posted by Chen Quan on August 21, 2019

cgo 从 go传递 slice 到 C

假设有一个Go [] int,并想要将它传递给C * int,你可以这样做:

package main

/*
#include<stdio.h>

void slice(int *a){
	for(int i=0;i<4;i++){
		printf("%d\n",a[i]);
	}
}

*/
import "C"
import (
	"fmt"
)

func main() {

	intSlice := []C.int{108880, 18, 28, 83, 488} //使用cgo类型C.int
	fmt.Println(&intSlice[0])
	C.slice(&intSlice[0])

}

我们可以看到当我们把Go切片中第一个元素的地址传给C中的slice(int *a)函数,我们就可以打印出原来Go切片中的所有元素,因此我们可以知道其实Go切片中第一个元素也就是C中Array的第一个元素,内存没有重新分配。下面我们通过一个案例来证实一下:

package main

import "C"
import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {
	var c int = 1

	intSlice := []int{100, 1, 2, 3, 4}
	newSlice := intSlice[c:]

	fmt.Printf("切片的指针 %p\n", &intSlice)            //0xc000004460
	fmt.Printf("指向底层数组的第一个元素: %d\n", &intSlice[0]) //824633770800

	fmt.Printf("指向newSlice第一个元素而不是数组: %d\n", &newSlice[0]) //824633770808
	fmt.Printf("%v \n", newSlice[0])                       //1

	ref := reflect.ValueOf(newSlice)
	t := reflect.TypeOf(newSlice)

	//基础数组的起始地址
	addr := int(ref.Pointer()) - (t.Align() * c) //824633770800

	fmt.Printf("基础数据的地址: %d\n", addr) //824633770800

	underArray := (*[5]int)(unsafe.Pointer(uintptr(addr)))
	fmt.Println(*underArray) //[100 1 2 3 4]

}

cgo 从 C 传递 slice 到 go

这里一般会用在c的callback中。

需要加一个wrapper,比直接调用go函数中间多了一个转换步骤,但方便了许多。

执行流程为,c调用发起 -> c wrapper -> go export

.go:

//export a_function_callback
 func a_function_callback(args []C.astruct) {}

.c:

extern void a_function_callback(GoSlice args);

void a_function_callback_c(args *C.astruct) {
    GoSlice args_go;
    args_go.data = args;
    args_go.len = nargs;
    args_go.cap = nargs;
    return a_function_callback(args_go);
}

这里的GoSlice需要从 export 出来的 .h 中获取,或者 include 该 .h文件。 也可以直接拷贝过来,注意不要产生类型冲突。

#ifndef GO_CGO_PROLOGUE_H
#define GO_CGO_PROLOGUE_H

 typedef long long GoInt64;
 typedef GoInt64 GoInt;
 typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

 #endif

C.CString 的自动化回收方法

一般使用 C.CString 时,定义一个临时变量存储结果,使用 defer C.free 回收这个临时的变量。

在 go 的 runtime 包中,有一个SetFinalizer方法,可以在回收对象的时候调用一个处理函数。

那么我们可以考虑使用它来自动化回收 C.CString产生的临时变量。

       type CString struct {
             Ptr *C.char
       }

       func CCString(s string) *CString {
            p := C.CString(s)
             this := &CString{Ptr: p}

            runtime.SetFinalizer(this, freeCString)
             return this
       }

       func freeCString(cs *CString) {
              C.free(unsafe.Pointer(cs.Ptr))
       }

在使用的时候,基本能嵌入到调用的位置,不需要显式定义一个临时变量并执行 defer C.free。

       C.somefunc(CCString(s).Ptr)

注意,只在somefunc不接管传递的参数的所有权时调用这种方式,如果somefunc接管了所有权, 而在未来某时间内存被回收了,则会出现程序SEGFAULT崩溃。

这种可能会产生一点runtime效率损失,另外还有额外的内存分配,看情况使用。

bool 类型的转换处理

在 C 语言中,c99之前 bool 都是定义为 int的,要想转换为go的bool类型,比较曲折。

     var bval_c C.GBoolean
     var bval_go bool
     if int(bval) == 1  { 
           bval_go = true
     } else {
           bval_go = false
     }

如果go有三元运算符的话,也可一行代码实现该转换,问题是go没有三元运算符。

一般来说,对其包装一个简单的转换函数比较好,比如 c2gobool(C.GBoolean).

有时候不同的 C 项目中 bool 定义不同,所以一般需要为每个项目定义这么一个函数,而不能放在一个库中共用。

在 c99 之后,则没有这个问题,cgo能够直接判断是内置的bool类型,能够直接转换为go的bool。

     var bval_c C._Bool
     var bval_go = bool(bval_c)