鍍金池/ 教程/ GO/ go tool cgo
go install
go clean
go list
go test
go doc與godoc
go build
go fix與go tool fix
go tool pprof
go run
go env
go tool cgo
標(biāo)準(zhǔn)命令詳解
go get
go vet與go tool vet

go tool cgo

cgo也是一個Go語言自帶的特殊工具。一般情況下,我們使用命令go tool cgo來運行它。這個工具可以使我們創(chuàng)建能夠調(diào)用C語言代碼的Go語言源碼文件。這使得我們可以使用Go語言代碼去封裝一些C語言的代碼庫,并提供給Go語言代碼或項目使用。

在執(zhí)行go tool cgo命令的時候,我們需要加入作為目標(biāo)的Go語言源碼文件的路徑。這個路徑可以是絕對路徑也可以是相對路徑。但是,作者強烈建議在目標(biāo)源碼文件所屬的代碼包目錄下執(zhí)行go tool cgo命令并以目標(biāo)源碼文件的名字作為參數(shù)。因為,go tool cgo命令會在當(dāng)前目錄(也就是我們執(zhí)行go tool cgo命令的目錄)中生成一個名為_obj的子目錄。該目錄下會包含一些Go源碼文件和C源碼文件。這個子目錄及其包含的文件理應(yīng)被保存在目標(biāo)代碼包目錄之下。至于原因,我們稍后再做解釋。

我們現(xiàn)在來看可以作為go tool cgo命令參數(shù)的Go語言源碼文件。這個源碼文件必須要包含一行只針對于代碼包C的導(dǎo)入語句。其實,Go語言標(biāo)準(zhǔn)庫中并不存在代碼包C。代碼包C是一個偽造的代碼包。導(dǎo)入這個代碼包是為了告訴cgo工具在這個源碼文件中需要調(diào)用C代碼,同時也是給予cgo所產(chǎn)生的代碼一個專屬的命名空間。除此之外,我們還需要在這個代碼包導(dǎo)入語句之前加入一些注釋,并且在注釋行中寫出我們真正需要使用的C語言接口文件的名稱。像這樣:

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

在Go語言的規(guī)范中,把在代碼包C導(dǎo)入語句之前的若干注釋行叫做序文(preamble)。 在引入了C語言的標(biāo)準(zhǔn)代碼庫stdlib.h之后,我們就可以在后面的源碼中調(diào)用這個庫中的接口了。像這樣:

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

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

我們把上述的這些Go語言代碼寫入Go語言的庫源碼文件rand.go中,并將這個源碼文件保存在goc2項目的代碼包basic/cgo/lib的對應(yīng)目錄中。

在Go語言源碼文件rand.go中對代碼包C有四處引用,分別是三個函數(shù)調(diào)用語句C.rand、C.srandC.uint,以及一個導(dǎo)入語句import "C"。其中,在Go語言函數(shù)Random中調(diào)用了C語言標(biāo)準(zhǔn)庫代碼中的函數(shù)rand并返回了它的結(jié)果。但是,C語言的rand函數(shù)返回的結(jié)果的類型是C語言中的int類型。在cgo工具的環(huán)境中,C語言中的int類型與C.int相對應(yīng)。作為一個包裝C語言接口的函數(shù),我們必須將代碼包C的使用限制在當(dāng)前代碼包內(nèi)。也就是說,我們必須對當(dāng)前代碼包之外的Go代碼隱藏代碼包C。這樣做也是為了遵循代碼隔離原則。我們在設(shè)計接口或者接口適配程序的時候經(jīng)常會用到這種方法。因此,rand函數(shù)的結(jié)果的類型必須是Go語言的。所以,我們在這里使用函數(shù)int對C.int類型的C語言接口的結(jié)果進行了轉(zhuǎn)換。當(dāng)然,為了更清楚的表達,我們也可以將函數(shù)Random中的代碼return int(C.rand())拆分成兩行,像這樣:

var r C.int = C.rand()
return int(r)

而Go語言函數(shù)Seed則恰好相反。C語言標(biāo)準(zhǔn)代碼庫中的函數(shù)srand接收一個參數(shù),且這個參數(shù)的類型必須為C語言的uint類型,即C.uint。而Go語言函數(shù)Seed的參數(shù)為Go語言的int類型。為此,我們需要使用代碼包C的函數(shù)unit對其進行轉(zhuǎn)換。

實際上,標(biāo)準(zhǔn)C語言的數(shù)字類型都在cgo工具中有對應(yīng)的名稱,包括:C.char、C.schar(有符號字符類型)、C.uchar(無符號字符類型)、C.short、C.ushort(無符號短整數(shù)類型)、C.int、C.uint(無符號整數(shù)類型)、C.long、C.ulong(無符號長整數(shù)類型)、C.longlong(對應(yīng)于C語言的類型long long,它是在C語言的C99標(biāo)準(zhǔn)中定義的新整數(shù)類型)、C.ulonglong(無符號的long long類型)、C.float和C.double。另外,C語言類型void*對應(yīng)于Go語言的類型unsafe.Pointer。

如果想直接訪問C語言中的struct、union或enum類型的話,就需要在名稱前分別加入前綴struct_、union或enum。比如,我們需要在Go源碼文件中訪問C語言代碼中的名為command的struct類型的話,就需要這樣寫:C.structcommand。那么,如果我們想在Go語言代碼中訪問C語言類型struct中的字段需要怎樣做呢?解決方案是,同樣以C語言類型struct的實例名以及選擇符“.”作為前導(dǎo),但需要在字段的名稱前加入下劃線“”。例如,如果command1是名為command的C語言struct類型的實例名,并且這個類型中有一個名為name的字段,那么我們在Go語言代碼中訪問這個字段的方式就是command1._name。需要注意的是,我們不能在Go的struct類型中嵌入C語言類型的字段。這與我們在前面所說的代碼隔離原則具有相同的意義。

在上面展示的庫源碼文件rand.go中有多處對C語言函數(shù)的訪問。實際上,任何C語言的函數(shù)都可以 被Go語言代碼調(diào)用。只要在源碼文件中導(dǎo)入了代碼包C。并且,我們還可以同時取回C語言函數(shù)的結(jié)果,以及一個作為錯誤提示信息的變量。這與我們在Go語言中同時獲取多個函數(shù)結(jié)果的方法一樣。同樣的,我們可以使用下劃線“_”直接丟棄取回的值。這在調(diào)用無結(jié)果的C語言函數(shù)時非常有用。請看下面的例子:

package cgo

/*
#cgo LDFLAGS: -lm
#include <math.h>
*/
import "C"

func Sqrt(p float32) (float32, error) {
    n, err := C.sqrt(C.double(p))
    return float32(n), err
}

上面這段代碼被保存在了Go語言庫源碼文件math.go中,并與源碼文件rand.go在同一個代碼包目錄。在Go語言函數(shù)Sqrt中的C.sqrt是一個在C語言標(biāo)準(zhǔn)代碼庫math.h中的函數(shù)。它會返回參數(shù)的平方根。但是在第一行代碼中,我們接收由函數(shù)C.sqrt返回的兩個值。其中,第一個值即為C語言函數(shù)sqrt的結(jié)果。而第二個值就是我們上面所說的那個作為錯誤提示信息的變量。實際上,這個變量的類型是Go語言的error接口類型。它包裝了一個C語言的全局變量errno。這個全局變量被定義在了C語言代碼庫errno.h中。cgo工具在為我們生成C語言源碼文件時會默認(rèn)引入兩個C語言標(biāo)準(zhǔn)代碼庫,其中一個就是errno.h。所以我們并不用在Go語言源碼文件中使用指令符#include顯式的引入這個代碼庫。cgo工具默認(rèn)為我們引入的另一個是C語言標(biāo)準(zhǔn)代碼庫string.h。它包含了很多用于字符串處理和內(nèi)存處理的函數(shù)。

在我們以“C.*”的形式調(diào)用C語言代碼庫時,有一點需要特別注意。在C語言中,如果一個函數(shù)的參數(shù)是一個具有固定尺寸的數(shù)組,那么實際上這個函數(shù)所需要的是指向這個數(shù)組的第一個元素的指針。C編譯器能夠正確識別和處理這個調(diào)用慣例。它可以自行獲取到這個指針并傳給函數(shù)。但是,這在我們使用cgo工具調(diào)用C語言代碼庫時是行不通的。在Go語言中,我們必須顯式的將這個指向數(shù)組的第一個元素的指針傳遞給C語言的函數(shù),像這樣:``C.func1(&x[0])````。

另一個需要特別注意的地方是,在C語言中沒有像Go語言中獨立的字符串類型。C語言使用最后一個元素為‘\0’的字符數(shù)組來代表字符串。在Go語言的字符串和C語言的字符串之間進行轉(zhuǎn)換的時候,我們就需要用到代碼包C中的C.C.CString、C.GoStringC.GoStringN等函數(shù)。這些轉(zhuǎn)換操作是通過對字符串?dāng)?shù)據(jù)的拷貝來完成的。Go語言內(nèi)存管理器并不能感知此類內(nèi)存分配操作。因為它們是由C語言代碼引發(fā)的。所以,我們在使用與C.CString函數(shù)類似的會導(dǎo)致內(nèi)存分配操作的函數(shù)之后,需要調(diào)用代碼包C的free函數(shù)以手動的釋放內(nèi)存。這里有一個小技巧,即我們可以把對C.free函數(shù)的調(diào)用放到defer語句中或者放入在defer之后的匿名函數(shù)中。這樣就可以保證在退出當(dāng)前函數(shù)之前釋放這些被分配的內(nèi)存了。請看下面這個示例:

func Print(s string) {
        cs := C.CString(s)
        defer C.free(unsafe.Pointer(cs))
        C.myprint(cs)
}

上面這段代碼被存放在goc2p項目的代碼包basic/cgo/lib的庫源碼文件print.go中。其中的函數(shù)C.myprint是我們在該庫源碼文件的序文中自定義的。關(guān)于這種C語言函數(shù)定義方式,我們一會兒再解釋。在這段代碼中,我們首先把Go語言的字符串轉(zhuǎn)換為了C語言的字符串。注意,變量cs的值實際上是指向字符串(在C語言中,字符串由字符數(shù)組代表)中第一個字符的指針。在cgo工具對應(yīng)的上下文環(huán)境中,cs變量的類型是*C.Char。然后,我們通過defer語句和C.free函數(shù)保證由C語言代碼分配的內(nèi)存得以釋放。請注意子語句unsafe.Pointer(cs)。正因為cs變量在C語言中的類型是指針類型,且與之相對應(yīng)的Go語言類型是unsafe.Pointer。所以,我們需要先將其轉(zhuǎn)換為Go語言可以識別的類型再作為參數(shù)傳遞給函數(shù)C.free。最后,我們將這個字符串打印到標(biāo)準(zhǔn)輸出。

