CGO函數調用


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語言內存空間的指針。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM