CGO函數調用
函數是C語言編程的核心,通過CGO技術我們不僅僅可以在Go語言中調用C語言函數,也可以將Go語言函數導出為C語言函數。
Go調用C函數
對於一個啟用CGO特性的程序,CGO會構造一個虛擬的C包。通過這個虛擬的C包可以調用C語言函數。
package main
/*
static int add(int a, int b) {
return a+b;
}
*/
import "C"
func main() {
C.add(1, 1)
}
以上的CGO代碼首先定義了一個當前文件內可見的add函數,然后通過C.add
。
C函數的返回值
對於有返回值的C函數,我們可以正常獲取返回值。
package main
/*
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函數實現了一個整數除法的運算,然后通過返回值返回除法的結果。
不過對於除數為0的情形並沒有做特殊處理。如果希望在除數為0的時候返回一個錯誤,其他時候返回正常的結果。因為C語言不支持返回多個結果
,因此<errno.h>
標准庫提供了一個errno
宏用於返回錯誤狀態。我們可以近似地將errno看成一個線程安全的全局變量,可以用於記錄最近一次錯誤的狀態碼。
改進后的div函數實現如下:
#include <errno.h>
int div(int a, int b) {
if(b == 0) {
errno = EINVAL;
return 0;
}
return a/b;
}
CGO也針對<errno.h>
標准庫的errno
宏做的特殊支持:在CGO調用C函數時如果有兩個返回值,那么第二個返回值將對應errno
錯誤狀態。
package main
/*
#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)
}
運行這個代碼將會產生以下輸出:
$ go run .
>> 2 <nil>
>> 0 invalid argument
我們可以近似地將div函數看作為以下類型的函數:
func C.div(a, b C.int) (C.int, [error])
第二個返回值是可忽略的error
接口類型,底層對應 syscall.Errno
錯誤類型。
void函數的返回值
C語言函數還有一種沒有返回值類型的函數,用void
表示返回值類型。一般情況下,我們無法獲取void
類型函數的返回值,因為沒有返回值可以獲取。前面的例子中提到,cgo對errno
做了特殊處理,可以通過第二個返回值來獲取C語言的錯誤狀態。對於void
類型函數,這個特性依然有效。
package main
//static void noreturn() {}
import "C"
import "fmt"
func main() {
_, err := C.noreturn()
fmt.Println(err)
}
此時,我們忽略了第一個返回值,只獲取第二個返回值對應的錯誤碼。
$ go run .
>> <nil>
我們也可以嘗試獲取第一個返回值,它對應的是C語言的void對應的Go語言類型:
package main
//static void noreturn() {}
import "C"
import "fmt"
func main() {
v, _ := C.noreturn()
fmt.Printf("%#v", v)
}
運行這個代碼將會產生以下輸出:
$ go run .
>> main._Ctype_void{}
我們可以看出C語言的void
類型對應的是當前的main包中的_Ctype_void
類型。其實也將C語言的noreturn函數
看作是返回_Ctype_void
類型的函數,這樣就可以直接獲取void類型函數的返回值:
package main
//static void noreturn() {}
import "C"
import "fmt"
func main() {
fmt.Println(C.noreturn())
}
運行這個代碼將會產生以下輸出:
$ go run .
>> []
其實在CGO生成的代碼中,_Ctype_void
類型對應一個0長的數組類型[0]byte
,因此fmt.Println輸出的是一個表示空數值的方括弧。
以上有效特性雖然看似有些無聊,但是通過這些例子我們可以精確掌握CGO代碼的邊界,可以從更深層次的設計的角度來思考產生這些奇怪特性的原因。
C調用Go導出函數
CGO還有一個強大的特性:將Go函數導出為C語言函數。這樣的話我們可以定義好C語言接口,然后通過Go語言實現。
下面是用Go語言重新實現本節開始的add函數:
import "C"
//export add
func add(a, b C.int) C.int {
return a+b
}
add函數名以小寫字母開頭,對於Go語言來說是包內的私有函數。但是從C語言角度來看,導出的add函數是一個可全局訪問的C語言函數。如果在兩個不同的Go語言包內,都存在一個同名的要導出為C語言函數的add函數,那么在最終的鏈接階段將會出現符號重名的問題。
CGO生成的 _cgo_export.h
文件會包含導出后的C語言函數的聲明。我們可以在純C源文件中包含 _cgo_export.h
文件來引用導出的add函數。如果希望在當前的CGO文件中馬上使用導出的C語言add函數,則無法引用 _cgo_export.h
文件。因為_cgo_export.h
文件的生成需要依賴當前文件可以正常構建,而如果當前文件內部循環依賴還未生成的_cgo_export.h
文件將會導致cgo命令錯誤。
#include "_cgo_export.h"
void foo() {
add(1, 1);
}
當導出C語言接口時,需要保證函數的參數和返回值類型都是C語言友好的類型,同時返回值不得直接或間接包含Go語言內存空間的指針。