原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch2-cgo/ch2-04-func.html
函數(shù)是 C 語(yǔ)言編程的核心,通過(guò) CGO 技術(shù)我們不僅僅可以在 Go 語(yǔ)言中調(diào)用 C 語(yǔ)言函數(shù),也可以將 Go 語(yǔ)言函數(shù)導(dǎo)出為 C 語(yǔ)言函數(shù)。
對(duì)于一個(gè)啟用 CGO 特性的程序,CGO 會(huì)構(gòu)造一個(gè)虛擬的 C 包。通過(guò)這個(gè)虛擬的 C 包可以調(diào)用 C 語(yǔ)言函數(shù)。
/*
static int add(int a, int b) {
return a+b;
}
*/
import "C"
func main() {
C.add(1, 1)
}
以上的 CGO 代碼首先定義了一個(gè)當(dāng)前文件內(nèi)可見(jiàn)的 add 函數(shù),然后通過(guò) C.add
。
對(duì)于有返回值的 C 函數(shù),我們可以正常獲取返回值。
/*
static int div(int a, int b) {
return a/b;
}
*/
import "C"
import "fmt"
func main() {
v := C.div(6, 3)
fmt.Println(v)
}
上面的 div 函數(shù)實(shí)現(xiàn)了一個(gè)整數(shù)除法的運(yùn)算,然后通過(guò)返回值返回除法的結(jié)果。
不過(guò)對(duì)于除數(shù)為 0 的情形并沒(méi)有做特殊處理。如果希望在除數(shù)為 0 的時(shí)候返回一個(gè)錯(cuò)誤,其他時(shí)候返回正常的結(jié)果。因?yàn)?C 語(yǔ)言不支持返回多個(gè)結(jié)果,因此 <errno.h>
標(biāo)準(zhǔn)庫(kù)提供了一個(gè) errno
宏用于返回錯(cuò)誤狀態(tài)。我們可以近似地將 errno
看成一個(gè)線(xiàn)程安全的全局變量,可以用于記錄最近一次錯(cuò)誤的狀態(tài)碼。
改進(jìn)后的 div 函數(shù)實(shí)現(xiàn)如下:
#include <errno.h>
int div(int a, int b) {
if(b == 0) {
errno = EINVAL;
return 0;
}
return a/b;
}
CGO 也針對(duì) <errno.h>
標(biāo)準(zhǔn)庫(kù)的 errno
宏做的特殊支持:在 CGO 調(diào)用 C 函數(shù)時(shí)如果有兩個(gè)返回值,那么第二個(gè)返回值將對(duì)應(yīng) errno
錯(cuò)誤狀態(tài)。
/*
#include <errno.h>
static int div(int a, int b) {
if(b == 0) {
errno = EINVAL;
return 0;
}
return a/b;
}
*/
import "C"
import "fmt"
func main() {
v0, err0 := C.div(2, 1)
fmt.Println(v0, err0)
v1, err1 := C.div(1, 0)
fmt.Println(v1, err1)
}
運(yùn)行這個(gè)代碼將會(huì)產(chǎn)生以下輸出:
2 <nil>
0 invalid argument
我們可以近似地將 div 函數(shù)看作為以下類(lèi)型的函數(shù):
func C.div(a, b C.int) (C.int, [error])
第二個(gè)返回值是可忽略的 error 接口類(lèi)型,底層對(duì)應(yīng) syscall.Errno
錯(cuò)誤類(lèi)型。
C 語(yǔ)言函數(shù)還有一種沒(méi)有返回值類(lèi)型的函數(shù),用 void 表示返回值類(lèi)型。一般情況下,我們無(wú)法獲取 void 類(lèi)型函數(shù)的返回值,因?yàn)闆](méi)有返回值可以獲取。前面的例子中提到,cgo 對(duì) errno 做了特殊處理,可以通過(guò)第二個(gè)返回值來(lái)獲取 C 語(yǔ)言的錯(cuò)誤狀態(tài)。對(duì)于 void 類(lèi)型函數(shù),這個(gè)特性依然有效。
以下的代碼是獲取沒(méi)有返回值函數(shù)的錯(cuò)誤狀態(tài)碼:
//static void noreturn() {}
import "C"
import "fmt"
func main() {
_, err := C.noreturn()
fmt.Println(err)
}
此時(shí),我們忽略了第一個(gè)返回值,只獲取第二個(gè)返回值對(duì)應(yīng)的錯(cuò)誤碼。
我們也可以嘗試獲取第一個(gè)返回值,它對(duì)應(yīng)的是 C 語(yǔ)言的 void 對(duì)應(yīng)的 Go 語(yǔ)言類(lèi)型:
//static void noreturn() {}
import "C"
import "fmt"
func main() {
v, _ := C.noreturn()
fmt.Printf("%#v", v)
}
運(yùn)行這個(gè)代碼將會(huì)產(chǎn)生以下輸出:
main._Ctype_void{}
我們可以看出 C 語(yǔ)言的 void 類(lèi)型對(duì)應(yīng)的是當(dāng)前的 main 包中的 _Ctype_void
類(lèi)型。其實(shí)也將 C 語(yǔ)言的 noreturn 函數(shù)看作是返回 _Ctype_void
類(lèi)型的函數(shù),這樣就可以直接獲取 void 類(lèi)型函數(shù)的返回值:
//static void noreturn() {}
import "C"
import "fmt"
func main() {
fmt.Println(C.noreturn())
}
運(yùn)行這個(gè)代碼將會(huì)產(chǎn)生以下輸出:
[]
其實(shí)在 CGO 生成的代碼中,_Ctype_void
類(lèi)型對(duì)應(yīng)一個(gè) 0 長(zhǎng)的數(shù)組類(lèi)型 [0]byte
,因此 fmt.Println
輸出的是一個(gè)表示空數(shù)值的方括弧。
以上有效特性雖然看似有些無(wú)聊,但是通過(guò)這些例子我們可以精確掌握 CGO 代碼的邊界,可以從更深層次的設(shè)計(jì)的角度來(lái)思考產(chǎn)生這些奇怪特性的原因。
CGO 還有一個(gè)強(qiáng)大的特性:將 Go 函數(shù)導(dǎo)出為 C 語(yǔ)言函數(shù)。這樣的話(huà)我們可以定義好 C 語(yǔ)言接口,然后通過(guò) Go 語(yǔ)言實(shí)現(xiàn)。在本章的第一節(jié)快速入門(mén)部分我們已經(jīng)展示過(guò) Go 語(yǔ)言導(dǎo)出 C 語(yǔ)言函數(shù)的例子。
下面是用 Go 語(yǔ)言重新實(shí)現(xiàn)本節(jié)開(kāi)始的 add 函數(shù):
import "C"
//export add
func add(a, b C.int) C.int {
return a+b
}
add 函數(shù)名以小寫(xiě)字母開(kāi)頭,對(duì)于 Go 語(yǔ)言來(lái)說(shuō)是包內(nèi)的私有函數(shù)。但是從 C 語(yǔ)言角度來(lái)看,導(dǎo)出的 add 函數(shù)是一個(gè)可全局訪(fǎng)問(wèn)的 C 語(yǔ)言函數(shù)。如果在兩個(gè)不同的 Go 語(yǔ)言包內(nèi),都存在一個(gè)同名的要導(dǎo)出為 C 語(yǔ)言函數(shù)的 add 函數(shù),那么在最終的鏈接階段將會(huì)出現(xiàn)符號(hào)重名的問(wèn)題。
CGO 生成的 _cgo_export.h
文件會(huì)包含導(dǎo)出后的 C 語(yǔ)言函數(shù)的聲明。我們可以在純 C 源文件中包含 _cgo_export.h
文件來(lái)引用導(dǎo)出的 add 函數(shù)。如果希望在當(dāng)前的 CGO 文件中馬上使用導(dǎo)出的 C 語(yǔ)言 add 函數(shù),則無(wú)法引用 _cgo_export.h
文件。因?yàn)?nbsp;_cgo_export.h
文件的生成需要依賴(lài)當(dāng)前文件可以正常構(gòu)建,而如果當(dāng)前文件內(nèi)部循環(huán)依賴(lài)還未生成的 _cgo_export.h
文件將會(huì)導(dǎo)致
cgo 命令錯(cuò)誤。
#include "_cgo_export.h"
void foo() {
add(1, 1);
}
當(dāng)導(dǎo)出 C 語(yǔ)言接口時(shí),需要保證函數(shù)的參數(shù)和返回值類(lèi)型都是 C 語(yǔ)言友好的類(lèi)型,同時(shí)返回值不得直接或間接包含 Go 語(yǔ)言?xún)?nèi)存空間的指針。
![]() | ![]() |
更多建議: