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 ,否則慎用。
參考資料
- https://golang.org/cmd/cgo/
golang cgo 使用總結