這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
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.gomalloc: 0x6f7f50fmt.Printf: hello world!print: hello world!gc: 0x6f7f50