Go语言作为一个现代化的编程语言以及支持垃圾内存的自动回收特性(GC). 我们现在关注的是C语言返回的内存资源的自动回收技术.

CGO初步

Go语言的cgo技术允许在Go代码中方便的使用C语言代码. 基本的用法如下:

package rand

/*
#include <stdlib.h>
*/
import "C"

func Random() int {
    return int(C.random())
}

func Seed(i int) {
    C.srandom(C.uint(i))
}

其中"C"是导入一个虚拟的包, 用于引用C语言的符号.

Go语言和C语言通讯交互主要是通过传递参数和返回值. 其中参数和返回值除了基本的 数据类型外, 最重要的是如何相互传递/共享二进制的内存块.

Go向C语言传递内存块

这个最简单, 有很多现成的例子:

package print

// #include <stdio.h>
// #include <stdlib.h>
import "C"
import "unsafe"

func Print(s string) {
    cs := C.CString(s)
    C.fputs(cs, (*C.FILE)(C.stdout))
    C.free(unsafe.Pointer(cs))
}

因为C语言的字符串结尾有\0, Go语言字符串没有\0, 因此需要重新构造一个C字符串. 其中 C.CString(s) 是构造一个C的字符串, 然后复制字符串并传入 C.fputs. 用完之后不要忘记调用C.free释放新创建的C字符串(可以用defer释放).

如果是普通的内存块, 可以直接传递给C函数:

package main

// #include <stdlib.h>
import "C"
import "unsafe"

func Copy(dst, src []byte, size int) {
    C.memcpy(unsafe.Pointer(&dst[0]), unsafe.Pointer(&src[0]), C.size_t(size)
}

这个代码并没有涉及内存的创建/复制/删除等额外的操作, 是比较理想的集成方式.

注意: 在C语言使用该资源期间要防止Go语言的GC提前释放被C语言使用的Go内存!

C向Go语言返回内存块

如果是C语言向Go返回内存块, 一般是先创建一个对应的Go的切片. 有现成的函数C.GoBytes()可以基于C的内存块构造切片.

比如获取C返回的内存块数据:

package main

// #include <stdlib.h>
import "C"
import "unsafe"

func GetData() []byte {
    p := C.malloc(1024)
    defer C.free(p)
    return C.GoBytes(p, 1024)
}

代码并不复杂. 但是效率并不理想: 其中需要新创建一个Go的切片, 并进行一次冗余的复制操作.

如果想去掉冗余的复制操作, 就需要基于C的内存块构造切片. 这个需要依赖Go语言的反射技术.

package main

// #include <stdlib.h>
import "C"
import "unsafe"
import "reflect"

func GetData() []byte {
    p := C.malloc(1024)
    var s []byte
    h := (*reflect.SliceHeader)((unsafe.Pointer(&s)))
    h.Cap = 1024
    h.Len = 1024
    h.Data = uintptr(p)
    return s
}

返回的s是基于C语言内存块构造的切片. 没有冗余的内存复制操作.

但是, 上面的代码却有内存泄漏的问题. Go语言的GC并不会自动释放C.malloc释放的内存.

如果需要Go语言的GC自动管理C语言返回的内存, 需要基于之前讲过的 “Go语言资源自动回收技术[OSC源创会主题补充3]” .

简而言之, 就是要将C语言的内存块绑定到一个Go语言的内存资源, 然后依靠runtime.SetFinalizer的技术管理C语言的内存块.

核心代码如下:

type Slice struct {
    Data []byte
    data *c_slice_t
}

type c_slice_t struct {
    p unsafe.Pointer
    n int
}

func newSlice(p unsafe.Pointer, n int) *Slice {
    data := &c_slice_t{p, n}
    runtime.SetFinalizer(data, func(data *c_slice_t) {
        C.free(data.p)
    })
    s := &Slice{data: data}
    h := (*reflect.SliceHeader)((unsafe.Pointer(&s.Data)))
    h.Cap = n
    h.Len = n
    h.Data = uintptr(p)
    return s
}

其中 newSlice 基于C语言的内存块构造 Slice 结构体. 如果 Slice.data 资源没有被引用, 则会自动触发C语言的内存释放函数.

完整的测试代码

package main

/*
#include <stdio.h>
#include <stdlib.h>

void print(char* s) {
    printf("print: %s\n", s);
}
*/
import "C"
import (
    "fmt"
    "reflect"
    "runtime"
    "time"
    "unsafe"
)

type Slice struct {
    Data []byte
    data *c_slice_t
}

type c_slice_t struct {
    p unsafe.Pointer
    n int
}

func newSlice(p unsafe.Pointer, n int) *Slice {
    data := &c_slice_t{p, n}
    runtime.SetFinalizer(data, func(data *c_slice_t) {
        println("gc:", data.p)
        C.free(data.p)
    })
    s := &Slice{data: data}
    h := (*reflect.SliceHeader)((unsafe.Pointer(&s.Data)))
    h.Cap = n
    h.Len = n
    h.Data = uintptr(p)
    return s
}

func testSlice() {
    msg := "hello world!"
    p := C.calloc((C.size_t)(len(msg) + 1), 1)
    println("malloc:", p)

    s := newSlice(p, len(msg)+1)
    copy(s.Data, []byte(msg))

    fmt.Printf("fmt.Printf: %s\n", string(s.Data))
    C.print((*C.char)(p))
}

func main() {
    testSlice()

    runtime.GC()
    runtime.Gosched()
    time.Sleep(1e9)
}

测试程序的输出:

D:>go run hello.go
malloc: 0x6f7f50
fmt.Printf: hello world!
print: hello world!
gc: 0x6f7f50

注: Go1.3之前有效, Go1.4之后改了移动栈.