golang cgo 使用總結

原文地址

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

參數傳遞

基本數值類型

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 函數調用,而是須要將存儲切片數據的內部緩衝區的首地址及切片長度取出傳傳遞:post

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 函數調用:ui

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 也定位不出來。spa

其餘類型

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 ,不然慎用。

參考資料

相關文章
相關標籤/搜索