文章系列
【GO】Golang/C++混合編程 - SWIG
【GO】Golang/C++混合編程 - 初識
【GO】Golang/C++混合編程 - 入門
【GO】Golang/C++混合編程 - 基礎(chǔ)
【GO】Golang/C++混合編程 - 進階一
【GO】Golang/C++混合編程 - 進階二
【GO】Golang/C++混合編程 - 實戰(zhàn)
Golang/C++混合編程
類型轉(zhuǎn)換
CGO 是一個聯(lián)通 GO 語言和 C 語言的雙向橋梁盗舰,它允許 GO 語言調(diào)用 C 語言庫垛孔,反之亦然。CGO 的一個核心功能就是類型轉(zhuǎn)換泽示,它允許 GO 語言和 C 語言之間的數(shù)據(jù)交換。
在 GO 語言中訪問 C 語言的符號時谅猾,一般是通過虛擬的“C”包訪問抗果,比如
C.int
對應 C 語言的 int 類型。有些 C 語言的類型是由多個關(guān)鍵字組成纳像,但通過虛擬的“C”包訪問 C 語言類型時名稱部分不能有空格字符,比如unsigned int不能直接通過C.unsigned int
訪問拯勉。因此CGO為C語言的基礎(chǔ)數(shù)值類型都提供了相應轉(zhuǎn)換規(guī)則竟趾,比如C.uint對應C語言的unsigned int憔购。
基礎(chǔ)類型對照表
以下為CGO類型和C語言類型的對照表:
C語言類型 | CGO類型 | Go語言類型 |
---|---|---|
char | C.char | byte |
signed 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 |
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 |
需要注意的是,雖然在 C 語言中int岔帽、short等類型沒有明確定義內(nèi)存大小玫鸟,但是在 CGO 中它們的內(nèi)存大小是確定的。在 CGO 中犀勒,C 語言的 int 和 long 類型都是對應4個字節(jié)的內(nèi)存大小屎飘,size_t 類型可以當作 Go 語言 uint 無符號整數(shù)類型對待。
CGO 中贾费,雖然 C 語言的 int 固定為4字節(jié)的大小钦购,但是 Go 語言自己的 int 和 uint 卻在32位和64位系統(tǒng)下分別對應4個字節(jié)和8個字節(jié)大小。如果需要在 C 語言中訪問 Go 語言的 int 類型褂萧,可以通過 GoInt 類型訪問押桃。
CGO 頭文件 "_cgo_export.h"
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;
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)存模型導致我們無法保持這些由 Go 語言管理的內(nèi)存指針谎痢,所以它們 C 語言環(huán)境并無使用的價值磕昼。
結(jié)構(gòu)體、聯(lián)合节猿、枚舉類型
結(jié)構(gòu)體
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 中訪問。
// 示例:
package main
/*
struct A {
int i;
float f;
int type; // type 是 Go 語言的關(guān)鍵字
float _type; // 將屏蔽CGO對 type 成員的訪問
};
*/
import "C"
import "fmt"
func main() {
var a C.struct_A
fmt.Println(a.i)
fmt.Println(a.f)
fmt.Println(a._type)
// 未聲明 float _type 時梅惯, _type 對應 int type
// 聲明 float _type 時宪拥, _type 對應 float _type
}
注:C 語言結(jié)構(gòu)體中位字段對應的成員無法在 Go 語言中訪問,如果需要操作位字段成員铣减,需要通過在 C 語言中定義輔助函數(shù)來完成她君。對應零長數(shù)組的成員,無法在 Go 語言中直接訪問數(shù)組的元素葫哗。在C語言中缔刹,我們無法直接訪問Go語言定義的結(jié)構(gòu)體類型球涛。
聯(lián)合
對于聯(lián)合類型,我們可以通過
C.union_xxx
來訪問 C 語言中定義的union xxx
類型校镐。但是Go語言中并不支持C語言聯(lián)合類型亿扁,它們會被轉(zhuǎn)為對應大小的字節(jié)數(shù)組。
// 示例:
package main
/*
#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)型為對應類型(這是性能最好的方式)引谜。下面展示通過unsafe包訪問聯(lián)合類型成員的方式:
fmt.Println("b1.i:", *(*C.int)(unsafe.Pointer(&b1)))
fmt.Println("b1.f:", *(*C.float)(unsafe.Pointer(&b1)))
}
枚舉
對于枚舉類型牍陌,我們可以通過
C.enum_xxx
來訪問 C 語言中定義的enum xxx
結(jié)構(gòu)體類型。
// 示例:
package main
/*
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)
}
數(shù)組员咽、字符串和切片
C/GO 數(shù)組字符串定義
在 C 語言中毒涧,數(shù)組名其實對應于一個指針,指向特定類型特定長度的一段內(nèi)存骏融,但是這個指針不能被修改链嘀;當把數(shù)組名傳遞給一個函數(shù)時,實際上傳遞的是數(shù)組第一個元素的地址档玻。為了討論方便怀泊,我們將一段特定長度的內(nèi)存統(tǒng)稱為數(shù)組。C 語言的字符串是一個 char 類型的數(shù)組误趴,字符串的長度需要根據(jù)表示結(jié)尾的 NULL 字符的位置確定霹琼。C 語言中沒有切片類型。
在 GO 語言中凉当,數(shù)組是一種值類型枣申,而且數(shù)組的長度是數(shù)組類型的一個部分。GO 語言字符串對應一段長度確定的只讀 byte 類型的內(nèi)存看杭。GO 語言的切片則是一個簡化版的動態(tài)數(shù)組忠藤。
相互轉(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ù)都是以克隆的方式運行永淌,轉(zhuǎn)換前和轉(zhuǎn)換后的內(nèi)存依然在各自的語言環(huán)境中,它們并沒有跨越Go語言和C語言佩耳⊙鲑鳎克隆方式實現(xiàn)轉(zhuǎn)換的優(yōu)點是接口和內(nèi)存管理都很簡單,缺點是克隆需要分配新的內(nèi)存和復制操作都會導致額外的開銷蚕愤。
指針
在 C 語言中答恶,不同類型的指針是可以顯式或隱式轉(zhuǎn)換的,如果是隱式只是會在編譯時給出一些警告信息萍诱。但是 GO 語言對于不同類型的轉(zhuǎn)換非常嚴格悬嗓,任何 C 語言中可能出現(xiàn)的警告信息在 GO 語言中都可能是錯誤!指針是 C 語言的靈魂裕坊,指針間的自由轉(zhuǎn)換也是 CGO 代碼中經(jīng)常要解決的第一個重要的問題包竹。
CGO 存在的一個目的就是打破 GO 語言的禁止,恢復 C 語言應有的指針的自由轉(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
任何類型的指針都可以通過強制轉(zhuǎn)換為
unsafe.Pointer
指針類型去掉原有的類型信息周瞎,然后再重新賦予新的指針類型而達到指針間的轉(zhuǎn)換的目的。類似 C 語言的 void* 指針饵蒂。
數(shù)值與指針
在 C 語言中声诸,數(shù)值和指針之間可以相互轉(zhuǎn)換,但是轉(zhuǎn)換的結(jié)果可能出乎意料退盯。在 CGO 中彼乌,數(shù)值和指針之間的轉(zhuǎn)換也是被禁止的,但是通過
unsafe
包渊迁,可以繞過這個限制慰照。GO 語言針對unsafe.Pointr
指針類型特別定義了一個uintptr
類型。我們可以uintptr
為中介琉朽,實現(xiàn)數(shù)值類型到unsafe.Pointr
指針類型到轉(zhuǎn)換毒租。以下代碼演示了如何將數(shù)值轉(zhuǎn)換為指針:
var p *X
var i uintptr
i = uintptr(unsafe.Pointer(p)) // *X => uintptr
p = (*X)(unsafe.Pointer(i)) // uintptr => *X
任何數(shù)值類型都可以通過
uintptr
類型轉(zhuǎn)換為指針類型,但是轉(zhuǎn)換的結(jié)果可能并不是我們期望的箱叁。數(shù)值到指針的轉(zhuǎn)換墅垮,實際上是將數(shù)值解釋為內(nèi)存地址,然后通過這個地址去訪問內(nèi)存蝌蹂。如果數(shù)值對應的內(nèi)存地址是非法的噩斟,那么轉(zhuǎn)換后的指針將無法正常工作曹锨。因此孤个,數(shù)值到指針的轉(zhuǎn)換需要格外小心,必須保證數(shù)值對應的內(nèi)存地址是合法的沛简。
函數(shù)
函數(shù)是 C 語言編程的核心齐鲤,通過 CGO 技術(shù)我們不僅僅可以在 GO 語言中調(diào)用 C 語言函數(shù)斥废,也可以將Go語言函數(shù)導出為C語言函數(shù)。
GO調(diào)用C函數(shù)
package main
/*
static int div(int a, int b) {
return a/b;
}
*/
import "C"
import "fmt"
func main() {
v := C.div(6, 3)
fmt.Println(v)
}
在 C 語言中给郊,函數(shù)并不支持返回多個返回值牡肉。若我們期望 C 語言函數(shù)像 GO 語言函數(shù)一樣同時返回結(jié)果和錯誤信息,則可以借助
errno.h
標準庫所提供的errno
宏來實現(xiàn)淆九。errno
變量是一個全局變量统锤,當 C 語言函數(shù)執(zhí)行失敗時,會設(shè)置errno
變量的值為一個錯誤碼炭庙,然后返回一個錯誤結(jié)果饲窿。GO 語言函數(shù)可以通過檢查errno
變量的值來判斷 C 語言函數(shù)是否執(zhí)行成功。
package main
/*
#include <errno.h>
static int div(int a, int b) {
if(b == 0) {
errno = EINVAL;
return 0;
}
return a/b;
}
*/
import "C"
import "fmt"
func main() {
v0, err0 := C.div(2, 1)
fmt.Println(v0, err0)
v1, err1 := C.div(1, 0)
fmt.Println(v1, err1)
}
C 語言 void 函數(shù)
C 語言函數(shù)還有一種沒有返回值類型的函數(shù)焕蹄,用
void
表示返回值類型逾雄。一般情況下,我們無法獲取void
類型函數(shù)的返回值腻脏,因為沒有返回值可以獲取鸦泳。前面的例子中提到,CGO 對errno
做了特殊處理永品,可以通過第二個返回值來獲取 C 語言的錯誤狀態(tài)做鹰。對于void
類型函數(shù),這個特性依然有效鼎姐。
C調(diào)用GO函數(shù)
CGO 還有一個強大的特性:將 GO 函數(shù)導出為 C 語言函數(shù)誊垢。
package main
import "C"
//export add
func add(a, b C.int) C.int {
return a+b
}
注:當導出 C 語言接口時,需要保證函數(shù)的參數(shù)和返回值類型都是 C 語言友好的類型症见,同時返回值不得直接或間接包含Go語言內(nèi)存空間的指針喂走。如果在兩個不同的 GO 語言包內(nèi),都存在一個同名的要導出為 C 語言函數(shù)的
add
函數(shù)谋作,那么在最終的鏈接階段將會出現(xiàn)符號重名的問題芋肠。