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))
}

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

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 資源

有關 C 偽包和構建過程的更多詳細資訊,請參閱 cgo 命令文件。Go 樹中的 cgo 示例演示了更高階的概念。

最後,如果你好奇所有這些是如何在內部工作的,可以看看執行時包的 cgocall.go 的介紹性註釋。

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