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"

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))
}
\0\0C.CString(s)C.fputsC.freedefer

如果是普通的内存块, 可以直接传递给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.GoBytes()

比如获取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.malloc

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

runtime.SetFinalizer

核心代码如下:

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
}
newSliceSliceSlice.data

完整的测试代码

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