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.srand
和C.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.GoString
和C.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)記CFLAGS
和LDFLAGS``可以被放在指令符
#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_CFLAGS
和CGO_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_text
和my_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)代碼。
在上述的源碼文件中,文件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)記-dynimport
的go 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工具完成如下幾件事:
Go語言代碼調(diào)用標(biāo)準(zhǔn)C語言的代碼。這也使得我們可以使用Go語言封裝任何已存在的C語言代碼庫,并提供給其他Go語言代碼使用。
可以在Go語言源碼文件的序文中自定義任何C語言代碼并由Go語言代碼使用。這使得我們可以更靈活的對C語言代碼進行封裝。同時,我們還可以利用這一特性在我們自定義的C語言代碼中使用Go語言代碼。
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í)行文件中的這些信息。