新聞中心
作者:panhuili,騰訊 IEG 后臺(tái)開發(fā)工程師

創(chuàng)新互聯(lián)建站10多年成都定制網(wǎng)站服務(wù);為您提供網(wǎng)站建設(shè),網(wǎng)站制作,網(wǎng)頁設(shè)計(jì)及高端網(wǎng)站定制服務(wù),成都定制網(wǎng)站及推廣,對小攪拌車等多個(gè)方面擁有多年的營銷推廣經(jīng)驗(yàn)的網(wǎng)站建設(shè)公司。
Go 作為當(dāng)下最火的開發(fā)語言之一,它的優(yōu)勢不必多說。Go 對于高并發(fā)的支持,使得它可以很方便的作為獨(dú)立模塊嵌入業(yè)務(wù)系統(tǒng)。有鑒于我司大量的 C/C++存量代碼,如何將 Go 和 C/C++進(jìn)行打通就尤為重要。Golang 自帶的 CGO 可以支持與 C 語言接口的互通。本文首先介紹了 cgo 的常見用法,然后根據(jù)底層代碼分析其實(shí)現(xiàn)機(jī)制,最后在特定場景下進(jìn)行 cgo 實(shí)踐。
一、CGO 快速入門
1.1、啟用 CGO 特性
在 golang 代碼中加入 import “C” 語句就可以啟動(dòng) CGO 特性。這樣在進(jìn)行 go build 命令時(shí),就會(huì)在編譯和連接階段啟動(dòng) gcc 編譯器。
- // go.1.15// test1.go
- package main
- import "C" // import "C"更像是一個(gè)關(guān)鍵字,CGO工具在預(yù)處理時(shí)會(huì)刪掉這一行
- func main() {
- }
使用 -x 選項(xiàng)可以查看 go 程序編譯過程中執(zhí)行的所有指令??梢钥吹?golang 編譯器已經(jīng)為 test1.go 創(chuàng)建了 CGO 編譯選項(xiàng)
- [root@VM-centos ~/cgo_test/golink2]# go build -x test1.go
- WORK=/tmp/go-build330287398
- mkdir -p $WORK/b001/
- cd /root/cgo_test/golink2
- CGO_LDFLAGS='"-g" "-O2"' /usr/lib/golang/pkg/tool/linux_amd64/cgo -objdir $WORK/b001/ -importpath command-line-arguments -- -I $WORK/b001/ -g -O2 ./test1.go # CGO編譯選項(xiàng)
- cd $WORK
- gcc -fno-caret-diagnostics -c -x c - -o /dev/null || true
- gcc -Qunused-arguments -c -x c - -o /dev/null || true
- gcc -fdebug-prefix-map=a=b -c -x c - -o /dev/null || true
- gcc -gno-record-gcc-switches -c -x c - -o /dev/null || true
- .......
1.2、Hello Cgo
通過 import “C” 語句啟用 CGO 特性后,CGO 會(huì)將上一行代碼所處注釋塊的內(nèi)容視為 C 代碼塊,被稱為序文(preamble)。
- // test2.go
- package main
- //#include
// 序文中可以鏈接標(biāo)準(zhǔn)C程序庫 - import "C"
- func main() {
- C.puts(C.CString("Hello, Cgo\n"))
- }
在序文中可以使用 C.func 的方式調(diào)用 C 代碼塊中的函數(shù),包括庫文件中的函數(shù)。對于 C 代碼塊的變量,類型也可以使用相同方法進(jìn)行調(diào)用。
test2.go 通過 CGO 提供的 C.CString 函數(shù)將 Go 語言字符串轉(zhuǎn)化為 C 語言字符串,最后再通過 C.puts 調(diào)用 中的 puts 函數(shù)向標(biāo)準(zhǔn)輸出打印字符串。
1.3 cgo 工具
當(dāng)你在包中引用 import "C",go build 就會(huì)做很多額外的工作來構(gòu)建你的代碼,構(gòu)建就不僅僅是向 go tool compile 傳遞一堆 .go 文件了,而是要先進(jìn)行以下步驟:
1)cgo 工具就會(huì)被調(diào)用,在 C 轉(zhuǎn)換 Go、Go 轉(zhuǎn)換 C 的之間生成各種文件。
2)系統(tǒng)的 C 編譯器會(huì)被調(diào)用來處理包中所有的 C 文件。
3)所有獨(dú)立的編譯單元會(huì)被組合到一個(gè) .o 文件。
4)生成的 .o 文件會(huì)在系統(tǒng)的連接器中對它的引用進(jìn)行一次檢查修復(fù)。
cgo 是一個(gè) Go 語言自帶的特殊工具,可以使用命令 go tool cgo 來運(yùn)行。它可以生成能夠調(diào)用 C 語言代碼的 Go 語言源文件,也就是說所有啟用了 CGO 特性的 Go 代碼,都會(huì)首先經(jīng)過 cgo 的"預(yù)處理"。
對 test2.go,cgo 工具會(huì)在同目錄生成以下文件
- _obj--|
- |--_cgo.o // C代碼編譯出的鏈接庫
- |--_cgo_main.c // C代碼部分的main函數(shù)
- |--_cgo_flags // C代碼的編譯和鏈接選項(xiàng)
- |--_cgo_export.c //
- |--_cgo_export.h // 導(dǎo)出到C語言的Go類型
- |--_cgo_gotypes.go // 導(dǎo)出到Go語言的C類型
- |--test1.cgo1.go // 經(jīng)過“預(yù)處理”的Go代碼
- |--test1.cgo2.c // 經(jīng)過“預(yù)處理”的C代碼
二、CGO 的 N 種用法
CGO 作為 Go 語言和 C 語言之間的橋梁,其使用場景可以分為兩種:Go 調(diào)用 C 程序 和 C 調(diào)用 Go 程序。
2.1、Go 調(diào)用自定義 C 程序
- // test3.go
- package main
- /*
- #cgo LDFLAGS: -L/usr/local/lib
- #include
- #include
- #define REPEAT_LIMIT 3 // CGO會(huì)保留C代碼塊中的宏定義
- typedef struct{ // 自定義結(jié)構(gòu)體
- int repeat_time;
- char* str;
- }blob;
- int SayHello(blob* pblob) { // 自定義函數(shù)
- for ( ;pblob->repeat_time < REPEAT_LIMIT; pblob->repeat_time++){
- puts(pblob->str);
- }
- return 0;
- }
- */
- import "C"
- import (
- "fmt"
- "unsafe"
- )
- func main() {
- cblob := C.blob{} // 在GO程序中創(chuàng)建的C對象,存儲(chǔ)在Go的內(nèi)存空間
- cblob.repeat_time = 0
- cblob.str = C.CString("Hello, World\n") // C.CString 會(huì)在C的內(nèi)存空間申請一個(gè)C語言字符串對象,再將Go字符串拷貝到C字符串
- ret := C.SayHello(&cblob) // &cblob 取C語言對象cblob的地址
- fmt.Println("ret", ret)
- fmt.Println("repeat_time", cblob.repeat_time)
- C.free(unsafe.Pointer(cblob.str)) // C.CString 申請的C空間內(nèi)存不會(huì)自動(dòng)釋放,需要顯示調(diào)用C中的free釋放
- }
CGO 會(huì)保留序文中的宏定義,但是并不會(huì)保留注釋,也不支持#program,C 代碼塊中的#program 語句極可能產(chǎn)生未知錯(cuò)誤。
CGO 中使用 #cgo 關(guān)鍵字可以設(shè)置編譯階段和鏈接階段的相關(guān)參數(shù),可以使用 ${SRCDIR} 來表示 Go 包當(dāng)前目錄的絕對路徑。
使用 C.結(jié)構(gòu)名 或 C.struct_結(jié)構(gòu)名 可以在 Go 代碼段中定義 C 對象,并通過成員名訪問結(jié)構(gòu)體成員。
test3.go 中使用 C.CString 將 Go 字符串對象轉(zhuǎn)化為 C 字符串對象,并將其傳入 C 程序空間進(jìn)行使用,由于 C 的內(nèi)存空間不受 Go 的 GC 管理,因此需要顯示的調(diào)用 C 語言的 free 來進(jìn)行回收。詳情見第三章。
2.2、Go 調(diào)用 C/C++模塊
2.2.1、簡單 Go 調(diào) C
直接將完整的 C 代碼放在 Go 源文件中,這種編排方式便于開發(fā)人員快速在 C 代碼和 Go 代碼間進(jìn)行切換。
- // demo/test4.go
- package main
- /*
- #include
- int SayHello() {
- puts("Hello World");
- return 0;
- }
- */
- import "C"
- import (
- "fmt"
- )
- func main() {
- ret := C.SayHello()
- fmt.Println(ret)
- }
但是當(dāng) CGO 中使用了大量的 C 語言代碼時(shí),將所有的代碼放在同一個(gè) go 文件中即不利于代碼復(fù)用,也會(huì)影響代碼的可讀性。此時(shí)可以將 C 代碼抽象成模塊,再將 C 模塊集成入 Go 程序中。
2.2.2、Go 調(diào)用 C 模塊
將 C 代碼進(jìn)行抽象,放到相同目錄下的 C 語言源文件 hello.c 中
- // demo/hello.c
- #include
- int SayHello() {
- puts("Hello World");
- return 0;
- }
在 Go 代碼中,聲明 SayHello() 函數(shù),再引用 hello.c 源文件,就可以調(diào)起外部 C 源文件中的函數(shù)了。同理也可以將C 源碼編譯打包為靜態(tài)庫或動(dòng)態(tài)庫進(jìn)行使用。
- // demo/test5.go
- package main
- /*
- #include "hello.c"
- int SayHello();
- */
- import "C"
- import (
- "fmt"
- )
- func main() {
- ret := C.SayHello()
- fmt.Println(ret)
- }
test5.go 中只對 SayHello 函數(shù)進(jìn)行了聲明,然后再通過鏈接 C 程序庫的方式加載函數(shù)的實(shí)現(xiàn)。那么同樣的,也可以通過鏈接 C++程序庫的方式,來實(shí)現(xiàn) Go 調(diào)用 C++程序。
2.2.3、Go 調(diào)用 C++模塊
基于 test4。可以抽象出一個(gè) hello 模塊,將模塊的接口函數(shù)在 hello.h 頭文件進(jìn)行定義
- // demo/hello.h
- int SayHello();
再使用 C++來重新實(shí)現(xiàn)這個(gè) C 函數(shù)
- // demo/hello.cpp
- #include
- extern "C" {
- #include "hello.h"
- }
- int SayHello() {
- std::cout<<"Hello World";
- return 0;
- }
最后再在 Go 代碼中,引用 hello.h 頭文件,就可以調(diào)用 C++實(shí)現(xiàn)的 SayHello 函數(shù)了
- // demo/test6.go
- package main
- /*
- #include "hello.h"
- */
- import "C"
- import (
- "fmt"
- )
- func main() {
- ret := C.SayHello()
- fmt.Println(ret)
- }
CGO 提供的這種面向 C 語言接口的編程方式,使得開發(fā)者可以使用是任何編程語言來對接口進(jìn)行實(shí)現(xiàn),只要最終滿足 C 語言接口即可。
2.3、C 調(diào)用 Go 模塊
C 調(diào)用 Go 相對于 Go 調(diào) C 來說要復(fù)雜多,可以分為兩種情況。一是原生 Go 進(jìn)程調(diào)用 C,C 中再反調(diào) Go 程序。另一種是原生 C 進(jìn)程直接調(diào)用 Go。
2.3.1、Go 實(shí)現(xiàn)的 C 函數(shù)
如前述,開發(fā)者可以用任何編程語言來編寫程序,只要支持 CGO 的 C 接口標(biāo)準(zhǔn),就可以被 CGO 接入。那么同樣可以用 Go 實(shí)現(xiàn) C 函數(shù)接口。
在 test6.go 中,已經(jīng)定義了 C 接口模塊 hello.h
- // demo/hello.h
- void SayHello(char* s);
可以創(chuàng)建一個(gè) hello.go 文件,來用 Go 語言實(shí)現(xiàn) SayHello 函數(shù)
- // demo/hello.go
- package main
- //#include
- import "C"
- import "fmt"
- //export SayHello
- func SayHello(str *C.char) {
- fmt.Println(C.GoString(str))
- }
CGO 的//export SayHello 指令將 Go 語言實(shí)現(xiàn)的 SayHello 函數(shù)導(dǎo)出為 C 語言函數(shù)。這樣再 Go 中調(diào)用 C.SayHello 時(shí),最終調(diào)用的是 hello.go 中定義的 Go 函數(shù) SayHello
- // demo/test7.go
- // go run ../demo
- package main
- //#include "hello.h"
- import "C"
- func main() {
- C.SayHello(C.CString("Hello World"))
- }
Go 程序先調(diào)用 C 的 SayHello 接口,由于 SayHello 接口鏈接在 Go 的實(shí)現(xiàn)上,又調(diào)到 Go。
看起來調(diào)起方和實(shí)現(xiàn)方都是 Go,但實(shí)際執(zhí)行順序是 Go 的 main 函數(shù),調(diào)到 CGO 生成的 C 橋接函數(shù),最后 C 橋接函數(shù)再調(diào)到 Go 的 SayHello。這部分會(huì)在第四章進(jìn)行分析。
2.3.2、原生 C 調(diào)用 Go
C 調(diào)用到 Go 這種情況比較復(fù)雜,Go 一般是便以為 c-shared/c-archive 的庫給 C 調(diào)用。
- // demo/hello.go
- package main
- import "C"
- //export hello
- func hello(value string)*C.char { // 如果函數(shù)有返回值,則要將返回值轉(zhuǎn)換為C語言對應(yīng)的類型
- return C.CString("hello" + value)
- }
- func main(){
- // 此處一定要有main函數(shù),有main函數(shù)才能讓cgo編譯器去把包編譯成C的庫
- }
如果 Go 函數(shù)有多個(gè)返回值,會(huì)生成一個(gè) C 結(jié)構(gòu)體進(jìn)行返回,結(jié)構(gòu)體定義參考生成的.h 文件
生成 c-shared 文件 命令
- go build -buildmode=c-shared -o hello.so hello.go
在 C 代碼中,只需要引用 go build 生成的.h 文件,并在編譯時(shí)鏈接對應(yīng)的.so 程序庫,即可從 C 調(diào)用 Go 程序
- // demo/test8.c
- #include
- #include
- #include "hello.h" //此處為上一步生成的.h文件
- int main(){
- char c1[] = "did";
- GoString s1 = {c1,strlen(c1)}; //構(gòu)建Go語言的字符串類型
- char *c = hello(s1);
- printf("r:%s",c);
- return 0;
- }
編譯命令
- gcc -o c_go main.c hello.so
C 函數(shù)調(diào)入進(jìn) Go,必須按照 Go 的規(guī)則執(zhí)行,當(dāng)主程序是 C 調(diào)用 Go 時(shí),也同樣有一個(gè) Go 的 runtime 與 C 程序并行執(zhí)行。這個(gè) runtime 的初始化在對應(yīng)的 c-shared 的庫加載時(shí)就會(huì)執(zhí)行。因此,在進(jìn)程啟動(dòng)時(shí)就有兩個(gè)線程執(zhí)行,一個(gè) C 的,一 (多)個(gè)是 Go 的。
三、類型轉(zhuǎn)換
想要更好的使用 CGO 必須了解 Go 和 C 之間類型轉(zhuǎn)換的規(guī)則
3.1、數(shù)值類型
在 Go 語言中訪問 C 語言的符號(hào)時(shí),一般都通過虛擬的“C”包進(jìn)行。比如 C.int,C.char 就對應(yīng)與 C 語言中的 int 和 char,對應(yīng)于 Go 語言中的 int 和 byte。
C 語言和 Go 語言的數(shù)值類型對應(yīng)如下:
Go 語言的 int 和 uint 在 32 位和 64 位系統(tǒng)下分別是 4 個(gè)字節(jié)和 8 個(gè)字節(jié)大小。它在 C 語言中的導(dǎo)出類型 GoInt 和 GoUint 在不同位數(shù)系統(tǒng)下內(nèi)存大小也不同。
如下是 64 位系統(tǒng)中,Go 數(shù)值類型在 C 語言的導(dǎo)出列表
- // _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 __SIZE_TYPE__ GoUintptr;
- typedef float GoFloat32;
- typedef double GoFloat64;
- typedef float _Complex GoComplex64;
- typedef double _Complex GoComplex128;
需要注意的是在 C 語言符號(hào)名前加上 Ctype, 便是其在 Go 中的導(dǎo)出名,因此在啟用 CGO 特性后,Go 語言中禁止出現(xiàn)以Ctype 開頭的自定義符號(hào)名,類似的還有Cfunc等。
可以在序文中引入_obj/_cgo_export.h 來顯式使用 cgo 在 C 中的導(dǎo)出類型
- // test9.go
- package main
- /*
- #include "_obj/_cgo_export.h" // _cgo_export.h由cgo工具動(dòng)態(tài)生成
- GoInt32 Add(GoInt32 param1, GoInt32 param2) { // GoInt32即為cgo在C語言的導(dǎo)出類型
- return param1 + param2;
- }
- */
- import "C"
- import "fmt"
- func main() {
- // _Ctype_ // _Ctype_ 會(huì)在cgo預(yù)處理階段觸發(fā)異常,
- fmt.Println(C.Add(1, 2))
- }
如下是 64 位系統(tǒng)中,C 數(shù)值類型在 Go 語言的導(dǎo)出列表
- // _cgo_gotypes.go
- type _Ctype_char int8
- type _Ctype_double float64
- type _Ctype_float float32
- type _Ctype_int int32
- type _Ctype_long int64
- type _Ctype_longlong int64
- type _Ctype_schar int8
- type _Ctype_short int16
- type _Ctype_size_t = _Ctype_ulong
- type _Ctype_uchar uint8
- type _Ctype_uint uint32
- type _Ctype_ulong uint64
- type _Ctype_ulonglong uint64
- type _Ctype_void [0]byte
為了提高 C 語言的可移植性,更好的做法是通過 C 語言的 C99 標(biāo)準(zhǔn)引入的頭文件,不但每個(gè)數(shù)值類型都提供了明確內(nèi)存大小,而且和 Go 語言的類型命名更加一致。
3.2、切片
Go 中切片的使用方法類似 C 中的數(shù)組,但是內(nèi)存結(jié)構(gòu)并不一樣。C 中的數(shù)組實(shí)際上指的是一段連續(xù)的內(nèi)存,而 Go 的切片在存儲(chǔ)數(shù)據(jù)的連續(xù)內(nèi)存基礎(chǔ)上,還有一個(gè)頭結(jié)構(gòu)體,其內(nèi)存結(jié)構(gòu)如下
因此 Go 的切片不能直接傳遞給 C 使用,而是需要取切片的內(nèi)部緩沖區(qū)的首地址(即首個(gè)元素的地址)來傳遞給 C 使用。使用這種方式把 Go 的內(nèi)存空間暴露給 C 使用,可以大大減少 Go 和 C 之間參數(shù)傳遞時(shí)內(nèi)存拷貝的消耗。
- // test10.go
- package main
- /*
- int SayHello(char* buff, int len) {
- char hello[] = "Hello Cgo!";
- int movnum = len < sizeof(hello) ? len:sizeof(hello);
- memcpy(buff, hello, movnum); // go字符串沒有'\0',所以直接內(nèi)存拷貝
- return movnum;
- }
- */
- import "C"
- import (
- "fmt"
- "unsafe"
- )
- func main() {
- buff := make([]byte, 8)
- C.SayHello((*C.char)(unsafe.Pointer(&buff[0])), C.int(len(buff)))
- a := string(buff)
- fmt.Println(a)
- }
3.3 字符串
Go 的字符串與 C 的字符串在底層的內(nèi)存模型也不一樣:
Go 的字符串并沒有以'\0' 結(jié)尾,因此使用類似切片的方式,直接將 Go 字符串的首元素地址傳遞給 C 是不可行的。
3.3.1、Go 與 C 的字符串傳遞
cgo 給出的解決方案是標(biāo)準(zhǔn)庫函數(shù) C.CString(),它會(huì)在 C 內(nèi)存空間內(nèi)申請足夠的空間,并將 Go 字符串拷貝到 C 空間中。因此 C.CString 申請的內(nèi)存在 C 空間中,因此需要顯式的調(diào)用 C.free 來釋放空間,如 test3。
如下是 C.CString()的底層實(shí)現(xiàn)
- func _Cfunc_CString(s string) *_Ctype_char { // 從Go string 到 C char* 類型轉(zhuǎn)換
- p := _cgo_cmalloc(uint64(len(s)+1))
- pp := (*[1<<30]byte)(p)
- copy(pp[:], s)
- pp[len(s)] = 0
- return (*_Ctype_char)(p)
- }
- //go:cgo_unsafe_args
- func _cgo_cmalloc(p0 uint64) (r1 unsafe.Pointer) {
- _cgo_runtime_cgocall(_cgo_bb7421b6328a_Cfunc__Cmalloc, uintptr(unsafe.Pointer(&p0)))
- if r1 == nil {
- runtime_throw("runtime: C malloc failed")
- }
- return
- }
_Cfunc_CString
_Cfunc_CString 是 cgo 定義的從 Go string 到 C char* 的類型轉(zhuǎn)換函數(shù)
1)使用_cgo_cmalloc 在 C 空間內(nèi)申請內(nèi)存(即不受 Go GC 控制的內(nèi)存)
2)使用該段 C 內(nèi)存初始化一個(gè)[]byte 對象
3)將 string 拷貝到[]byte 對象
4)將該段 C 空間內(nèi)存的地址返回
它的實(shí)現(xiàn)方式類似前述,切片的類型轉(zhuǎn)換。不同在于切片的類型轉(zhuǎn)換,是將 Go 空間內(nèi)存暴露給 C 函數(shù)使用。而_Cfunc_CString 是將 C 空間內(nèi)存暴露給 Go 使用。
_cgo_cmalloc
定義了一個(gè)暴露給 Go 的 C 函數(shù),用于在 C 空間申請內(nèi)存
與 C.CString()對應(yīng)的是從 C 字符串轉(zhuǎn) Go 字符串的轉(zhuǎn)換函數(shù) C.GoString()。C.GoString()函數(shù)的實(shí)現(xiàn)較為簡單,檢索 C 字符串長度,然后申請相同長度的 Go-string 對象,最后內(nèi)存拷貝。
如下是 C.GoString()的底層實(shí)現(xiàn)
- //go:linkname _cgo_runtime_gostring runtime.gostring
- func _cgo_runtime_gostring(*_Ctype_char) string
- func _Cfunc_GoString(p *_Ctype_char) string { // 從C char* 到 Go string 類型轉(zhuǎn)換
- return _cgo_runtime_gostring(p)
- }
- //go:linkname gostring
- func gostring(p *byte) string { // 底層實(shí)現(xiàn)
- l := findnull(p)
- if l == 0 {
- return ""
- }
- s, b := rawstring(l)
- memmove(unsafe.Pointer(&b[0]), unsafe.Pointer(p), uintptr(l))
- return s
- }
3.3.2、更高效的字符串傳遞方法
C.CString 簡單安全,但是它涉及了一次從 Go 到 C 空間的內(nèi)存拷貝,對于長字符串而言這會(huì)是難以忽視的開銷。
Go 官方文檔中聲稱 string 類型是”不可改變的“,但是在實(shí)操中可以發(fā)現(xiàn),除了常量字符串會(huì)在編譯期被分配到只讀段,其他的動(dòng)態(tài)生成的字符串實(shí)際上都是在堆上。
因此如果能夠獲得 string 的內(nèi)存緩存區(qū)地址,那么就可以使用類似切片傳遞的方式將字符串指針和長度直接傳遞給 C 使用。
查閱源碼,可知 String 實(shí)際上是由緩沖區(qū)首地址 和 長度構(gòu)成的。這樣就可以通過一些方式拿到緩存區(qū)地址。
- type stringStruct struct {
- str unsafe.Pointer //str首地址
- len int //str長度
- }
test11.go 將 fmt 動(dòng)態(tài)生成的 string 轉(zhuǎn)為自定義類型 MyString 便可以獲得緩沖區(qū)首地址,將地址傳入 C 函數(shù),這樣就可以在 C 空間直接操作 Go-String 的內(nèi)存空間了,這樣可以免去內(nèi)存拷貝的消耗。
- // test11.go
- package main
- /*
- #include
- int SayHello(char* buff, int len) {
- char hello[] = "Hello Cgo!";
- int movnum = len < sizeof(hello) ? len:sizeof(hello);
- memcpy(buff, hello, movnum);
- return movnum;
- }
- */
- import "C"
- import (
- "fmt"
- "unsafe"
- )
- type MyString struct {
- Str *C.char
- Len int
- }
- func main() {
- s := fmt.Sprintf(" ")
- C.SayHello((*MyString)(unsafe.Pointer(&s)).Str, C.int((*MyString)(unsafe.Pointer(&s)).Len))
- fmt.Print(s)
- }
這種方法背離了 Go 語言的設(shè)計(jì)理念,如非必要,不要把這種代碼帶入你的工程,這里只是作為一種“黑科技”進(jìn)行分享。
3.4、結(jié)構(gòu)體,聯(lián)合,枚舉
cgo 中結(jié)構(gòu)體,聯(lián)合,枚舉的使用方式類似,可以通過 C.struct_XXX 來訪問 C 語言中 struct XXX 類型。union,enum 也類似。
3.4.1、結(jié)構(gòu)體
如果結(jié)構(gòu)體的成員名字中碰巧是 Go 語言的關(guān)鍵字,可以通過在成員名開頭添加下劃線來訪問
如果有 2 個(gè)成員:一個(gè)是以 Go 語言關(guān)鍵字命名,另一個(gè)剛好是以下劃線和 Go 語言關(guān)鍵字命名,那么以 Go 語言關(guān)鍵字命名的成員將無法訪問(被屏蔽)
C 語言結(jié)構(gòu)體中位字段對應(yīng)的成員無法在 Go 語言中訪問,如果需要操作位字段成員,需要通過在 C 語言中定義輔助函數(shù)來完成。對應(yīng)零長數(shù)組的成員(C 中經(jīng)典的變長數(shù)組),無法在 Go 語言中直接訪問數(shù)組的元素,但同樣可以通過在 C 中定義輔助函數(shù)來訪問。
結(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 中訪問。
- // test11.go
- package main
- /*
- struct Test {
- int a;
- float b;
- double type;
- int size:10;
- int arr1[10];
- int arr2[];
- };
- int Test_arr2_helper(struct Test * tm ,int pos){
- return tm->arr2[pos];
- }
- #pragma pack(1)
- struct Test2 {
- float a;
- char b;
- int c;
- };
- */
- import "C"
- import "fmt"
- func main() {
- test := C.struct_Test{}
- fmt.Println(test.a)
- fmt.Println(test.b)
- fmt.Println(test._type)
- //fmt.Println(test.size) // 位數(shù)據(jù)
- fmt.Println(test.arr1[0])
- //fmt.Println(test.arr) // 零長數(shù)組無法直接訪問
- //Test_arr2_helper(&test, 1)
- test2 := C.struct_Test2{}
- fmt.Println(test2.c)
- //fmt.Println(test2.c) // 由于內(nèi)存對齊,該結(jié)構(gòu)體部分字段Go無法訪問
- }
3.4.2、聯(lián)合
Go 語言中并不支持 C 語言聯(lián)合類型,它們會(huì)被轉(zhuǎn)為對應(yīng)大小的字節(jié)數(shù)組。
如果需要操作 C 語言的聯(lián)合類型變量,一般有三種方法:第一種是在 C 語言中定義輔助函數(shù);第二種是通過 Go 語言的"encoding/binary"手工解碼成員(需要注意大端小端問題);第三種是使用unsafe包強(qiáng)制轉(zhuǎn)型為對應(yīng)類型(這是性能最好的方式)。
test12 給出了 union 的三種訪問方式
- // test12.go
- package main
- /*
- #include
- union SayHello {
- int Say;
- float Hello;
- };
- union SayHello init_sayhello(){
- union SayHello us;
- us.Say = 100;
- return us;
- }
- int SayHello_Say_helper(union SayHello * us){
- return us->Say;
- }
- */
- import "C"
- import (
- "fmt"
- "unsafe"
- "encoding/binary"
- )
- func main() {
- SayHello := C.init_sayhello()
- fmt.Println("C-helper ",C.SayHello_Say_helper(&SayHello)) // 通過C輔助函數(shù)
- buff := C.GoBytes(unsafe.Pointer(&SayHello), 4)
- Say2 := binary.LittleEndian.Uint32(buff)
- fmt.Println("binary ",Say2) // 從內(nèi)存直接解碼一個(gè)int32
- fmt.Println("unsafe modify ", *(*C.int)(unsafe.Pointer(&SayHello))) // 強(qiáng)制類型轉(zhuǎn)換
- }
3.4.3、枚舉
對于枚舉類型,可以通過C.enum_xxx來訪問 C 語言中定義的enum xxx結(jié)構(gòu)體類型。
使用方式和 C 相同,這里就不列例子了
3.5、指針
在 Go 語言中兩個(gè)指針的類型完全一致則不需要轉(zhuǎn)換可以直接通用。如果一個(gè)指針類型是用 type 命令在另一個(gè)指針類型基礎(chǔ)之上構(gòu)建的,換言之兩個(gè)指針底層是相同完全結(jié)構(gòu)的指針,那么也可以通過直接強(qiáng)制轉(zhuǎn)換語法進(jìn)行指針間的轉(zhuǎn)換。
但是 C 語言中,不同類型的指針是可以顯式或隱式轉(zhuǎn)換。cgo 經(jīng)常要面對的是 2 個(gè)完全不同類型的指針間的轉(zhuǎn)換,實(shí)現(xiàn)這一轉(zhuǎn)換的關(guān)鍵就是 unsafe.Pointer,類似于 C 語言中的 Void*類型指針。
使用這種方式就可以實(shí)現(xiàn)不同類型間的轉(zhuǎn)換,如下是從 Go - int32 到 *C.char 的轉(zhuǎn)換。
四、內(nèi)部機(jī)制
go tool cgo 是分析 CGO 內(nèi)部運(yùn)行機(jī)制的重要工具,本章根據(jù) cgo 工具生成的中間代碼,再輔以 Golang 源碼中 runtime 部分,來對 cgo 的內(nèi)部運(yùn)行機(jī)制進(jìn)行分析。
cgo 的工作流程為:代碼預(yù)處理 -> gcc 編譯 -> Go Complier 編譯。其產(chǎn)生的中間文件如圖所示
4.1、Go 調(diào) C
Go 調(diào) C 的過程比較簡單。test13 中定義了一個(gè) C 函數(shù) sum,并在 Go 中調(diào)用了 C.sum。
- package main
- //int sum(int a, int b) { return a+b; }
- import "C"
- func main() {
- println(C.sum(1, 1))
- }
下面是 cgo 工具產(chǎn)生的中間文件,最重要的是 test13.cgo1.go,test13.cgo1.c,_cgo_gotypes.go
test13.cgo1.go
test13.cgo1.go 是原本 test13.go 被 cgo 處理之后的文件。
- // Code generated by cmd/cgo; DO NOT EDIT.
- //line test4.go:1:1
- package main
- //int sum(int a, int b) { return a+b; }
- import _ "unsafe"
- func main() {
- println(( /*line :7:10*/_Cfunc_sum /*line :7:14*/)(1, 1))
- }
這個(gè)文件才是 go complier 真正編譯的代碼。可以看到原本的C.sum 被改寫為_Cfunc_sum,_Cfunc_sum的定義在_cgo_gotypes.go 中。
_cgo_gotypes.go
- // Code generated by cmd/cgo; DO NOT EDIT.
- package main
- import "unsafe"
- import _ "runtime/cgo"
- import "syscall"
- var _ syscall.Errno
- func _Cgo_ptr(ptr unsafe.Pointer) unsafe.Pointer { return ptr }
- //go:linkname _Cgo_always_false runtime.cgoAlwaysFalse
- var _Cgo_always_false bool // 永遠(yuǎn)為 false
- //go:linkname _Cgo_use runtime.cgoUse
- func _Cgo_use(interface{}) // 返回一個(gè) Error
- type _Ctype_int int32 // CGO類型導(dǎo)出
- type _Ctype_void [0]byte // CGO類型導(dǎo)出
- //go:linkname _cgo_runtime_cgocall runtime.cgocall
- func _cgo_runtime_cgocall(unsafe.Pointer, uintptr) int32 // Go調(diào)C的入口函數(shù)
- //go:linkname _cgo_runtime_cgocallback runtime.cgocallback
- func _cgo_runtime_cgocallback(unsafe.Pointer, unsafe.Pointer, uintptr, uintptr) // 回調(diào)入口
- //go:linkname _cgoCheckPointer runtime.cgoCheckPointer
- func _cgoCheckPointer(interface{}, interface{}) // 檢查傳入C的指針,防止傳入了指向Go指針的Go指針
- //go:linkname _cgoCheckResult runtime.cgoCheckResult
- func _cgoCheckResult(interface{}) // 檢查返回值,防止返回了一個(gè)Go指針
- //go:cgo_import_static _cgo_53efb99bd95c_Cfunc_sum
- //go:linkname __cgofn__cgo_53efb99bd95c_Cfunc_sum _cgo_53efb99bd95c_Cfunc_sum
- var __cgofn__cgo_53efb99bd95c_Cfunc_sum byte // 指向C空間的sum函
- var _cgo_53efb99bd95c_Cfunc_sum = unsafe.Pointer(&__cgofn__cgo_53efb99bd95c_Cfunc_sum) // 將sum函數(shù)指針賦值給_cgo_53efb99bd95c_Cfunc_sum
- //go:cgo_unsafe_args
- func _Cfunc_sum(p0 _Ctype_int, p1 _Ctype_int) (r1 _Ctype_int) {
- _cgo_runtime_cgocall(_cgo_53efb99bd95c_Cfunc_sum, uintptr(unsafe.Pointer(&p0))) // 將參數(shù)塞到列表中,調(diào)用C函數(shù)
- if _Cgo_always_false {
- _Cgo_use(p0) // 針對編譯器的優(yōu)化操作,為了將C函數(shù)的參數(shù)分配在堆上,實(shí)際永遠(yuǎn)不會(huì)執(zhí)行
- _Cgo_use(p1)
- }
- return
- }
_cgo_gotypes.go 是 Go 調(diào) C 的精髓,這里逐段分析。
_Cgo_always_false & _Cgo_use
- //go:linkname _Cgo_always_false runtime.cgoAlwaysFalse
- var _Cgo_always_false bool // 永遠(yuǎn)為 false
- //go:linkname _Cgo_use runtime.cgoUse
- func _Cgo_use(interface{}) // 返回一個(gè) Error
- ..........
- if _Cgo_always_false {
- _Cgo_use(p0) // 針對編譯器的優(yōu)化操作,為了將C函數(shù)的參數(shù)分配在堆上,實(shí)際永遠(yuǎn)不會(huì)執(zhí)行
- _Cgo_use(p1)
- }
_Cgo_always_false 是一個(gè)"常量",正常情況下永遠(yuǎn)為 false。
_Cgo_use的函數(shù)實(shí)現(xiàn)如下
- // runtime/cgo.go
- func cgoUse(interface{}) { throw("cgoUse should not be called") }
Go 中變量可以分配在棧或者堆上。棧中變量的地址會(huì)隨著 go 程調(diào)度,發(fā)生變化。堆中變量則不會(huì)。
而程序進(jìn)入到 C 空間后,會(huì)脫離 Go 程的調(diào)度機(jī)制,所以必須保證 C 函數(shù)的參數(shù)分配在堆上。
Go 通過在編譯器里做逃逸分析來決定一個(gè)對象放棧上還是放堆上,不逃逸的對象放棧上,可能逃逸的放堆上。
由于棧上內(nèi)存存在不需要 gc,內(nèi)存碎片少,分配速度快等優(yōu)點(diǎn),所以 Go 會(huì)將變量更多的放在棧上。
_Cgo_use以 interface 類型為入?yún)?,編譯器很難在編譯期知道,變量最后會(huì)是什么類型,因此它的參數(shù)都會(huì)被分配在堆上。
_cgo_runtime_cgocall
- //go:linkname _cgo_runtime_cgocall runtime.cgocall
- func _cgo_runtime_cgocall(unsafe.Pointer, uintptr) int32 // Go調(diào)C的入口函數(shù)
_cgo_runtime_cgocall是從 Go 調(diào) C 的關(guān)鍵函數(shù),這個(gè)函數(shù)里面做了一些調(diào)度相關(guān)的安排。
- // Call from Go to C.
- //
- // This must be nosplit because it's used for syscalls on some
- // platforms. Syscalls may have untyped arguments on the stack, so
- // it's not safe to grow or scan the stack.
- //
- //go:nosplit
- func cgocall(fn, arg unsafe.Pointer) int32 {
- if !iscgo && GOOS != "solaris" && GOOS != "illumos" && GOOS != "windows" {
- throw("cgocall unavailable")
- }
- if fn == nil {
- throw("cgocall nil")
- }
- if raceenabled { // 數(shù)據(jù)競爭檢測,與CGO無瓜
- racereleasemerge(unsafe.Pointer(&racecgosync))
- }
- mp := getg().m
- mp.ncgocall++ // 統(tǒng)計(jì) M 調(diào)用CGO次數(shù)
- mp.ncgo++ // 周期內(nèi)調(diào)用次數(shù)
- // Reset traceback.
- mp.cgoCallers[0] = 0
標(biāo)題名稱:Go與C的橋梁:CGO入門剖析與實(shí)踐
文章鏈接:http://www.5511xx.com/article/cohpoch.html


咨詢
建站咨詢