再次重申,我們在使用C.CString函數(shù)將Go語言的字符串轉(zhuǎn)換為C語言字符串后,需要顯式的調(diào)用C.free函數(shù)以釋放用于數(shù)據(jù)拷貝的內(nèi)存。而最佳實踐是,將在defer語句中調(diào)用C.free函數(shù)。

在前面我們已經(jīng)提到過,在導(dǎo)入代碼包C的語句之上可以加入若干個為cgo工具而寫的若干注釋行(也被叫做序文)。并且,以#include和一個空格開始的注釋行可以用來引入一個C語言的接口文件。我們也把序文中這種形式的字符串叫做指令符。指令符#cgo的用途是為編譯器和連接器提供標(biāo)記。這些標(biāo)記在編譯當(dāng)前源碼文件中涉及到代碼包C的那部分代碼時會被用到。

標(biāo)記CFLAGSLDFLAGS``可以被放在指令符#cgo```之后,并用于定制編譯器gcc的行為。gcc(GNU Compiler Collection,GNU編譯器套裝),是一套由GNU開發(fā)的開源的編程語言編譯器。它是GNU項目的關(guān)鍵部分,也是類Unix操作系統(tǒng)(也包括Linux操作系統(tǒng))中的標(biāo)準(zhǔn)編譯器。gcc(特別是其中的C語言編譯器)也常被認(rèn)為是跨平臺編譯器的事實標(biāo)準(zhǔn)。gcc原名為GNU C語言編譯器(GNU C Compiler),因為它原本只能處理C語言。不過,gcc變得可以處理更多的語言?,F(xiàn)在,gcc中包含了很多針對特定編程語言的編譯器。我們在本節(jié)第一小節(jié)的末尾提及的gccgo就是這款套件中針對Go語言的編譯器。標(biāo)記CFLAGS可以指定用于gcc中的C編譯器的選項。它嘗嘗用于指定頭文件(.h文件)的路徑。而標(biāo)記LDFLAGS則可以指定gcc編譯器會用到的一些優(yōu)化參數(shù),也可以用來告訴鏈接器需要用到的C語言代碼庫文件的位置。

為了清晰起見,我們可以把這些標(biāo)記及其值拆分成多個注釋行,并均以指令符#cgo作為前綴。另外,在指令符#cgo和標(biāo)記之間,我們也可以加入一些可選的內(nèi)容,即環(huán)境變量GOOS和GOARCH中的有效值。這樣,我們就可以使這些標(biāo)記只在某些操作系統(tǒng)和/或某些計算架構(gòu)的環(huán)境下起作用了。示例如下:

// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo linux CFLAGS: -DLINUX=1
// #cgo LDFLAGS: -lpng
// #include <png.h>
import "C"

在上面的示例中,序文由四個注釋行組成。第一行注釋的含義是預(yù)定義一個名為PNG_DEBUG的宏并將它的值設(shè)置為1。而第二行注釋的意思是,如果在Linux操作系統(tǒng)下,則預(yù)定義一個名為LINUX的宏并將它的值設(shè)置為1。第三行注釋是與鏈接器有關(guān)的。它告訴鏈接器需要用到一個庫名為png的代碼庫文件。最后,第四行注釋引入了C語言的標(biāo)準(zhǔn)代碼庫png.h。

如果我們有一些在所有場景下都會用到的CFLAGS標(biāo)記或LDFLAGS標(biāo)記的值,那么就可以把它們分別作為環(huán)境變量CGO_CFLAGSCGO_LDFLAGS的值。而對于需要針對某個導(dǎo)入了“C”的代碼包的標(biāo)記值就只能連同指令符#cgo一起放入Go語言源碼文件的注釋行中了。

相信讀者對指令符#cgo#include的用法已經(jīng)有所了解了。

實際上,我們幾乎可以在序文中加入任何C代碼。像這樣:

/*
#cgo LDFLAGS: -lsqlite3

#include <sqlite3.h>
#include <stdlib.h>

// These wrappers are necessary because SQLITE_TRANSIENT
// is a pointer constant, and cgo doesn't translate them correctly.
// The definition in sqlite3.h is:
//
// typedef void (*sqlite3_destructor_type)(void*);
// #define SQLITE_STATIC      ((sqlite3_destructor_type)0)
// #define SQLITE_TRANSIENT   ((sqlite3_destructor_type)-1)

static int my_bind_text(sqlite3_stmt *stmt, int n, char *p, int np) {
        return sqlite3_bind_text(stmt, n, p, np, SQLITE_TRANSIENT);
}
static int my_bind_blob(sqlite3_stmt *stmt, int n, void *p, int np) {
        return sqlite3_bind_blob(stmt, n, p, np, SQLITE_TRANSIENT);
}

*/

上面這段代碼摘自開源項目gosqlite的Go語言源碼文件sqlite.go。gosqlite項目是一個開源數(shù)據(jù)SQLite的Go語言版本的驅(qū)動代碼庫。實際上,它只是把C語言版本的驅(qū)動代碼庫進行了簡單的封裝。在Go語言的世界里,這樣的封裝隨處可見,尤其是在Go語言發(fā)展早期。因為,這樣可以非常方便的重用C語言版本的客戶端程序,而大多數(shù)軟件都早已擁有這類程序了。并且,封裝C語言版本的代碼庫與從頭開發(fā)一個Go語言版本的客戶端程序相比,無論從開發(fā)效率還是運行效率上來講都會是非常迅速的?,F(xiàn)在讓我們看看在上面的序文中都有些什么。很顯然,在上面的序文中直接出現(xiàn)了兩個C語言的函數(shù)my_bind_textmy_bind_blob。至于為什么要把C語言函數(shù)直接寫到這里,在它們前面的注釋中已經(jīng)予以說明。大意翻譯如下:這些包裝函數(shù)是必要的,這是因為SQLITE_TRANSIENT是一個指針常量,而cgo并不能正確的翻譯它們??吹贸鰜恚@是一種備選方案,只有在cgo不能幫我們完成工作時才會被選用。不管怎樣,在序文中定義的這兩個函數(shù)可以直接在當(dāng)前的Go語言源碼文件中被使用。具體的使用方式同樣是通過“C.*”的形式來調(diào)用。比如源碼文件sqlite.go中的代碼:

