Go 部落格

C? Go? Cgo!

Andrew Gerrand
2011 年 3 月 17 日

引言

Cgo 允許 Go 包呼叫 C 程式碼。給定一個帶有特殊功能的 Go 原始檔,cgo 會輸出 Go 和 C 檔案,這些檔案可以組合成一個 Go 包。

為了以一個例子開頭,這裡有一個 Go 包,它提供了兩個函式——RandomSeed——它們封裝了 C 的 randomsrandom 函式。

package rand

/*
#include <stdlib.h>
*/
import "C"

func Random() int {
    return int(C.random())
}

func Seed(i int) {
    C.srandom(C.uint(i))
}

讓我們看看這裡發生了什麼,從 import 語句開始。

rand 包匯入了 "C",但你會發現標準 Go 庫中沒有這樣一個包。這是因為 C 是一個“偽包”,一個由 cgo 解釋為對 C 名稱空間的引用的特殊名稱。

rand 包包含對 C 包的四處引用:呼叫 C.randomC.srandom,轉換 C.uint(i),以及 import 語句。

Random 函式呼叫標準 C 庫的 random 函式並返回結果。在 C 中,random 返回 C 型別 long 的值,cgo 將其表示為 C.long 型別。在使用此包之外的 Go 程式碼之前,它必須使用普通的 Go 型別轉換轉換為 Go 型別

func Random() int {
    return int(C.random())
}

這裡有一個使用臨時變數更明確地說明型別轉換的等效函式

func Random() int {
    var r C.long = C.random()
    return int(r)
}

Seed 函式在某種程度上做了相反的事情。它接受一個普通的 Go int,將其轉換為 C unsigned int 型別,並將其傳遞給 C 函式 srandom

func Seed(i int) {
    C.srandom(C.uint(i))
}

注意 cgo 將 unsigned int 型別識別為 C.uint;有關這些數值型別名稱的完整列表,請參閱cgo 文件

這個例子中我們還沒有考察的一個細節是 import 語句上方的註釋。

/*
#include <stdlib.h>
*/
import "C"

Cgo 能夠識別此註釋。任何以 #cgo 後跟一個空格字元開頭的行都會被刪除;這些行會成為 cgo 的指令。其餘行在編譯包的 C 部分時用作標頭檔案。在本例中,這些行只是一個簡單的 #include 語句,但它們可以是幾乎任何 C 程式碼。#cgo 指令用於在構建包的 C 部分時為編譯器和連結器提供標誌。

有一個限制:如果你的程式使用了任何 //export 指令,那麼註釋中的 C 程式碼只能包含宣告(extern int f();),不能包含定義(int f() { return 1; })。你可以使用 //export 指令使 Go 函式可以被 C 程式碼訪問。

#cgo//export 指令在cgo 文件中有詳細說明。

字串等

與 Go 不同,C 沒有顯式的字串型別。C 中的字串由以零結尾的字元陣列表示。

Go 字串和 C 字串之間的轉換使用 C.CStringC.GoStringC.GoStringN 函式完成。這些轉換會複製字串資料。

下一個例子實現了一個 Print 函式,它使用 C stdio 庫中的 fputs 函式將字串寫入標準輸出

package print

// #include <stdio.h>
// #include <stdlib.h>
import "C"
import "unsafe"

func Print(s string) {
    cs := C.CString(s)
    C.fputs(cs, (*C.FILE)(C.stdout))
    C.free(unsafe.Pointer(cs))
}

C 程式碼進行的記憶體分配對於 Go 的記憶體管理器來說是未知的。當你使用 C.CString(或任何 C 記憶體分配)建立一個 C 字串時,你必須記住在使用完畢後透過呼叫 C.free 來釋放記憶體。

呼叫 C.CString 會返回一個指向字元陣列開頭的指標,因此在函式退出之前,我們將其轉換為 unsafe.Pointer 並使用 C.free 釋放記憶體分配。cgo 程式中一個常見的習慣用法是在分配後立即 defer 釋放(尤其是在後續程式碼比單個函式呼叫更復雜時),就像 Print 的這個重寫版本一樣

func Print(s string) {
    cs := C.CString(s)
    defer C.free(unsafe.Pointer(cs))
    C.fputs(cs, (*C.FILE)(C.stdout))
}

構建 cgo 包

要構建 cgo 包,只需像往常一樣使用 go buildgo install。go 工具會識別特殊的 "C" 匯入並自動對這些檔案使用 cgo。

更多 cgo 資源

cgo 命令文件提供了關於 C 偽包和構建過程的更多細節。Go 原始碼樹中的cgo 示例展示了更高階的概念。

最後,如果你對這一切的內部工作原理感到好奇,可以看看執行時包的 cgocall.go 檔案開頭的註釋。

下一篇文章:Gobs of data
上一篇文章:Go 變得更穩定
部落格索引