1. 程式人生 > >golang cgo 使用總結

golang cgo 使用總結

原文地址

CGO 提供了 golang 和 C 語言相互呼叫的機制。某些第三方庫可能只有 C/C++ 的實現,完全用純 golang 的實現可能工程浩大,這時候 CGO 就派上用場了。可以通 CGO 在 golang 在呼叫 C 的介面,C++ 的介面可以用 C 包裝一下提供給 golang 呼叫。被呼叫的 C 程式碼可以直接以原始碼形式提供或者打包靜態庫或動態庫在編譯時連結。推薦使用靜態庫的方式,這樣方便程式碼隔離,編譯的二進位制也沒有動態庫依賴方便釋出也符合 golang 的哲學。
CGO 的具體使用教程本文就不涉及了,這裡主要介紹下一些細節避免使用 CGO 的時候踩坑。

引數傳遞

基本數值型別

golang 的基本數值型別記憶體模型和 C 語言一樣,就是連續的幾個位元組(1 / 2 / 4 / 8 位元組)。因此傳遞數值型別時可以直接將 golang 的基本數值型別轉換成對應的 CGO 型別然後傳遞給 C 函式呼叫,反之亦然:

package main

/*
#include <stdint.h>

static int32_t add(int32_t a, int32_t b) {
	return a + b;
}
*/
import "C"
import "fmt"

func main() {
	var a, b int32 = 1, 2
	var c int32 = int32
(C.add(C.int32_t(a), C.int32_t(b))) fmt.Println(c) // 3 }

golang 和 C 的基本數值型別轉換對照表如下:

C語言型別 CGO型別 Go語言型別
char C.char byte
singed char C.schar int8
unsigned char C.uchar uint8
short C.short int16
unsigned short C.ushort uint16
int C.int int32
unsigned int C.uint uint32
long C.long int32
unsigned long C.ulong uint32
long long int C.longlong int64
unsigned long long int C.ulonglong uint64
float C.float float32
double C.double float64
size_t C.size_t uint

注意 C 中的整形比如 int 在標準中是沒有定義具體字長的,但一般預設認為是 4 位元組,對應 CGO 型別中 C.int 則明確定義了字長是 4 ,但 golang 中的 int 字長則是 8 ,因此對應的 golang 型別不是 int 而是 int32 。為了避免誤用,C 程式碼最好使用 C99 標準的數值型別,對應的轉換關係如下:

C語言型別 CGO型別 Go語言型別
int8_t C.int8_t int8
uint8_t C.uint8_t uint8
int16_t C.int16_t int16
uint16_t C.uint16_t uint16
int32_t C.int32_t int32
uint32_t C.uint32_t uint32
int64_t C.int64_t int64
uint64_t C.uint64_t uint64

切片

golang 中切片用起來有點像 C 中的陣列,但實際的記憶體模型還是有點區別的。C 中的陣列就是一段連續的記憶體,陣列的值實際上就是這段記憶體的首地址。golang 切片的記憶體模型如下所示(參考原始碼 $GOROOT/src/runtime/chan.go):

由於底層記憶體模型的差異,不能直接將 golang 切片的指標傳給 C 函式呼叫,而是需要將儲存切片資料的內部緩衝區的首地址及切片長度取出傳傳遞:

package main

/*
#include <stdint.h>

static void fill_255(char* buf, int32_t len) {
	int32_t i;
	for (i = 0; i < len; i++) {
		buf[i] = 255;
	}
}
*/
import "C"
import (
	"fmt"
	"unsafe"
)

func main() {
	b := make([]byte, 5)
	fmt.Println(b) // [0 0 0 0 0]
	C.fill_255((*C.char)(unsafe.Pointer(&b[0])), C.int32_t(len(b)))
	fmt.Println(b) // [255 255 255 255 255]
}

字串

golang 的字串和 C 中的字串在底層的記憶體模型也是不一樣的:

golang 字串符串並沒有用 ‘\0’ 終止符標識字串的結束,因此直接將 golang 字串底層資料指標傳遞給 C 函式是不行的。一種方案類似切片的傳遞一樣將字串資料指標和長度傳遞給 C 函式後,C 函式實現中自行申請一段記憶體拷貝字串資料然後加上未層終止符後再使用。更好的方案是使用標準庫提供的 C.CString() 將 golang 的字串轉換成 C 字串然後傳遞給 C 函式呼叫:

package main

/*
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

static char* cat(char* str1, char* str2) {
	static char buf[256];
	strcpy(buf, str1);
	strcat(buf, str2);

	return buf;
}
*/
import "C"
import (
	"fmt"
	"unsafe"
)

func main() {
	str1, str2 := "hello", " world"
	// golang string -> c string
	cstr1, cstr2 := C.CString(str1), C.CString(str2)
	defer C.free(unsafe.Pointer(cstr1)) // must call
	defer C.free(unsafe.Pointer(cstr2))
	cstr3 := C.cat(cstr1, cstr2)
	// c string -> golang string
	str3 := C.GoString(cstr3)
	fmt.Println(str3) // "hello world"
}

需要注意的是 C.CString() 返回的 C 字串是在堆上新建立的並且不受 GC 的管理,使用完後需要自行呼叫 C.free() 釋放,否則會造成記憶體洩露,而且這種記憶體洩露用前文中介紹的 pprof 也定位不出來。

其他型別

golang 中其他型別(比如 map) 在 C/C++ 中並沒有對等的型別或者記憶體模型也不一樣。傳遞的時候需要了解 golang 型別的底層記憶體模型,然後進行比較精細的記憶體拷貝操作。傳遞 map 的一種方案是可以把 map 的所有鍵值對放到切片裡,然後把切片傳遞給 C++ 函式,C++ 函式再還原成 C++ 標準庫的 map 。由於使用場景比較少,這裡就不贅述了。

總結

本文主要介紹了在 golang 中使用 CGO 呼叫 C/C++ 介面涉及的一些細節問題。C/C++ 比較底層的語言,需要自己管理記憶體。使用 CGO 時需要對 golang 底層的記憶體模型有所瞭解。另外 goroutine 通過 CGO 進入到 C 介面的執行階段後,已經脫離了 golang 執行時的排程並且會獨佔執行緒,此時實際上變成了多執行緒同步的程式設計模型。如果 C 接口裡有阻塞操作,這時候可能會導致所有執行緒都處於阻塞狀態,其他 goroutine 沒有機會得到排程,最終導致整個系統的效能大大較低。總的來說,只有在第三方庫沒有 golang 的實現並且實現起來成本比較高的情況下才需要考慮使用 CGO ,否則慎用。

參考資料