rv := C.my_bind_text(s.stmt, C.int(i+1), cstr, C.int(len(str)))

rv := C.my_bind_blob(s.stmt, C.int(i+1), unsafe.Pointer(p), C.int(len(v)))

上述示例中涉及到的源碼文件可以通過這個網(wǎng)址訪問到。有興趣的讀者可以前往查看。

我們再來看看我們之前提到過的庫源碼文件print.go(位于goc2p項目的代碼包basic/cgo/lib之中)的序文:

/*
#include <stdio.h>
#include <stdlib.h>

void myprint(char* s) {
        printf("%s", s);
}
*/
import "C"

我們在序文中定義一個名為myprint的函數(shù)。在這個函數(shù)中調(diào)用了C語言的函數(shù)printf。自定義函數(shù)myprint充當(dāng)了類似于適配器的角色。之后,我們就可以在后續(xù)的代碼中直接使用這個自定義的函數(shù)了:

C.myprint(cs)

關(guān)于在序文中嵌入C語言代碼的方法我們就介紹到這里。

現(xiàn)在,讓我們來使用go tool cgo命令并以rand.go作為參數(shù)生成_obj子目錄和相關(guān)源碼文件:

hc@ubt:~/golang/goc2p/src/basic/cgo/lib$ go tool cgo rand.go 
hc@ubt:~/golang/goc2p/src/basic/cgo/lib$ ls
_obj  rand.go
hc@ubt:~/golang/goc2p/src/basic/cgo/lib$ ls _obj
_cgo_defun.c   _cgo_export.h  _cgo_gotypes.go  _cgo_.o       rand.cgo2.c
_cgo_export.c  _cgo_flags     _cgo_main.c      rand.cgo1.go

子目錄_obj中一共包含了九個文件。

其中,cgo工具會把作為參數(shù)的Go語言源碼文件rand.go轉(zhuǎn)換為四個文件。其中包括兩個Go語言源碼文件rand.cgo1.go和_cgo_gotypes.go,以及兩個C語言源碼文件_cgo_defun.c和rand.cgo2.c。

文件rand.cgo1.go用于存放cgo工具對原始源碼文件rand.go改寫后的內(nèi)容。改寫具體細(xì)節(jié)包括去掉其中的代碼包C導(dǎo)入語句,以及替換涉及到代碼包C的語句,等等。最后,這些替換后的標(biāo)識符所對應(yīng)的Go語言的函數(shù)、類型或變量的定義,將會被寫入到文件_cgo_gotypes.go中。

需要說明的是,替換涉及到代碼包C的語句的具體做法是根據(jù)xxx的種類將標(biāo)識符C.xxx替換為_Cfunc_xxx或者_Ctype_xxx。比如,作為參數(shù)的源碼文件rand.go中存在如下語句:

C.srand(C.uint(i))

cgo工具會把它改寫為:

_Cfunc_srand(_Ctype_uint(i))

其中,標(biāo)識符C.srand被替換為_Cfunc_srand,而標(biāo)識符C.uint被替換為了_Ctype_uint。并且,新的標(biāo)識符_Cfunc_srand_Ctype_uint的定義將會在文件_cgo_gotypes.go中被寫明:

type _Ctype_uint uint32

type _Ctype_void [0]byte

func _Cfunc_srand(_Ctype_uint) _Ctype_void

其中,類型_Ctype_void可以表示空的參數(shù)列表或空的結(jié)果列表。

文件_cgo_defun.c中包含了相應(yīng)的C語言函數(shù)的定義和實現(xiàn)。例如,C語言函數(shù)_Cfunc_srand的實現(xiàn)如下:

#pragma cgo_import_static _cgo_54716c7dc6a7_Cfunc_srand
void _cgo_54716c7dc6a7_Cfunc_srand(void*);

void
·_Cfunc_srand(struct{uint8 x[4];}p)
{
    runtime·cgocall(_cgo_54716c7dc6a7_Cfunc_srand, &p);
}

其中,十六進制數(shù)“54716c7dc6a7”是cgo工具由于作為參數(shù)的源碼文件的內(nèi)容計算得出的哈希值。這個十六進制數(shù)作為了函數(shù)名稱_cgo_54716c7dc6a7_Cfunc_srand的一部分。這樣做是為了生成一個唯一的名稱以避免沖突。我們看到,在源碼文件_cgo_defun.c中只包含了函數(shù)_cgo_54716c7dc6a7_Cfunc_srand的定義。而其實現(xiàn)被寫入到了另一個C語言源碼文件中。這個文件就是rand.cgo2.c。函數(shù)_cgo_54716c7dc6a7_Cfunc_srand對應(yīng)的實現(xiàn)代碼如下:

void
_cgo_f290d3e89fd1_Cfunc_srand(void *v)
{
    struct {
        unsigned int p0;
    } __attribute__((__packed__)) *a = v;
    srand(a->p0);
}

這個函數(shù)從指向函數(shù)_Cfunc_puts的參數(shù)幀中抽取參數(shù),并調(diào)用系統(tǒng)C語言函數(shù)srand,最后將結(jié)果存儲在幀中并返回。

下面我們對在子目錄_obj中存放的其余幾個文件進行簡要說明:

  • 文件_cgo_flags用于存放CFLAGS標(biāo)記和LDFLAGS標(biāo)記的值。

  • 文件_cgo_main.c用于存放一些C語言函數(shù)的存根,也可以說是一些函數(shù)的空實現(xiàn)。函數(shù)的空實現(xiàn)即在函數(shù)體重沒有任何代碼(return語句除外)的實現(xiàn)。其中包括在源碼文件_cgo_export.c出現(xiàn)的聲明為外部函數(shù)的函數(shù)。另外,文件_cgo_main.c中還會有一個被用于動態(tài)鏈接處理的main函數(shù)。

  • 在文件_cgo_export.h中存放了可以暴露給C語言代碼的與Go語言類型相對應(yīng)的C語言聲明語句。

  • 文件_cgo_export.c中則包含了與可以暴露給C語言代碼的Go語言函數(shù)相對應(yīng)的C語言函數(shù)定義和實現(xiàn)代碼。

  • 文件cgo.o是gcc編譯器在編譯C語言源碼文件rand.cgo2.c、_cgo_export.c和_cgo_main.c之后生成的結(jié)果文件。

在上述的源碼文件中,文件rand.cgo1.go和_cgo_gotypes.go將會在構(gòu)建代碼包時被Go官方Go語言編譯器(6g、8g或5g)編譯。文件_cgo_defun.c會在構(gòu)建代碼包時被Go官方的C語言的編譯器(6c、8c或5c)編譯。而文件rand.cgo2.c、_cgo_export.c和_cgo_main.c 則會被gcc編譯器編譯。

如果我們在執(zhí)行go tool cgo命令時加入多個Go語言源碼文件作為參數(shù),那么在當(dāng)前目錄的_obj子目錄下會出現(xiàn)與上述參數(shù)數(shù)量相同的x.cgo1.go文件和x.cgo2.c文件。其中,x為作為參數(shù)的Go語言源碼文件主文件名。

通過上面的描述,我們基本了解了由cgo工具生成的文件的內(nèi)容和用途。

與其它go命令一樣,我們在執(zhí)行go tool cgo命令的時候還可以加入一些標(biāo)記。如下表。

表0-24 go tool cgo命令可接受的標(biāo)記

名稱 默認(rèn)值 說明
-cdefs false 將改寫后的源碼內(nèi)容以C定義模式打印到標(biāo)準(zhǔn)輸出,而不生成相關(guān)的源碼文件。
-godefs false 將改寫后的源碼內(nèi)容以Go定義模式打印到標(biāo)準(zhǔn)輸出,而不生成相關(guān)的源碼文件。
-objdir "" gcc編譯的目標(biāo)文件所在的路徑。若未自定義則為當(dāng)前目錄下的_obj子目錄。
-dynimport "" 如果值不為空字符串,則打印為其值所代表的文件生成的動態(tài)導(dǎo)入數(shù)據(jù)到標(biāo)準(zhǔn)輸出。
-dynlinker false 記錄在dynimport模式下的動態(tài)鏈接器信息。
-dynout "" 將-dynimport的輸出(如果有的話)寫入到其值所代表的文件中。
-gccgo false 生成可供gccgo編譯器使用的文件。
-gccgopkgpath "" 對應(yīng)于gccgo編譯器的-fgo-pkgpath選項。
-gccgoprefix "" 對應(yīng)于gccgo編譯器的-fgo-prefix選項。
-debug-define false 打印相關(guān)的指令符#defines及其后續(xù)內(nèi)容到標(biāo)準(zhǔn)輸出。
-debug-gcc false 打印gcc調(diào)用信息到標(biāo)準(zhǔn)輸出。
-import_runtime_cgo true 在生成的代碼中加入語句“import runtime/cgo”。
-import_syscall true 在生成的代碼中加入語句“import syscall”。

在上表中,我們把標(biāo)記分為了五類并在它們之間以空行分隔。

在第一類標(biāo)記中,-cdefs標(biāo)記和-godefs標(biāo)記都可以打印相應(yīng)的代碼到標(biāo)準(zhǔn)輸出,并且使cgo工具不生成相應(yīng)的源碼文件。cgo工具在獲取目標(biāo)源碼文件內(nèi)容之后會改寫其中的內(nèi)容,包括去掉代碼包C的導(dǎo)入語句,以及對代碼包C的調(diào)用語句中屬于代碼包C的類型、函數(shù)和變量進行等價替換。如果我們加入了標(biāo)記-cdefs-godefs,那么cgo工具隨后就會把改寫后的目標(biāo)源碼打印到標(biāo)準(zhǔn)輸出了。需要注意的是,我們不能同時使用這兩個標(biāo)記。使用這兩個標(biāo)記打印出來的源碼內(nèi)容幾乎相同,而最大的區(qū)別也只是格式方面的。

第二類的三個標(biāo)記都與動態(tài)鏈接庫有關(guān)。在類Unix系統(tǒng)下,標(biāo)記-dynimport的值可以是一個ELF(Executable and Linkable Format)格式或者Mach-O(Mach Object)格式的文件的路徑。ELF即可執(zhí)行鏈接文件格式。ELF格式的文件保存了足夠的系統(tǒng)相關(guān)信息,以至于使它能夠支持不同平臺上的交叉編譯和交叉鏈接,可移植性很強。同時,它在執(zhí)行中支持動態(tài)鏈接共享庫。我們在Linux操作系統(tǒng)下使用go命令生成的命令源碼文件的可執(zhí)行文件就是ELF格式的。而Mach-O是一種用于可執(zhí)行文件、目標(biāo)代碼、動態(tài)鏈接庫和內(nèi)核轉(zhuǎn)儲的文件格式。在Windows下,這個標(biāo)記的值應(yīng)該是一個PE(Portable Execute)格式的文件的路徑。在Windows操作系統(tǒng)下,使用go命令生成的命令源碼文件的可執(zhí)行文件就是PE格式的。

實質(zhì)上,加入標(biāo)記-dynimportgo tool cgo命令相當(dāng)于一個被構(gòu)建在cgo工具內(nèi)部的獨立的幫助命令。使用方法如go tool cgo -dynimport='cgo_demo.go'。這個命令會掃描這個標(biāo)記的值所代表的可執(zhí)行文件,并將其中記錄的與已導(dǎo)入符號和已導(dǎo)入代碼庫相關(guān)的信息打印到標(biāo)準(zhǔn)輸出。go build命令程序中有專門為cgo工具制定的規(guī)則。這使得它可以在編譯直接或間接依賴了代碼包C的命令源碼文件時可以生成適當(dāng)?shù)目蓤?zhí)行文件。在這個可執(zhí)行文件中,直接包含了相關(guān)的已導(dǎo)入符號和已導(dǎo)入代碼庫的信息,以供之后使用。這樣就無需使鏈接器復(fù)制gcc編譯器的所有關(guān)于如何尋找已導(dǎo)入的符號以及使用它的位置的專業(yè)知識了。下面我們來試用一下go tool cgo -dynimport命令。

首先,我們創(chuàng)建一個命令源碼文件cgo_demo.go,并把它存放在goc2p項目的代碼包basic/cgo對應(yīng)的目錄下。命令源碼文件cgo_demo.go的內(nèi)容如下:

package main

import (
    cgolib "basic/cgo/lib"
    "fmt"
)

func main() {
    input := float32(2.33)
    output, err := cgolib.Sqrt(input)
    if err != nil {
        fmt.Errorf("Error: %s\n", err)
    }
    fmt.Printf("The square root of %f is %f.\n", input, output)
}

在這個命令源碼文件中,我們調(diào)用了goc2p項目的代碼包basic/cgo/lib中的函數(shù)Sqrt。這個函數(shù)是被保存在庫源碼文件math.go中的。而在文件math.go中,我們導(dǎo)入了代碼包C。也就是說,命令源碼文件cgo_demo.go間接的依賴了代碼包C?,F(xiàn)在,我們使用go build命令將這個命令源碼文件編譯成ELF格式的可執(zhí)行文件。然后,我們就能夠使用go tool cgo -dynimport命令查看其中的導(dǎo)入信息了。請看如下示例:

hc@ubt:~/golang/goc2p/basic/cgo$ go build cgo_demo.go
hc@ubt:~/golang/goc2p/basic/cgo$ go tool cgo -dynimport='cgo_demo'
#pragma cgo_import_dynamic pthread_attr_init pthread_attr_init#GLIBC_2.1 
    "libpthread.so.0"
#pragma cgo_import_dynamic pthread_attr_destroy pthread_attr_destroy#GLIBC_2.0 
    "libpthread.so.0"
#pragma cgo_import_dynamic stderr stderr#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic sigprocmask sigprocmask#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic free free#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic fwrite fwrite#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic malloc malloc#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic strerror strerror#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic srand srand#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic setenv setenv#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic __libc_start_main __libc_start_main#GLIBC_2.0 
    "libc.so.6"
#pragma cgo_import_dynamic fprintf fprintf#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic pthread_attr_getstacksize
     pthread_attr_getstacksize#GLIBC_2.1 "libpthread.so.0"
#pragma cgo_import_dynamic sigfillset sigfillset#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic __errno_location __errno_location#GLIBC_2.0 
    "libpthread.so.0"
#pragma cgo_import_dynamic sqrt sqrt#GLIBC_2.0 "libm.so.6"
#pragma cgo_import_dynamic rand rand#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic pthread_create pthread_create#GLIBC_2.1 
    "libpthread.so.0"
#pragma cgo_import_dynamic abort abort#GLIBC_2.0 "libc.so.6"
#pragma cgo_import_dynamic _ _ "libm.so.6"
#pragma cgo_import_dynamic _ _ "libpthread.so.0"
#pragma cgo_import_dynamic _ _ "libc.so.6"

從上面示例的輸出信息中,我們可以看到可執(zhí)行文件cgo_demo所涉及到的所有動態(tài)鏈接庫文件以及相關(guān)的函數(shù)名和代碼庫版本等信息。

如果我們再加入一個標(biāo)記-dynlinker,那么在命令的輸出信息還會包含動態(tài)鏈接器的信息。示例如下:

hc@ubt:~/golang/goc2p/src/basic/cgo$ go tool cgo -dynimport='cgo_demo' -dynlinker
#pragma cgo_dynamic_linker "/lib/ld-linux.so.2"
<省略部分輸出內(nèi)容>

如果我們在命令go tool cgo -dynimport后加入標(biāo)記-dynout,那么命令的輸出信息將會寫入到指定的文件中,而不是被打印到標(biāo)準(zhǔn)輸出。比如命令go tool cgo -dynimport='cgo_demo' -dynlinker -dynout='cgo_demo.di'就會將可執(zhí)行文件cgo_demo中的導(dǎo)入信息以及動態(tài)鏈接器信息轉(zhuǎn)儲到當(dāng)前目錄下的名為“cgo_demo.di”的文件中。

第四類標(biāo)記包含了-gccgo、-gccgopkgpath-gccgoprefix。它們都與編譯器gccgo有關(guān)。標(biāo)記-gccgo的作用是使cgo工具生成可供gccgo編譯器使用的源碼文件。這些源碼文件會與默認(rèn)情況下生成的源碼文件在內(nèi)容上有一些不同。實際上,到目前為止,cgo工具還不能很好的與gccgo編譯器一同使用。但是,按照gccgo編譯器的主要開發(fā)者Ian Lance Taylor的話來說,gccgo編譯器并不需要cgo工具,也不應(yīng)該使用gcc工具。不管怎樣,這種情況將會在Go語言的1.3版本中得到改善。

第五類標(biāo)記用于打印調(diào)試信息,包括標(biāo)記-debug-define-debug-gcc。gcc工具不但會生成新的Go語言源碼文件以保存其對目標(biāo)源碼改寫后的內(nèi)容,還會生成若干個C語言源碼文件。cgo工具為了編譯這些C語言源碼文件,就會用到gcc編譯器。在加入-debug-gcc標(biāo)記之后,gcc編譯器的輸出信息就會被打印到標(biāo)準(zhǔn)輸出上。另外,gcc編譯器在對C語言源碼文件進行編譯之后會產(chǎn)生一個結(jié)果文件。這個結(jié)果文件就是在_obj子目錄下的名為cgo.o的文件。

第六類標(biāo)記的默認(rèn)值都為true。也就是說,在默認(rèn)情況下cgo工具生成的_obj子目錄下的Go語言源碼文件_cgogotypes.go中會包含代碼包導(dǎo)入語句```import "runtime/cgo"import "syscall"。代碼包導(dǎo)入語句import _ "runtime/cgo"只是引發(fā)了代碼包runtime/cgo中的初始化函數(shù)的執(zhí)行而沒有被分配到一個具體的命名空間上。在這些初始化函數(shù)中,包含了對一些C語言的全局變量和函數(shù)聲明的初始化過程。需要注意的是,只要我們在執(zhí)行go tool cgo命令的時候加入了標(biāo)記-gccgo,即使標(biāo)記-import_runtime_cgo有效,在Go語言源碼文件_cgo_gotypes.go中也不會包含import _ "runtime/cgo"```語句。

至此,我們在本小節(jié)討論的都是Go語言代碼如果通過cgo工具調(diào)用標(biāo)準(zhǔn)C語言編寫的函數(shù)。其實,我們利用cgo工具還可以把Go語言編寫的函數(shù)暴露給C語言代碼。

Go語言可以使它的函數(shù)被C語言代碼所用。這是通過使用一個特殊的注釋“//export”來實現(xiàn)的。示例如下:

package cgo

/*
#include <stdio.h>
extern void CFunction1();
*/
import "C"

import "fmt"

//export GoFunction1
func GoFunction1() {
        fmt.Println("GoFunction1() is called.")
}

func CallCFunc() {
        C.CFunction1()
}

在這個示例中,我們使用注釋行“//export GoFunction1”把Go語言函數(shù)GoFunction1暴露給了C語言代碼。注意,注釋行中在“//export ”之后的字符串必須與其下一行的那個函數(shù)的名字一致。我們也可以把字符串“//export”看成一種指令符,就像#cgo#include。這里有一個限制,就是只要我們使用了指令符“//export”,在當(dāng)前源碼文件的序文中就不能包含任何C語言定義語句,只可以包含C語言聲明語句。上面示例的序文中的extern void CFunction1();就是一個很好的例子。序文中的這一行C語言聲明語句會被拷貝到兩個不同的cgo工具生成的C語言源碼文件中。這也正是其序文中不能包含C語言定義語句的原因。那么C語言函數(shù)CFunction1的定義語句我們應(yīng)該放在哪兒呢?答案是放到在同目錄的其它Go語言源碼文件的序文中,或者直接寫到C語言源碼文件中。

我們把上面示例中的內(nèi)容保存到名為go_export.go的文件中,并放到goc2p項目的basic/cgo/lib代碼包中。現(xiàn)在我們使用go tool cgo來處理這個源碼文件。如下:

hc@ubt:~/golang/goc2p/basic/cgo/lib$ go tool cgo go_export.go

之后,我們會發(fā)現(xiàn)在_obj子目錄下的C語言頭文件_cgo_export.h中包含了這樣一行代碼:

extern void GoFunction1();

這說明C語言代碼已經(jīng)可以對函數(shù)GoFunction1進行調(diào)用了?,F(xiàn)在我們使用go build命令構(gòu)建goc2p項目的代碼包basic/cgo,如下:

hc@ubt:~/golang/goc2p/basic/cgo/lib$ go build
# basic/cgo/lib
/tmp/go-build477634277/basic/cgo/lib/_obj/go_export.cgo2.o: In function `_cgo_cc103c85817e_Cfunc_CFunction1':
./go_export.go:34: undefined reference to `CFunction1'
collect2: ld return 1

構(gòu)建并沒有成功完成。根據(jù)錯誤提示信息我們獲知,C語言函數(shù)CFunction1未被定義。這個問題的原因是我們并沒有在Go語言源碼文件go_export.go的序文中寫入C語言函數(shù)CFunction1的實現(xiàn),也即未對它進行定義。我們之前說過,在這種情況下,對應(yīng)函數(shù)的定義應(yīng)該被放到其它Go語言源碼文件的序文或者C語言源碼文件中?,F(xiàn)在,我們在當(dāng)前目錄下創(chuàng)建一個新的Go語言源碼文件go_export_def.go。其內(nèi)容如下:

package cgo

/*
#include <stdio.h>
void CFunction1() {
        printf("CFunction1() is called.\n");
        GoFunction1();
} 
*/
import "C"

這個文件是專門用于存放C語言函數(shù)定義的。注意,由于C語言函數(shù)printf來自C語言標(biāo)準(zhǔn)代碼庫stdio.h,所以我們需要在序文中使用指令符#include將它引入。保存好源碼文件go_export_def.go之后,我們重新使用go tool cgo命令處理這兩個文件,如下:

hc@ubt:~/golang/goc2p/basic/cgo/lib$ go tool cgo go_export.go go_export_def.go

然后,我們再次執(zhí)行go build命令構(gòu)建代碼包basic/cgo/lib

hc@ubt:~/golang/goc2p/basic/cgo/lib$ go build

顯然,這次的構(gòu)建成功完成。當(dāng)然單獨構(gòu)建代碼包basic/cgo/lib并不是必須的。我們在這里是為了檢查該代碼包中的代碼(包括Go語言代碼和C語言代碼)是否都能夠被正確編譯。

還記得goc2p項目的代碼包basic/cgo中的命令源碼文件cgo_demo.go。現(xiàn)在我們在它的main函數(shù)的最后加入一行新代碼:cgo.CallCFunc(),即調(diào)用在代碼包``basic/cgo/lib```中的庫源碼文件go_export.go的函數(shù)。然后,我們運行這個命令源碼文件:

hc@ubt:~/golang/goc2p/basic/cgo$ go run cgo_demo.go
The square root of 2.330000 is 1.526434.
ABC
CFunction1() is called.
GoFunction1() is called.

從輸出的信息可以看出,我們定義的C語言函數(shù)CFunction1和Go語言函數(shù)GoFunction1都已被調(diào)用,并且調(diào)用順序正如我們所愿。這個例子也說明,我們可以非常方便的使用cgo工具完成如下幾件事:

  1. Go語言代碼調(diào)用標(biāo)準(zhǔn)C語言的代碼。這也使得我們可以使用Go語言封裝任何已存在的C語言代碼庫,并提供給其他Go語言代碼使用。

  2. 可以在Go語言源碼文件的序文中自定義任何C語言代碼并由Go語言代碼使用。這使得我們可以更靈活的對C語言代碼進行封裝。同時,我們還可以利用這一特性在我們自定義的C語言代碼中使用Go語言代碼。

  3. 通過指令符“//export”,可使C語言代碼能夠使用Go語言代碼。這里所說的C語言代碼是指我們在Go語言源碼文件的序文中自定義的C語言代碼。但是,go tool cgo命令會將序文中的C語言代碼聲明和定義分別寫入到其生成的C語言頭文件和C語言源碼文件中。所以,從原則上講,這已經(jīng)具備了讓外部C語言代碼使用Go語言代碼的能力。

綜上所述,cgo工具不但可以使Go語言直接使用現(xiàn)存的非常豐富的C語言代碼庫,還可以使用Go語言代碼擴展現(xiàn)有的C語言代碼庫。

至此,我們介紹了怎樣獨立的使用cgo工具。但實際上,我們可以直接使用標(biāo)準(zhǔn)go命令構(gòu)建、安裝和運行導(dǎo)入了代碼包C的代碼包和源碼文件。標(biāo)準(zhǔn)go命令能夠認(rèn)出代碼包C的導(dǎo)入語句并自動使用cgo工具進行處理。示例如下:

hc@ubt:~/golang/goc2p/src/basic/cgo$ rm -rf lib/_obj
hc@ubt:~/golang/goc2p/src/basic/cgo$ go run cgo_demo.go
The square root of 2.330000 is 1.526434.
ABC
CFunction1() is called.
GoFunction1() is called.

在上例中,我們首先刪除了代碼包basic/cgo/lib目錄下的子目錄_obj,以此來保證原始的測試環(huán)境。然后,我們直接運行了命令源碼文件cgo_demo.go。在這個源碼文件中,包含了對代碼包basic/cgo/lib中函數(shù)的調(diào)用語句,而在這些函數(shù)中又包含了對代碼包C的引用。從輸出信息我們可以看出,命令源碼文件cgo_demo.go的運行成功的完成了。這也驗證了標(biāo)準(zhǔn)go命令在這方面的功能。不過,有時候我們還是很有必要單獨使用go tool cgo命令,比如對相關(guān)的Go語言代碼和C語言代碼的功能進行驗證或者需要通過標(biāo)記定制化運行cgo工具的時候。另外,如果我們通過標(biāo)準(zhǔn)go命令構(gòu)建或者安裝直接或間接導(dǎo)入了代碼C的命令源碼文件,那么在生成的可執(zhí)行文件中就會包含動態(tài)導(dǎo)入數(shù)據(jù)和動態(tài)鏈接器信息。我們可以使用go tool cgo命令查看可執(zhí)行文件中的這些信息。

上一篇:go env下一篇:go clean