標籤:HERE puts poi 算術 支援 ongl 折扣 library 種類
Go有強烈的C背景,除了文法具有繼承性外,其設計者以及其設計目標都與C語言有著千絲萬縷的聯絡。在Go與C語言互操作(Interoperability)方面,Go更是提供了強大的支援。尤其是在Go中使用C,你甚至可以直接在Go源檔案中編寫C代碼,這是其他語言所無法望其項背的。
在如下一些情境中,可能會涉及到Go與C的互操作: 1、提升局部代碼效能時,用C替換一些Go代碼。C之於Go,好比彙編之於C。2、嫌Go記憶體GC效能不足,自己手動管理應用記憶體。3、實現一些庫的Go Wrapper。比如Oracle提供的C版本OCI,但Oracle並未提供Go版本的以及串連DB的協議細節,因此只能通過封裝C OCI版本的方式以提供Go開發人員使用。4、Go匯出函數供C開發人員使用(目前這種需求應該很少見)。5、Maybe more…
一、Go調用C代碼的原理 下面是一個短小的例子:
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) 在開頭的注釋中出現了C標頭檔的include字樣2) 在注釋中定義了C函數print3) import的一個名為C的"包"4) 在main函數中居然調用了上述的那個C函數-print 沒錯,這就是在Go源碼中調用C代碼的步驟,可以看出我們可直接在Go源碼檔案中編寫C代碼。 首先,Go源碼檔案中的C代碼是需要用注釋包裹的,就像上面的include 標頭檔以及print函數定義;其次,import "C"這個語句是必須的,而且其與上面的C代碼之間不能用空行分隔,必須緊密相連。這裡的"C"不是包名,而是一種類似名字空間的概念,或可以理解為偽包,C語言所有文法元素均在該偽包下面;最後,訪問C文法元素時都要在其前面加上偽包首碼,比如C.uint和上面代碼中的C.print、C.free等。 我們如何來編譯這個go源檔案呢?其實與"正常"Go源檔案沒啥區別,依舊可以直接通過go build或go run來編譯和執行。但實際編譯過程中,go調用了名為cgo的工具,cgo會識別和讀取Go源檔案中的C元素,並將其提取後交給C編譯器編譯,最後與Go源碼編譯後的目標檔案連結成一個可執行程式。這樣我們就不難理解為何Go源檔案中的C代碼要用注釋包裹了,這些特殊的文法都是可以被Cgo識別並使用的。
二、在Go中使用C語言的類型 1、原生類型 * 數實值型別在Go中可以用如下方式訪問C原生的數實值型別:
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的數實值型別與C中的數實值型別不是一一對應的。因此在使用對方類型變數時少不了顯式轉型操作,如Go doc中的這個例子:
func Random() int { return int(C.random())//C.long -> Go的int} func Seed(i int) { C.srandom(C.uint(i))//Go的uint -> C的uint}
* 指標類型原生數實值型別的指標類型可按Go文法在類型前面加上*,比如var p *C.int。而void*比較特殊,用Go中的unsafe.Pointer表示。任何類型的指標值都可以轉換為unsafe.Pointer類型,而unsafe.Pointer類型值也可以轉換為任意類型的指標值。unsafe.Pointer還可以與uintptr這個類型做相互轉換。由於unsafe.Pointer的指標類型無法做算術操作,轉換為uintptr後可進行算術操作。 * 字串類型C語言中並不存在正規的字串類型,在C中用帶結尾‘\0‘的字元數組來表示字串;而在Go中,string類型是原生類型,因此在兩種語言互操作是勢必要做字串類型的轉換。 通過C.CString函數,我們可以將Go的string類型轉換為C的"字串"類型,再傳給C函數使用。就如我們在本文開篇例子中使用的那樣:
s := "Hello Cgo\n"cs := C.CString(s)C.print(cs)
不過這樣轉型後所得到的C字串cs並不能由Go的gc所管理,我們必須手動釋放cs所佔用的記憶體,這就是為何例子中最後調用C.free釋放掉cs的原因。在C內部分配的記憶體,Go中的GC是無法感知到的,因此要記著釋放。 通過C.GoString可將C的字串(*C.char)轉換為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))}
* 數群組類型C語言中的數組與Go語言中的數組差異較大,後者是實值型別,而前者與C中的指標大部分場合都可以隨意轉換。目前似乎無法直接顯式的在兩者之間進行轉型,官方文檔也沒有說明。但我們可以通過編寫轉換函式,將C的數群組轉換為Go的Slice(由於Go中數組是實值型別,其大小是靜態,轉換為Slice更為通用一些),下面是一個整型數群組轉換的例子:
// 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)}
執行結果輸出:[1 2 3 4 5 6 7] 這裡要注意的是:Go編譯器並不能將C的cArray自動轉換為數組的地址,所以不能像在C中使用數組那樣將陣列變數直接傳遞給函數,而是將數組第一個元素的地址傳遞給函數。 2、自訂類型 除了原生類型外,我們還可以訪問C中的自訂類型。 * 枚舉(enum)
// enum color {// RED,// BLUE,// YELLOW// }; var e, f, g C.enum_color = C.RED, C.BLUE, C.YELLOWfmt.Println(e, f, g)
輸出:0 1 2 對於具名的C枚舉類型,我們可以通過C.enum_xx來訪問該類型。如果是匿名枚舉,則似乎只能訪問其欄位了。 * 結構體(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))
輸出:124721 和enum類似,我們可以通過C.struct_xx來訪問C中定義的結構體類型。 * 聯合體(union) 這裡我試圖用與訪問struct相同的方法來訪問一個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)}
不過編譯時間,go卻報錯:b.c undefined (type *[8]byte has no field or method c)。從報錯的資訊來看,Go對待union與其他類型不同,似乎將union當成[N]byte來對待,其中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中訪問使用用typedef定義的別名資料型別時,其訪問方式與原實際類型訪問方式相同。如:
// typedef int myint; var a C.myint = 5fmt.Println(a) // typedef struct employee myemployee; var m C.struct_myemployee
從例子中可以看出,對原生類型的別名,直接存取這個新類型名即可。而對於複合類型的別名,需要根據原複合類型的訪問方式對新別名進行訪問,比如myemployee實際類型為struct,那麼使用myemployee時也要加上struct_首碼。
三、Go中訪問C的變數和函數 實際上上面的例子中我們已經示範了在Go中是如何訪問C的變數和函數的,一般方法就是加上C首碼即可,對於C標準庫中的函數尤其是這樣。不過雖然我們可以在Go源碼檔案中直接定義C變數和C函數,但從代碼結構上來講,大量的在Go源碼中編寫C代碼似乎不是那麼“專業”。那如何將C函數和變數定義從Go源碼中分離出去單獨定義呢?我們很容易想到將C的代碼以共用庫的形式提供給Go源碼。 Cgo提供了#cgo指示符可以指定Go源碼在編譯後與哪些共用庫進行連結。我們來看一下例子:
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()}
我們看到上面例子中通過#cgo指示符告訴go編譯器連結目前的目錄下的libfoo共用庫。C.count變數和C.foo函數的定義都在libfoo共用庫中。我們來建立這個共用庫: // 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 我們首先建立一個靜態共用庫libfoo.a,不過在編譯Go源檔案時我們遇到了問題: $> go build foo.go# command-line-arguments/tmp/go-build565913544/command-line-arguments.a(foo.cgo2.)(.text): foo: not definedfoo(0): not defined 提示foo函數未定義。通過-x選項列印出具體的編譯細節,也未找出問題所在。不過在Go的問題列表中我發現了一個issue(http://code.google.com/p/go/issues/detail?id=3755),上面提到了目前Go的版本不支援連結靜態共用庫。 那我們來建立一個動態共用程式庫試試: $> gcc -c foo.c$> gcc -shared -Wl,-soname,libfoo.so -o libfoo.so foo.o 再編譯foo.go,的確能夠成功。執行foo。 $> go build foo.go && go6I am foo! 還有一點值得注意,那就是Go支援多傳回值,而C中並沒不支援。因此當將C函數用在多傳回值的調用中時,C的errno將作為err傳回值返回,下面是個例子:
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.goexec format error errno為8,其含義在errno.h中可以找到: #define ENOEXEC 8 /* Exec format error */ 的確是“exec format error”。
四、C中使用Go函數 與在Go中使用C源碼相比,在C中使用Go函數的場合較少。在Go中,可以使用"export + 函數名"來匯出Go函數為C所使用,看一個簡單例子:
package main /*#include <stdio.h> extern void GoExportedFunc(); void bar() { printf("I am bar!\n"); GoExportedFunc();}*/import "C" import "fmt" //export GoExportedFuncfunc GoExportedFunc() { fmt.Println("I am a GoExportedFunc!")} func main() { C.bar()}
不過當我們編譯該Go檔案時,我們得到了如下錯誤資訊: # 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 herecollect2: ld returned 1 exit status 代碼似乎沒有任何問題,但就是無法通過編譯,總是提示“多重定義”。翻看Cgo的文檔,找到了些端倪。原來
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不能放在一個Go源檔案中。我們把bar.go拆分成bar1.go和bar2.go兩個檔案: // 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 GoExportedFuncfunc GoExportedFunc() { fmt.Println("I am a GoExportedFunc!")}
編譯執行: $> go build -o bar bar1.go bar2.go$> barI am bar!I am a GoExportedFunc! 個人覺得目前Go對於匯出函數供C使用的功能還十分有限,兩種語言的呼叫慣例不同,類型無法一一對應以及Go中類似Gc這樣的進階功能讓匯出Go函數這一功能難於完美實現,匯出的函數依舊無法完全脫離Go的環境,因此實用性似乎有折扣。
五、其他 雖然Go提供了強大的與C互操作的功能,但目前依舊不完善,比如不支援在Go中直接調用可變個數參數的函數(issue975),如printf(因此,文檔中多用fputs)。 這裡的建議是:盡量縮小Go與C間互操作範圍。 什麼意思呢?如果你在Go中使用C代碼時,那麼盡量在C代碼中調用C函數。Go只使用你封裝好的一個C函數最好。不要像下面代碼這樣: C.fputs(…)C.atoi(..)C.malloc(..) 而是將這些C函數調用封裝到一個C函數中,Go只知道這個C函數即可。 C.foo(..) 相反,在C中使用Go匯出的函數也是一樣。
Go與C語言的互操作