Go有強(qiáng)烈的C背景,除了語(yǔ)法具有繼承性外,其設(shè)計(jì)者以及其設(shè)計(jì)目標(biāo)都與C語(yǔ)言有著千絲萬(wàn)縷的聯(lián)系。在Go與C語(yǔ)言互操作(Interoperability)方面,Go更是提供了強(qiáng)大的支持。尤其是在Go中使用C,你甚至可以直接在Go源文件中編寫(xiě)C代碼,這是其他語(yǔ)言所無(wú)法望其項(xiàng)背的。 在如下一些場(chǎng)景中,可能會(huì)涉及到Go與C的互操作: 1、提升局部代碼性能時(shí),用C替換一些Go代碼。C之于Go,好比匯編之于C。 2、嫌Go內(nèi)存GC性能不足,自己手動(dòng)管理應(yīng)用內(nèi)存。 3、實(shí)現(xiàn)一些庫(kù)的Go Wrapper。比如Oracle提供的C版本OCI,但Oracle并未提供Go版本的以及連接DB的協(xié)議細(xì)節(jié),因此只能通過(guò)包裝C OCI版本的方式以提供Go開(kāi)發(fā)者使用。 4、Go導(dǎo)出函數(shù)供C開(kāi)發(fā)者使用(目前這種需求應(yīng)該很少見(jiàn))。 5、Maybe more… 一、Go調(diào)用C代碼的原理 下面是一個(gè)短小的例子: package main // #include <stdio.h> // #include <stdlib.h> /* void print(char *str) { printf("%s\n", str); } */ import "C" import "unsafe" func main() { s := "Hello Cgo" cs := C.CString(s) C.print(cs) C.free(unsafe.Pointer(cs)) } 與"正常"Go代碼相比,上述代碼有幾處"特殊"的地方: 1) 在開(kāi)頭的注釋中出現(xiàn)了C頭文件的include字樣 2) 在注釋中定義了C函數(shù)print 3) import的一個(gè)名為C的"包" 4) 在main函數(shù)中居然調(diào)用了上述的那個(gè)C函數(shù)-print 沒(méi)錯(cuò),這就是在Go源碼中調(diào)用C代碼的步驟,可以看出我們可直接在Go源碼文件中編寫(xiě)C代碼。 首先,Go源碼文件中的C代碼是需要用注釋包裹的,就像上面的include 頭文件以及print函數(shù)定義; 其次,import "C"這個(gè)語(yǔ)句是必須的,而且其與上面的C代碼之間不能用空行分隔,必須緊密相連。這里的"C"不是包名,而是一種類似名字空間的概念,或可以理解為偽包,C語(yǔ)言所有語(yǔ)法元素均在該偽包下面; 最后,訪問(wèn)C語(yǔ)法元素時(shí)都要在其前面加上偽包前綴,比如C.uint和上面代碼中的C.print、C.free等。 我們?nèi)绾蝸?lái)編譯這個(gè)go源文件呢?其實(shí)與"正常"Go源文件沒(méi)啥區(qū)別,依舊可以直接通過(guò)go build或go run來(lái)編譯和執(zhí)行。但實(shí)際編譯過(guò)程中,go調(diào)用了名為cgo的工具,cgo會(huì)識(shí)別和讀取Go源文件中的C元素,并將其提取后交給C編譯器編譯,最后與Go源碼編譯后的目標(biāo)文件鏈接成一個(gè)可執(zhí)行程序。這樣我們就不難理解為何Go源文件中的C代碼要用注釋包裹了,這些特殊的語(yǔ)法都是可以被Cgo識(shí)別并使用的。 二、在Go中使用C語(yǔ)言的類型 1、原生類型 * 數(shù)值類型 在Go中可以用如下方式訪問(wèn)C原生的數(shù)值類型: C.char, C.schar (signed char), C.uchar (unsigned char), C.short, C.ushort (unsigned short), C.int, C.uint (unsigned int), C.long, C.ulong (unsigned long), C.longlong (long long), C.ulonglong (unsigned long long), C.float, C.double Go的數(shù)值類型與C中的數(shù)值類型不是一一對(duì)應(yīng)的。因此在使用對(duì)方類型變量時(shí)少不了顯式轉(zhuǎn)型操作,如Go doc中的這個(gè)例子: func Random() int { return int(C.random())//C.long -> Go的int } func Seed(i int) { C.srandom(C.uint(i))//Go的uint -> C的uint } * 指針類型 原生數(shù)值類型的指針類型可按Go語(yǔ)法在類型前面加上*,比如var p *C.int。而void*比較特殊,用Go中的unsafe.Pointer表示。任何類型的指針值都可以轉(zhuǎn)換為unsafe.Pointer類型,而unsafe.Pointer類型值也可以轉(zhuǎn)換為任意類型的指針值。unsafe.Pointer還可以與uintptr這個(gè)類型做相互轉(zhuǎn)換。由于unsafe.Pointer的指針類型無(wú)法做算術(shù)操作,轉(zhuǎn)換為uintptr后可進(jìn)行算術(shù)操作。 * 字符串類型 C語(yǔ)言中并不存在正規(guī)的字符串類型,在C中用帶結(jié)尾'\0'的字符數(shù)組來(lái)表示字符串;而在Go中,string類型是原生類型,因此在兩種語(yǔ)言互操作是勢(shì)必要做字符串類型的轉(zhuǎn)換。 通過(guò)C.CString函數(shù),我們可以將Go的string類型轉(zhuǎn)換為C的"字符串"類型,再傳給C函數(shù)使用。就如我們?cè)诒疚拈_(kāi)篇例子中使用的那樣: s := "Hello Cgo\n" cs := C.CString(s) C.print(cs) 不過(guò)這樣轉(zhuǎn)型后所得到的C字符串cs并不能由Go的gc所管理,我們必須手動(dòng)釋放cs所占用的內(nèi)存,這就是為何例子中最后調(diào)用C.free釋放掉cs的原因。在C內(nèi)部分配的內(nèi)存,Go中的GC是無(wú)法感知到的,因此要記著釋放。 通過(guò)C.GoString可將C的字符串(*C.char)轉(zhuǎn)換為Go的string類型,例如: // #include <stdio.h> // #include <stdlib.h> // char *foo = "hellofoo"; import "C" import "fmt" func main() { … … fmt.Printf("%s\n", C.GoString(C.foo)) } * 數(shù)組類型 C語(yǔ)言中的數(shù)組與Go語(yǔ)言中的數(shù)組差異較大,后者是值類型,而前者與C中的指針大部分場(chǎng)合都可以隨意轉(zhuǎn)換。目前似乎無(wú)法直接顯式的在兩者之間進(jìn)行轉(zhuǎn)型,官方文檔也沒(méi)有說(shuō)明。但我們可以通過(guò)編寫(xiě)轉(zhuǎn)換函數(shù),將C的數(shù)組轉(zhuǎn)換為Go的Slice(由于Go中數(shù)組是值類型,其大小是靜態(tài)的,轉(zhuǎn)換為Slice更為通用一些),下面是一個(gè)整型數(shù)組轉(zhuǎn)換的例子: // int cArray[] = {1, 2, 3, 4, 5, 6, 7}; func CArrayToGoArray(cArray unsafe.Pointer, size int) (goArray []int) { p := uintptr(cArray) for i :=0; i < size; i++ { j := *(*int)(unsafe.Pointer(p)) goArray = append(goArray, j) p += unsafe.Sizeof(j) } return } func main() { … … goArray := CArrayToGoArray(unsafe.Pointer(&C.cArray[0]), 7) fmt.Println(goArray) } 執(zhí)行結(jié)果輸出:[1 2 3 4 5 6 7] 這里要注意的是:Go編譯器并不能將C的cArray自動(dòng)轉(zhuǎn)換為數(shù)組的地址,所以不能像在C中使用數(shù)組那樣將數(shù)組變量直接傳遞給函數(shù),而是將數(shù)組第一個(gè)元素的地址傳遞給函數(shù)。 2、自定義類型 除了原生類型外,我們還可以訪問(wèn)C中的自定義類型。 * 枚舉(enum) // enum color { // RED, // BLUE, // YELLOW // }; var e, f, g C.enum_color = C.RED, C.BLUE, C.YELLOW fmt.Println(e, f, g) 輸出:0 1 2 對(duì)于具名的C枚舉類型,我們可以通過(guò)C.enum_xx來(lái)訪問(wèn)該類型。如果是匿名枚舉,則似乎只能訪問(wèn)其字段了。 * 結(jié)構(gòu)體(struct) // struct employee { // char *id; // int age; // }; id := C.CString("1247") var employee C.struct_employee = C.struct_employee{id, 21} fmt.Println(C.GoString(employee.id)) fmt.Println(employee.age) C.free(unsafe.Pointer(id)) 輸出: 1247 21 和enum類似,我們可以通過(guò)C.struct_xx來(lái)訪問(wèn)C中定義的結(jié)構(gòu)體類型。 * 聯(lián)合體(union) 這里我試圖用與訪問(wèn)struct相同的方法來(lái)訪問(wèn)一個(gè)C的union: // #include <stdio.h> // union bar { // char c; // int i; // double d; // }; import "C" func main() { var b *C.union_bar = new(C.union_bar) b.c = 4 fmt.Println(b) } 不過(guò)編譯時(shí),go卻報(bào)錯(cuò):b.c undefined (type *[8]byte has no field or method c)。從報(bào)錯(cuò)的信息來(lái)看,Go對(duì)待union與其他類型不同,似乎將union當(dāng)成[N]byte來(lái)對(duì)待,其中N為union中最大字段的size(圓整后的),因此我們可以按如下方式處理C.union_bar: func main() { var b *C.union_bar = new(C.union_bar) b[0] = 13 b[1] = 17 fmt.Println(b) } 輸出:&[13 17 0 0 0 0 0 0] * typedef 在Go中訪問(wèn)使用用typedef定義的別名類型時(shí),其訪問(wèn)方式與原實(shí)際類型訪問(wèn)方式相同。如: // typedef int myint; var a C.myint = 5 fmt.Println(a) // typedef struct employee myemployee; var m C.struct_myemployee 從例子中可以看出,對(duì)原生類型的別名,直接訪問(wèn)這個(gè)新類型名即可。而對(duì)于復(fù)合類型的別名,需要根據(jù)原復(fù)合類型的訪問(wèn)方式對(duì)新別名進(jìn)行訪問(wèn),比如myemployee實(shí)際類型為struct,那么使用myemployee時(shí)也要加上struct_前綴。 三、Go中訪問(wèn)C的變量和函數(shù) 實(shí)際上上面的例子中我們已經(jīng)演示了在Go中是如何訪問(wèn)C的變量和函數(shù)的,一般方法就是加上C前綴即可,對(duì)于C標(biāo)準(zhǔn)庫(kù)中的函數(shù)尤其是這樣。不過(guò)雖然我們可以在Go源碼文件中直接定義C變量和C函數(shù),但從代碼結(jié)構(gòu)上來(lái)講,大量的在Go源碼中編寫(xiě)C代碼似乎不是那么“專業(yè)”。那如何將C函數(shù)和變量定義從Go源碼中分離出去單獨(dú)定義呢?我們很容易想到將C的代碼以共享庫(kù)的形式提供給Go源碼。 Cgo提供了#cgo指示符可以指定Go源碼在編譯后與哪些共享庫(kù)進(jìn)行鏈接。我們來(lái)看一下例子: package main // #cgo LDFLAGS: -L ./ -lfoo // #include <stdio.h> // #include <stdlib.h> // #include "foo.h" import "C" import "fmt“ func main() { fmt.Println(C.count) C.foo() } 我們看到上面例子中通過(guò)#cgo指示符告訴go編譯器鏈接當(dāng)前目錄下的libfoo共享庫(kù)。C.count變量和C.foo函數(shù)的定義都在libfoo共享庫(kù)中。我們來(lái)創(chuàng)建這個(gè)共享庫(kù): // foo.h int count; void foo(); //foo.c #include "foo.h" int count = 6; void foo() { printf("I am foo!\n"); } $> gcc -c foo.c $> ar rv libfoo.a foo.o 我們首先創(chuàng)建一個(gè)靜態(tài)共享庫(kù)libfoo.a,不過(guò)在編譯Go源文件時(shí)我們遇到了問(wèn)題: $> go build foo.go # command-line-arguments /tmp/go-build565913544/command-line-arguments.a(foo.cgo2.)(.text): foo: not defined foo(0): not defined 提示foo函數(shù)未定義。通過(guò)-x選項(xiàng)打印出具體的編譯細(xì)節(jié),也未找出問(wèn)題所在。不過(guò)在Go的問(wèn)題列表中我發(fā)現(xiàn)了一個(gè)issue(http://code.google.com/p/go/issues/detail?id=3755),上面提到了目前Go的版本不支持鏈接靜態(tài)共享庫(kù)。 那我們來(lái)創(chuàng)建一個(gè)動(dòng)態(tài)共享庫(kù)試試: $> gcc -c foo.c $> gcc -shared -Wl,-soname,libfoo.so -o libfoo.so foo.o 再編譯foo.go,的確能夠成功。執(zhí)行foo。 $> go build foo.go && go 6 I am foo! 還有一點(diǎn)值得注意,那就是Go支持多返回值,而C中并沒(méi)不支持。因此當(dāng)將C函數(shù)用在多返回值的調(diào)用中時(shí),C的errno將作為err返回值返回,下面是個(gè)例子: package main // #include <stdlib.h> // #include <stdio.h> // #include <errno.h> // int foo(int i) { // errno = 0; // if (i > 5) { // errno = 8; // return i – 5; // } else { // return i; // } //} import "C" import "fmt" func main() { i, err := C.foo(C.int(8)) if err != nil { fmt.Println(err) } else { fmt.Println(i) } } $> go run foo.go exec format error errno為8,其含義在errno.h中可以找到: #define ENOEXEC 8 /* Exec format error */ 的確是“exec format error”。 四、C中使用Go函數(shù) 與在Go中使用C源碼相比,在C中使用Go函數(shù)的場(chǎng)合較少。在Go中,可以使用"export + 函數(shù)名"來(lái)導(dǎo)出Go函數(shù)為C所使用,看一個(gè)簡(jiǎn)單例子: package main /* #include <stdio.h> extern void GoExportedFunc(); void bar() { printf("I am bar!\n"); GoExportedFunc(); } */ import "C" import "fmt" //export GoExportedFunc func GoExportedFunc() { fmt.Println("I am a GoExportedFunc!") } func main() { C.bar() } 不過(guò)當(dāng)我們編譯該Go文件時(shí),我們得到了如下錯(cuò)誤信息: # command-line-arguments /tmp/go-build163255970/command-line-arguments/_obj/bar.cgo2.o: In function `bar': ./bar.go:7: multiple definition of `bar' /tmp/go-build163255970/command-line-arguments/_obj/_cgo_export.o:/home/tonybai/test/go/bar.go:7: first defined here collect2: ld returned 1 exit status 代碼似乎沒(méi)有任何問(wèn)題,但就是無(wú)法通過(guò)編譯,總是提示“多重定義”。翻看Cgo的文檔,找到了些端倪。原來(lái) There is a limitation: if your program uses any //export directives, then the C code in the comment may only include declarations (extern int f();), not definitions (int f() { return 1; }). 似乎是// extern int f()與//export f不能放在一個(gè)Go源文件中。我們把bar.go拆分成bar1.go和bar2.go兩個(gè)文件: // bar1.go package main /* #include <stdio.h> extern void GoExportedFunc(); void bar() { printf("I am bar!\n"); GoExportedFunc(); } */ import "C" func main() { C.bar() } // bar2.go package main import "C" import "fmt" //export GoExportedFunc func GoExportedFunc() { fmt.Println("I am a GoExportedFunc!") } 編譯執(zhí)行: $> go build -o bar bar1.go bar2.go $> bar I am bar! I am a GoExportedFunc! 個(gè)人覺(jué)得目前Go對(duì)于導(dǎo)出函數(shù)供C使用的功能還十分有限,兩種語(yǔ)言的調(diào)用約定不同,類型無(wú)法一一對(duì)應(yīng)以及Go中類似Gc這樣的高級(jí)功能讓導(dǎo)出Go函數(shù)這一功能難于完美實(shí)現(xiàn),導(dǎo)出的函數(shù)依舊無(wú)法完全脫離Go的環(huán)境,因此實(shí)用性似乎有折扣。 五、其他 雖然Go提供了強(qiáng)大的與C互操作的功能,但目前依舊不完善,比如不支持在Go中直接調(diào)用可變個(gè)數(shù)參數(shù)的函數(shù)(issue975),如printf(因此,文檔中多用fputs)。 這里的建議是:盡量縮小Go與C間互操作范圍。 什么意思呢?如果你在Go中使用C代碼時(shí),那么盡量在C代碼中調(diào)用C函數(shù)。Go只使用你封裝好的一個(gè)C函數(shù)最好。不要像下面代碼這樣: C.fputs(…) C.atoi(..) C.malloc(..) 而是將這些C函數(shù)調(diào)用封裝到一個(gè)C函數(shù)中,Go只知道這個(gè)C函數(shù)即可。 C.foo(..) 相反,在C中使用Go導(dǎo)出的函數(shù)也是一樣。
2012, bigwhite. 版權(quán)所有. Related posts:
|
|