原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch2-cgo/ch2-03-cgo-types.html
最初 CGO 是為了達到方便從 Go 語言函數(shù)調(diào)用 C 語言函數(shù)(用 C 語言實現(xiàn) Go 語言聲明的函數(shù))以復(fù)用 C 語言資源這一目的而出現(xiàn)的(因為 C 語言還會涉及回調(diào)函數(shù),自然也會涉及到從 C 語言函數(shù)調(diào)用 Go 語言函數(shù)(用 Go 語言實現(xiàn) C 語言聲明的函數(shù)))。現(xiàn)在,它已經(jīng)演變?yōu)?C 語言和 Go 語言雙向通訊的橋梁。要想利用好 CGO 特性,自然需要了解此二語言類型之間的轉(zhuǎn)換規(guī)則,這是本節(jié)要討論的問題。
在 Go 語言中訪問 C 語言的符號時,一般是通過虛擬的 “C” 包訪問,比如 C.int
對應(yīng) C 語言的 int
類型。有些 C 語言的類型是由多個關(guān)鍵字組成,但通過虛擬的 “C” 包訪問 C 語言類型時名稱部分不能有空格字符,比如 unsigned int
不能直接通過 C.unsigned int
訪問。因此
CGO 為 C 語言的基礎(chǔ)數(shù)值類型都提供了相應(yīng)轉(zhuǎn)換規(guī)則,比如 C.uint
對應(yīng) C 語言的 unsigned int
。
Go 語言中數(shù)值類型和 C 語言數(shù)據(jù)類型基本上是相似的,以下是它們的對應(yīng)關(guān)系表 2-1 所示。
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 |
表 2-1 Go 語言和 C 語言類型對比
需要注意的是,雖然在 C 語言中 int
、short
等類型沒有明確定義內(nèi)存大小,但是在 CGO 中它們的內(nèi)存大小是確定的。在 CGO 中,C 語言的 int
和 long
類型都是對應(yīng) 4 個字節(jié)的內(nèi)存大小,size_t
類型可以當(dāng)作 Go 語言 uint
無符號整數(shù)類型對待。
CGO 中,雖然 C 語言的 int
固定為 4 字節(jié)的大小,但是 Go 語言自己的 int
和 uint
卻在 32 位和 64 位系統(tǒng)下分別對應(yīng) 4 個字節(jié)和 8 個字節(jié)大小。如果需要在 C 語言中訪問 Go 語言的 int
類型,可以通過 GoInt
類型訪問,GoInt
類型在
CGO 工具生成的 _cgo_export.h
頭文件中定義。其實在 _cgo_export.h
頭文件中,每個基本的 Go 數(shù)值類型都定義了對應(yīng)的 C 語言類型,它們一般都是以單詞 Go 為前綴。下面是 64 位環(huán)境下,_cgo_export.h
頭文件生成的 Go 數(shù)值類型的定義,其中 GoInt
和 GoUint
類型分別對應(yīng) GoInt64
和 GoUint64
:
typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef float GoFloat32;
typedef double GoFloat64;
除了 GoInt
和 GoUint
之外,我們并不推薦直接訪問 GoInt32
、GoInt64
等類型。更好的做法是通過 C 語言的 C99 標(biāo)準(zhǔn)引入的 <stdint.h>
頭文件。為了提高 C 語言的可移植性,在 <stdint.h>
文件中,不但每個數(shù)值類型都提供了明確內(nèi)存大小,而且和
Go 語言的類型命名更加一致。Go 語言類型 <stdint.h>
頭文件類型對比如表 2-2 所示。
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 |
表 2-2 <stdint.h>
類型對比
前文說過,如果 C 語言的類型是由多個關(guān)鍵字組成,則無法通過虛擬的 “C” 包直接訪問(比如 C 語言的 unsigned short
不能直接通過 C.unsigned short
訪問)。但是,在 <stdint.h>
中通過使用 C 語言的 typedef
關(guān)鍵字將 unsigned short
重新定義為 uint16_t
這樣一個單詞的類型后,我們就可以通過 C.uint16_t
訪問原來的 unsigned short
類型了。對于比較復(fù)雜的
C 語言類型,推薦使用 typedef
關(guān)鍵字提供一個規(guī)則的類型命名,這樣更利于在 CGO 中訪問。
在 CGO 生成的 _cgo_export.h
頭文件中還會為 Go 語言的字符串、切片、字典、接口和管道等特有的數(shù)據(jù)類型生成對應(yīng)的 C 語言類型:
typedef struct {const char *p; GoInt n;} GoString;
typedef void *GoMap;
typedef void *GoChan;
typedef struct {void *t; void *v;} GoInterface;
typedef struct {void *data; GoInt len; GoInt cap;} GoSlice;
不過需要注意的是,其中只有字符串和切片在 CGO 中有一定的使用價值,因為 CGO 為他們的某些 GO 語言版本的操作函數(shù)生成了 C 語言版本,因此二者可以在 Go 調(diào)用 C 語言函數(shù)時馬上使用; 而 CGO 并未針對其他的類型提供相關(guān)的輔助函數(shù),且 Go 語言特有的內(nèi)存模型導(dǎo)致我們無法保持這些由 Go 語言管理的內(nèi)存指針,所以它們 C 語言環(huán)境并無使用的價值。
在導(dǎo)出的 C 語言函數(shù)中我們可以直接使用 Go 字符串和切片。假設(shè)有以下兩個導(dǎo)出函數(shù):
//export helloString
func helloString(s string) {}
//export helloSlice
func helloSlice(s []byte) {}
CGO 生成的 _cgo_export.h
頭文件會包含以下的函數(shù)聲明:
extern void helloString(GoString p0);
extern void helloSlice(GoSlice p0);
不過需要注意的是,如果使用了 GoString 類型則會對 _cgo_export.h
頭文件產(chǎn)生依賴,而這個頭文件是動態(tài)輸出的。
Go1.10 針對 Go 字符串增加了一個 _GoString_
預(yù)定義類型,可以降低在 cgo 代碼中可能對 _cgo_export.h
頭文件產(chǎn)生的循環(huán)依賴的風(fēng)險。我們可以調(diào)整 helloString 函數(shù)的 C 語言聲明為:
extern void helloString(_GoString_ p0);
因為 _GoString_
是預(yù)定義類型,我們無法通過此類型直接訪問字符串的長度和指針等信息。Go1.10 同時也增加了以下兩個函數(shù)用于獲取字符串結(jié)構(gòu)中的長度和指針信息:
size_t _GoStringLen(_GoString_ s);
const char *_GoStringPtr(_GoString_ s);
更嚴(yán)謹(jǐn)?shù)淖龇ㄊ菫?C 語言函數(shù)接口定義嚴(yán)格的頭文件,然后基于穩(wěn)定的頭文件實現(xiàn)代碼。
C 語言的結(jié)構(gòu)體、聯(lián)合、枚舉類型不能作為匿名成員被嵌入到 Go 語言的結(jié)構(gòu)體中。在 Go 語言中,我們可以通過 C.struct_xxx
來訪問 C 語言中定義的 struct xxx
結(jié)構(gòu)體類型。結(jié)構(gòu)體的內(nèi)存布局按照 C 語言的通用對齊規(guī)則,在 32 位 Go 語言環(huán)境 C 語言結(jié)構(gòu)體也按照 32 位對齊規(guī)則,在 64 位 Go 語言環(huán)境按照 64 位的對齊規(guī)則。對于指定了特殊對齊規(guī)則的結(jié)構(gòu)體,無法在
CGO 中訪問。
結(jié)構(gòu)體的簡單用法如下:
/*
struct A {
int i;
float f;
};
*/
import "C"
import "fmt"
func main() {
var a C.struct_A
fmt.Println(a.i)
fmt.Println(a.f)
}
如果結(jié)構(gòu)體的成員名字中碰巧是 Go 語言的關(guān)鍵字,可以通過在成員名開頭添加下劃線來訪問:
/*
struct A {
int type; // type 是 Go 語言的關(guān)鍵字
};
*/
import "C"
import "fmt"
func main() {
var a C.struct_A
fmt.Println(a._type) // _type 對應(yīng) type
}
但是如果有 2 個成員:一個是以 Go 語言關(guān)鍵字命名,另一個剛好是以下劃線和 Go 語言關(guān)鍵字命名,那么以 Go 語言關(guān)鍵字命名的成員將無法訪問(被屏蔽):
/*
struct A {
int type; // type 是 Go 語言的關(guān)鍵字
float _type; // 將屏蔽 CGO 對 type 成員的訪問
};
*/
import "C"
import "fmt"
func main() {
var a C.struct_A
fmt.Println(a._type) // _type 對應(yīng) _type
}
C 語言結(jié)構(gòu)體中位字段對應(yīng)的成員無法在 Go 語言中訪問,如果需要操作位字段成員,需要通過在 C 語言中定義輔助函數(shù)來完成。對應(yīng)零長數(shù)組的成員,無法在 Go 語言中直接訪問數(shù)組的元素,但其中零長的數(shù)組成員所在位置的偏移量依然可以通過 unsafe.Offsetof(a.arr)
來訪問。
/*
struct A {
int size: 10; // 位字段無法訪問
float arr[]; // 零長的數(shù)組也無法訪問
};
*/
import "C"
import "fmt"
func main() {
var a C.struct_A
fmt.Println(a.size) // 錯誤: 位字段無法訪問
fmt.Println(a.arr) // 錯誤: 零長的數(shù)組也無法訪問
}
在 C 語言中,我們無法直接訪問 Go 語言定義的結(jié)構(gòu)體類型。
對于聯(lián)合類型,我們可以通過 C.union_xxx
來訪問 C 語言中定義的 union xxx
類型。但是 Go 語言中并不支持 C 語言聯(lián)合類型,它們會被轉(zhuǎn)為對應(yīng)大小的字節(jié)數(shù)組。
/*
#include <stdint.h>
union B1 {
int i;
float f;
};
union B2 {
int8_t i8;
int64_t i64;
};
*/
import "C"
import "fmt"
func main() {
var b1 C.union_B1;
fmt.Printf("%T\n", b1) // [4]uint8
var b2 C.union_B2;
fmt.Printf("%T\n", b2) // [8]uint8
}
如果需要操作 C 語言的聯(lián)合類型變量,一般有三種方法:第一種是在 C 語言中定義輔助函數(shù);第二種是通過 Go 語言的 "encoding/binary" 手工解碼成員 (需要注意大端小端問題);第三種是使用 unsafe
包強制轉(zhuǎn)型為對應(yīng)類型 (這是性能最好的方式)。下面展示通過 unsafe
包訪問聯(lián)合類型成員的方式:
/*
#include <stdint.h>
union B {
int i;
float f;
};
*/
import "C"
import "fmt"
func main() {
var b C.union_B;
fmt.Println("b.i:", *(*C.int)(unsafe.Pointer(&b)))
fmt.Println("b.f:", *(*C.float)(unsafe.Pointer(&b)))
}
雖然 unsafe
包訪問最簡單、性能也最好,但是對于有嵌套聯(lián)合類型的情況處理會導(dǎo)致問題復(fù)雜化。對于復(fù)雜的聯(lián)合類型,推薦通過在 C 語言中定義輔助函數(shù)的方式處理。
對于枚舉類型,我們可以通過 C.enum_xxx
來訪問 C 語言中定義的 enum xxx
結(jié)構(gòu)體類型。
/*
enum C {
ONE,
TWO,
};
*/
import "C"
import "fmt"
func main() {
var c C.enum_C = C.TWO
fmt.Println(c)
fmt.Println(C.ONE)
fmt.Println(C.TWO)
}
在 C 語言中,枚舉類型底層對應(yīng) int
類型,支持負(fù)數(shù)類型的值。我們可以通過 C.ONE
、C.TWO
等直接訪問定義的枚舉值。
在 C 語言中,數(shù)組名其實對應(yīng)于一個指針,指向特定類型特定長度的一段內(nèi)存,但是這個指針不能被修改;當(dāng)把數(shù)組名傳遞給一個函數(shù)時,實際上傳遞的是數(shù)組第一個元素的地址。為了討論方便,我們將一段特定長度的內(nèi)存統(tǒng)稱為數(shù)組。C 語言的字符串是一個 char 類型的數(shù)組,字符串的長度需要根據(jù)表示結(jié)尾的 NULL 字符的位置確定。C 語言中沒有切片類型。
在 Go 語言中,數(shù)組是一種值類型,而且數(shù)組的長度是數(shù)組類型的一個部分。Go 語言字符串對應(yīng)一段長度確定的只讀 byte 類型的內(nèi)存。Go 語言的切片則是一個簡化版的動態(tài)數(shù)組。
Go 語言和 C 語言的數(shù)組、字符串和切片之間的相互轉(zhuǎn)換可以簡化為 Go 語言的切片和 C 語言中指向一定長度內(nèi)存的指針之間的轉(zhuǎn)換。
CGO 的 C 虛擬包提供了以下一組函數(shù),用于 Go 語言和 C 語言之間數(shù)組和字符串的雙向轉(zhuǎn)換:
// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char
// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer
// C string to Go string
func C.GoString(*C.char) string
// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string
// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte
其中 C.CString
針對輸入的 Go 字符串,克隆一個 C 語言格式的字符串;返回的字符串由 C 語言的 malloc
函數(shù)分配,不使用時需要通過 C 語言的 free
函數(shù)釋放。C.CBytes
函數(shù)的功能和 C.CString
類似,用于從輸入的 Go 語言字節(jié)切片克隆一個
C 語言版本的字節(jié)數(shù)組,同樣返回的數(shù)組需要在合適的時候釋放。C.GoString
用于將從 NULL 結(jié)尾的 C 語言字符串克隆一個 Go 語言字符串。C.GoStringN
是另一個字符數(shù)組克隆函數(shù)。C.GoBytes
用于從 C 語言數(shù)組,克隆一個 Go 語言字節(jié)切片。
該組輔助函數(shù)都是以克隆的方式運行。當(dāng) Go 語言字符串和切片向 C 語言轉(zhuǎn)換時,克隆的內(nèi)存由 C 語言的 malloc
函數(shù)分配,最終可以通過 free
函數(shù)釋放。當(dāng) C 語言字符串或數(shù)組向 Go 語言轉(zhuǎn)換時,克隆的內(nèi)存由 Go 語言分配管理。通過該組轉(zhuǎn)換函數(shù),轉(zhuǎn)換前和轉(zhuǎn)換后的內(nèi)存依然在各自的語言環(huán)境中,它們并沒有跨越 Go 語言和 C 語言??寺》绞綄崿F(xiàn)轉(zhuǎn)換的優(yōu)點是接口和內(nèi)存管理都很簡單,缺點是克隆需要分配新的內(nèi)存和復(fù)制操作都會導(dǎo)致額外的開銷。
在 reflect
包中有字符串和切片的定義:
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
如果不希望單獨分配內(nèi)存,可以在 Go 語言中直接訪問 C 語言的內(nèi)存空間:
/*
#include <string.h>
char arr[10];
char *s = "Hello";
*/
import "C"
import (
"reflect"
"unsafe"
)
func main() {
// 通過 reflect.SliceHeader 轉(zhuǎn)換
var arr0 []byte
var arr0Hdr = (*reflect.SliceHeader)(unsafe.Pointer(&arr0))
arr0Hdr.Data = uintptr(unsafe.Pointer(&C.arr[0]))
arr0Hdr.Len = 10
arr0Hdr.Cap = 10
// 通過切片語法轉(zhuǎn)換
arr1 := (*[31]byte)(unsafe.Pointer(&C.arr[0]))[:10:10]
var s0 string
var s0Hdr = (*reflect.StringHeader)(unsafe.Pointer(&s0))
s0Hdr.Data = uintptr(unsafe.Pointer(C.s))
s0Hdr.Len = int(C.strlen(C.s))
sLen := int(C.strlen(C.s))
s1 := string((*[31]byte)(unsafe.Pointer(C.s))[:sLen:sLen])
}
因為 Go 語言的字符串是只讀的,用戶需要自己保證 Go 字符串在使用期間,底層對應(yīng)的 C 字符串內(nèi)容不會發(fā)生變化、內(nèi)存不會被提前釋放掉。
在 CGO 中,會為字符串和切片生成和上面結(jié)構(gòu)對應(yīng)的 C 語言版本的結(jié)構(gòu)體:
typedef struct {const char *p; GoInt n;} GoString;
typedef struct {void *data; GoInt len; GoInt cap;} GoSlice;
在 C 語言中可以通過 GoString
和 GoSlice
來訪問 Go 語言的字符串和切片。如果是 Go 語言中數(shù)組類型,可以將數(shù)組轉(zhuǎn)為切片后再行轉(zhuǎn)換。如果字符串或切片對應(yīng)的底層內(nèi)存空間由 Go 語言的運行時管理,那么在 C 語言中不能長時間保存 Go 內(nèi)存對象。
關(guān)于 CGO 內(nèi)存模型的細(xì)節(jié)在稍后章節(jié)中會詳細(xì)討論。
在 C 語言中,不同類型的指針是可以顯式或隱式轉(zhuǎn)換的,如果是隱式只是會在編譯時給出一些警告信息。但是 Go 語言對于不同類型的轉(zhuǎn)換非常嚴(yán)格,任何 C 語言中可能出現(xiàn)的警告信息在 Go 語言中都可能是錯誤!指針是 C 語言的靈魂,指針間的自由轉(zhuǎn)換也是 cgo 代碼中經(jīng)常要解決的第一個重要的問題。
在 Go 語言中兩個指針的類型完全一致則不需要轉(zhuǎn)換可以直接通用。如果一個指針類型是用 type 命令在另一個指針類型基礎(chǔ)之上構(gòu)建的,換言之兩個指針底層是相同完全結(jié)構(gòu)的指針,那么我我們可以通過直接強制轉(zhuǎn)換語法進行指針間的轉(zhuǎn)換。但是 cgo 經(jīng)常要面對的是 2 個完全不同類型的指針間的轉(zhuǎn)換,原則上這種操作在純 Go 語言代碼是嚴(yán)格禁止的。
cgo 存在的一個目的就是打破 Go 語言的禁止,恢復(fù) C 語言應(yīng)有的指針的自由轉(zhuǎn)換和指針運算。以下代碼演示了如何將 X 類型的指針轉(zhuǎn)化為 Y 類型的指針:
var p *X
var q *Y
q = (*Y)(unsafe.Pointer(p)) // *X => *Y
p = (*X)(unsafe.Pointer(q)) // *Y => *X
為了實現(xiàn) X 類型指針到 Y 類型指針的轉(zhuǎn)換,我們需要借助 unsafe.Pointer
作為中間橋接類型實現(xiàn)不同類型指針之間的轉(zhuǎn)換。unsafe.Pointer
指針類型類似 C 語言中的 void*
類型的指針。
下面是指針間的轉(zhuǎn)換流程的示意圖:
圖 2-1 X 類型指針轉(zhuǎn) Y 類型指針
任何類型的指針都可以通過強制轉(zhuǎn)換為 unsafe.Pointer
指針類型去掉原有的類型信息,然后再重新賦予新的指針類型而達到指針間的轉(zhuǎn)換的目的。
不同類型指針間的轉(zhuǎn)換看似復(fù)雜,但是在 cgo 中已經(jīng)算是比較簡單的了。在 C 語言中經(jīng)常遇到用普通數(shù)值表示指針的場景,也就是說如何實現(xiàn)數(shù)值和指針的轉(zhuǎn)換也是 cgo 需要面對的一個問題。
為了嚴(yán)格控制指針的使用,Go 語言禁止將數(shù)值類型直接轉(zhuǎn)為指針類型!不過,Go 語言針對 unsafe.Pointr
指針類型特別定義了一個 uintptr 類型。我們可以 uintptr 為中介,實現(xiàn)數(shù)值類型到 unsafe.Pointr
指針類型到轉(zhuǎn)換。再結(jié)合前面提到的方法,就可以實現(xiàn)數(shù)值和指針的轉(zhuǎn)換了。
下面流程圖演示了如何實現(xiàn) int32 類型到 C 語言的 char*
字符串指針類型的相互轉(zhuǎn)換:
圖 2-2 int32 和 char*
指針轉(zhuǎn)換
轉(zhuǎn)換分為幾個階段,在每個階段實現(xiàn)一個小目標(biāo):首先是 int32 到 uintptr 類型,然后是 uintptr 到 unsafe.Pointr
指針類型,最后是 unsafe.Pointr
指針類型到 *C.char
類型。
在 C 語言中數(shù)組也一種指針,因此兩個不同類型數(shù)組之間的轉(zhuǎn)換和指針間轉(zhuǎn)換基本類似。但是在 Go 語言中,數(shù)組或數(shù)組對應(yīng)的切片都不再是指針類型,因此我們也就無法直接實現(xiàn)不同類型的切片之間的轉(zhuǎn)換。
不過 Go 語言的 reflect 包提供了切片類型的底層結(jié)構(gòu),再結(jié)合前面討論到不同類型之間的指針轉(zhuǎn)換技術(shù)就可以實現(xiàn) []X
和 []Y
類型的切片轉(zhuǎn)換:
var p []X
var q []Y
pHdr := (*reflect.SliceHeader)(unsafe.Pointer(&p))
qHdr := (*reflect.SliceHeader)(unsafe.Pointer(&q))
pHdr.Data = qHdr.Data
pHdr.Len = qHdr.Len * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0])
pHdr.Cap = qHdr.Cap * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0])
不同切片類型之間轉(zhuǎn)換的思路是先構(gòu)造一個空的目標(biāo)切片,然后用原有的切片底層數(shù)據(jù)填充目標(biāo)切片。如果 X 和 Y 類型的大小不同,需要重新設(shè)置 Len 和 Cap 屬性。需要注意的是,如果 X 或 Y 是空類型,上述代碼中可能導(dǎo)致除 0 錯誤,實際代碼需要根據(jù)情況酌情處理。
下面演示了切片間的轉(zhuǎn)換的具體流程:
圖 2-3 X 類型切片轉(zhuǎn) Y 類型切片
針對 CGO 中常用的功能,作者封裝了 "github.com/chai2010/cgo" 包,提供基本的轉(zhuǎn)換功能,具體的細(xì)節(jié)可以參考實現(xiàn)代碼。
![]() | ![]() |
更多建議: