[轉]Go與C語言的互操做

Go有強烈的C背景,除了語法具備繼承性外,其設計者以及其設計目標都與C語言有着千絲萬縷的聯繫。在Go與C語言互操做(Interoperability)方面,Go更是提供了強大的支持。尤爲是在Go中使用C,你甚至能夠直接在Go源文件中編寫C代碼,這是其餘語言所沒法望其項背的。golang

 
在以下一些場景中,可能會涉及到Go與C的互操做:
 
一、提高局部代碼性能時,用C替換一些Go代碼。C之於Go,比如彙編之於C。
二、嫌Go內存GC性能不足,本身手動管理應用內存。
三、實現一些庫的Go Wrapper。好比Oracle提供的C版本OCI,但Oracle並未提供Go版本的以及鏈接DB的協議細節,所以只能經過包裝C  OCI版本的方式以提供Go開發者使用。
四、Go導出函數供C開發者使用(目前這種需求應該不多見)。
五、Maybe more…
 
1、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函數print
3) 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識別並使用的。
 
2、在Go中使用C語言的類型
 
一、原生類型
 
* 數值類型
在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中使用數組那樣將數組變量直接傳遞給函數,而是將數組第一個元素的地址傳遞給函數。
 
二、自定義類型
 
除了原生類型外,咱們還能夠訪問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
 
對於具名的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))
 
輸出:
1247
21
 
和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 = 5
fmt.Println(a)
 
// typedef struct employee myemployee;
 
var m C.struct_myemployee
 
從例子中能夠看出,對原生類型的別名,直接訪問這個新類型名便可。而對於複合類型的別名,須要根據原複合類型的訪問方式對新別名進行訪問,好比myemployee實際類型爲struct,那麼使用myemployee時也要加上struct_前綴。
 
3、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 defined
foo(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 && go
6
I 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.go
exec format error
 
errno爲8,其含義在errno.h中能夠找到:
 
#define ENOEXEC      8  /* Exec format error */
 
的確是「exec format error」。
 
4、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 GoExportedFunc
func 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 here
collect2: 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 GoExportedFunc
func GoExportedFunc() {
        fmt.Println("I am a GoExportedFunc!")
}
 
編譯執行:
 
$> go build -o bar bar1.go bar2.go
$> bar
I am bar!
I am a GoExportedFunc!
 
我的以爲目前Go對於導出函數供C使用的功能還十分有限,兩種語言的調用約定不一樣,類型沒法一一對應以及Go中相似Gc這樣的高級功能讓導出Go函數這一功能難於完美實現,導出的函數依舊沒法徹底脫離Go的環境,所以實用性彷佛有折扣。
 
5、其餘
 
雖然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導出的函數也是同樣。
相關文章
相關標籤/搜索