指针和内存互访

指针是c语言的灵魂,同样指针在go语言中也有很重要的位置。在cgo中go的指针和c的指针可以互相传递,但大多时候这种传递是危险的。go语言是一种有GC的语言,你很难知道指针指向的内容是否已经被回收。而c语言的指针通常情况下是稳定的(除非手动释放,或者返回局部变量指针)。本小节我们来探讨下cgo中指针的运用,以及cgo中go和c内存互相访问。

指针

在本节中,术语Go指针表示指向Go分配的内存的指针(例如通过使用&运算符或调用go内置函数new), 术语C指针表示指向C分配的内存的指针(例如通过调用C.malloc)。 指针是指向内存的起始地址,以指针类型是无关的。在c语言中我们使用void*类型无关的指针,而在go是unsafe.Pointer。这两者指针在cgo中是可以互相传递的。

cgo
void*unsafe.Pointer

go访问c的内存

go的由于内存策略的原因,很多时候向系统申请的内存在使用完之后,不会立即返回给系统,而是留着给下次使用。如果有场景想立即返回给出给系统,可以用cgo实现。

package main

/*
#include <stdlib.h>

void* m_malloc(int typeSize ,size_t length) {
    return malloc(typeSize * length);
}
 */
import "C"
import (
	"fmt"
	"unsafe"
)

func main()  {
	c := (*[3]int32)(C.m_malloc(C.int(4),3))
	defer C.free(unsafe.Pointer(c))
	c[0],c[1],c[2] = 0,1,2
	fmt.Println(c,len(c))
}
$ go run main.go
&[0 1 2] 3

c访问go内存

我们cgo入门介绍了cgo字符串和字节数组的转换,用了C.CString的方法将 go string转成 * C.char。我们同时强调了在使用完* C.char后记得调用C.free 方法。我们来看下C.CString的定义

const cStringDef = `
func _Cfunc_CString(s string) *_Ctype_char {
	p := _cgo_cmalloc(uint64(len(s)+1))
	pp := (*[1<<30]byte)(p)
	copy(pp[:], s)
	pp[len(s)] = 0
	return (*_Ctype_char)(p)
}
`

可以看出C.CString是使用cgo函数_cgo_cmalloc申请了内存,然后把go string数据拷贝到这块内存上。所以这块内存在c函数使用是安全的。但是很明显,耗性能,浪费内存空间。

cgo在调用c函数时,能保证作为参数传入c函数的go内存不会发生移动。所以我们可以使用如下方法让c函数临时访问go内存。

package main

/*
#include <stdio.h>
#include <stdlib.h>
int mconcat(const char * a,const char *b,char *c){
	return sprintf(c,"%s%s",a,b);
}
 */
import "C"
import (
	"fmt"
	"reflect"
	"unsafe"
)

func main()  {
	a := C.CString("hello")
	b := C.CString("world")

	defer func() {
		C.free(unsafe.Pointer(a)) //需要手动释放
		C.free(unsafe.Pointer(b)) //需要手动释放
	}()

	ret := make([]byte,20)
	p := (*reflect.SliceHeader)(unsafe.Pointer(&ret)) 

	len := C.mconcat(a,b,(*C.char)(unsafe.Pointer(p.Data)))
	fmt.Println(len,string(ret[:len]))
}
$ go run main.go
10 helloworld

上面的例子中我们使用了go语言的[]byte去接收c函数字符串拼接,这样子省去很多额外的内存分配。

参考资料