Go Wiki: cgo

引言

首先,https://pkg.go.dev/cmd/cgo 是主要的 cgo 文件。

https://golang.com.tw/blog/cgo 也有一個很好的介紹文章。

基礎知識

如果一個 Go 原始檔匯入了 "C",那麼它就在使用 cgo。該 Go 檔案將可以訪問緊隨 import "C" 語句之前的註釋中的任何內容,並且將與所有其他 Go 檔案中的 cgo 註釋以及構建過程中包含的所有 C 檔案連結。

請注意,cgo 註釋和 import 語句之間不能有空行。

要訪問來自 C 語言端的符號,請使用包名 C。也就是說,如果你想從 Go 程式碼呼叫 C 函式 printf(),你可以寫 C.printf()。由於像 printf 這樣的可變引數方法尚未支援(參見 issue 975),我們將用 C 方法“myprint”來包裝它。

package cgoexample

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

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

import "unsafe"

func Example() {
    cs := C.CString("Hello from stdio\n")
    C.myprint(cs)
    C.free(unsafe.Pointer(cs))
}

從 C 呼叫 Go 函式

透過 cgo 從 Go 程式碼呼叫的 C 程式碼可以呼叫頂層 Go 函式和函式變數。

全域性函式

Go 透過使用特殊的 //export 註釋使其函式可供 C 程式碼使用。注意:如果你使用了 export,則不能在 preamble 中定義任何 C 函式。

例如,有兩個檔案,foo.c 和 foo.go:foo.go 包含

package gocallback

import "fmt"

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

//export AGoFunction
func AGoFunction() {
    fmt.Println("AGoFunction()")
}

func Example() {
    C.ACFunction()
}

foo.c 包含

##include "_cgo_export.h"
void ACFunction() {
    printf("ACFunction()\n");
    AGoFunction();
}

函式變數

下面的程式碼展示了一個從 C 程式碼呼叫 Go 回撥函式的示例。由於 指標傳遞規則,Go 程式碼不能直接將函式值傳遞給 C。因此,必須使用間接方式。本例使用了一個帶互斥鎖的登錄檔,但也有許多其他方法可以將可傳遞給 C 的值對映到 Go 函式。

package gocallback

import (
    "fmt"
    "sync"
)

/*
extern void go_callback_int(int foo, int p1);

// normally you will have to define function or variables
// in another separate C file to avoid the multiple definition
// errors, however, using "static inline" is a nice workaround
// for simple functions like this one.
static inline void CallMyFunction(int foo) {
    go_callback_int(foo, 5);
}
*/
import "C"

//export go_callback_int
func go_callback_int(foo C.int, p1 C.int) {
    fn := lookup(int(foo))
    fn(p1)
}

func MyCallback(x C.int) {
    fmt.Println("callback with", x)
}

func Example() {
    i := register(MyCallback)
    C.CallMyFunction(C.int(i))
    unregister(i)
}

var mu sync.Mutex
var index int
var fns = make(map[int]func(C.int))

func register(fn func(C.int)) int {
    mu.Lock()
    defer mu.Unlock()
    index++
    for fns[index] != nil {
        index++
    }
    fns[index] = fn
    return index
}

func lookup(i int) func(C.int) {
    mu.Lock()
    defer mu.Unlock()
    return fns[i]
}

func unregister(i int) {
    mu.Lock()
    defer mu.Unlock()
    delete(fns, i)
}

從 Go 1.17 開始,runtime/cgo 包提供了 runtime/cgo.Handle 機制,並簡化了上述示例為

package main

import (
    "fmt"
    "runtime/cgo"
)

/*
##include <stdint.h>

extern void go_callback_int(uintptr_t h, int p1);
static inline void CallMyFunction(uintptr_t h) {
    go_callback_int(h, 5);
}
*/
import "C"

//export go_callback_int
func go_callback_int(h C.uintptr_t, p1 C.int) {
    fn := cgo.Handle(h).Value().(func(C.int))
    fn(p1)
}

func MyCallback(x C.int) {
    fmt.Println("callback with", x)
}

func main() {
    h := cgo.NewHandle(MyCallback)
    C.CallMyFunction(C.uintptr_t(h))
    h.Delete()
}

函式指標回撥

C 程式碼可以呼叫匯出的 Go 函式,使用它們的顯式名稱。但是,如果 C 程式需要一個函式指標,就必須編寫一個閘道器函式。這是因為我們不能獲取 Go 函式的地址並將其提供給 C 程式碼,因為 cgo 工具將生成一個 C 存根來呼叫。下面的示例展示瞭如何與需要給定型別函式指標的 C 程式碼整合。

將這些原始檔放在 $GOPATH/src/ccallbacks/ 目錄下。使用以下命令編譯並執行:

$ gcc -c clibrary.c
$ ar cru libclibrary.a clibrary.o
$ go build
$ ./ccallbacks
Go.main(): calling C function with callback to us
C.some_c_func(): calling callback with arg = 2
C.callOnMeGo_cgo(): called with arg = 2
Go.callOnMeGo(): called with arg = 2
C.some_c_func(): callback responded with 3

goprog.go

package main

/*
##cgo CFLAGS: -I .
##cgo LDFLAGS: -L . -lclibrary

##include "clibrary.h"

int callOnMeGo_cgo(int in); // Forward declaration.
*/
import "C"

import (
    "fmt"
    "unsafe"
)

//export callOnMeGo
func callOnMeGo(in int) int {
    fmt.Printf("Go.callOnMeGo(): called with arg = %d\n", in)
    return in + 1
}

func main() {
    fmt.Printf("Go.main(): calling C function with callback to us\n")
    C.some_c_func((C.callback_fcn)(unsafe.Pointer(C.callOnMeGo_cgo)))
}

cfuncs.go

package main

/*

##include <stdio.h>

// The gateway function
int callOnMeGo_cgo(int in)
{
    printf("C.callOnMeGo_cgo(): called with arg = %d\n", in);
    int callOnMeGo(int);
    return callOnMeGo(in);
}
*/
import "C"

clibrary.h

##ifndef CLIBRARY_H
##define CLIBRARY_H
typedef int (*callback_fcn)(int);
void some_c_func(callback_fcn);
##endif

clibrary.c

##include <stdio.h>

##include "clibrary.h"

void some_c_func(callback_fcn callback)
{
    int arg = 2;
    printf("C.some_c_func(): calling callback with arg = %d\n", arg);
    int response = callback(2);
    printf("C.some_c_func(): callback responded with %d\n", response);
}

Go 字串和 C 字串

Go 字串和 C 字串是不同的。Go 字串是長度和指向字串第一個字元的指標的組合。C 字串只是指向第一個字元的指標,並以第一個空字元 ('\0') 終止。

Go 提供了以下三個函式來實現這兩種字串之間的轉換:

  • func C.CString(goString string) *C.char
  • func C.GoString(cString *C.char) string
  • func C.GoStringN(cString *C.char, length C.int) string

有一點需要牢記的是,C.CString() 會分配一個新的、長度適當的字串並返回它。這意味著 C 字串不會被垃圾回收,而是由 **你** 來釋放。標準方法如下:

// #include <stdlib.h>
import "C"
import "unsafe"
...
    var cmsg *C.char = C.CString("hi")
    defer C.free(unsafe.Pointer(cmsg))
    // do something with the C string

當然,你不必使用 defer 來呼叫 C.free()。你可以在任何時候釋放 C 字串,但你必須確保它被釋放。

將 C 陣列轉換為 Go 切片

C 陣列通常是空終止的,或者它們的長度儲存在別處。

Go 提供了以下函式,用於從 C 陣列建立新的 Go 位元組切片:

  • func C.GoBytes(cArray unsafe.Pointer, length C.int) []byte

要建立一個由 C 陣列支援的 Go 切片(而不復制原始資料),需要在執行時獲取該長度,並將其型別轉換為指向一個非常大的陣列的指標,然後將其切片到所需的長度(如果使用 Go 1.2 或更高版本,請記住設定 cap),例如(參閱 https://golang.com.tw/play/p/XuC0xqtAIC 獲取可執行示例):

import "C"
import "unsafe"
...
        var theCArray *C.YourType = C.getTheArray()
        length := C.getTheArrayLength()
        slice := (*[1 << 28]C.YourType)(unsafe.Pointer(theCArray))[:length:length]

對於 Go 1.17 或更高版本,程式可以使用 unsafe.Slice,這同樣會得到一個由 C 陣列支援的 Go 切片:

import "C"
import "unsafe"
...
        var theCArray *C.YourType = C.getTheArray()
        length := C.getTheArrayLength()
        slice := unsafe.Slice(theCArray, length) // Go 1.17

重要的是要記住,Go 的垃圾回收器不會與底層 C 陣列進行互動,如果 C 陣列在 C 端被釋放,任何使用該切片的 Go 程式碼的行為都將是不可預測的。

常見陷阱

結構體對齊問題

由於 Go 不支援 packed struct(例如,最大對齊為 1 位元組的 struct),因此不能在 Go 中使用 packed C struct。即使你的程式編譯透過,它也不會按預期工作。要使用它,你必須將 struct 讀取/寫入為位元組陣列/切片。

另一個問題是,某些型別在 C 中的對齊要求比在 Go 中的對齊要求低,如果該型別在 C 中對齊而在 Go 規則中不對齊,那麼該 struct 根本無法在 Go 中表示。例如(參見 issue 7560):

struct T {
    uint32_t pad;
    complex float x;
};

Go 的 complex64 有 8 位元組的對齊,而 C 只有 4 位元組(因為 C 在內部將 complex float 視為 struct { float real; float imag; },而不是基本型別)。這個 T struct 根本沒有 Go 的表示。在這種情況下,如果你控制 struct 的佈局,最好將 complex float 移動到也對齊到 8 位元組的位置,如果你不願意移動它,使用這種形式可以強制它對齊到 8 位元組(並浪費 4 位元組):

struct T {
   uint32_t pad;
   __attribute__((align(8))) complex float x;
};

但是,如果你不控制 struct 的佈局,你將不得不為該 struct 定義訪問器 C 函式,因為 cgo 無法將該 struct 翻譯成等效的 Go struct。

//export 和 preamble 中的定義

如果 Go 原始檔使用任何 //export 指令,那麼註釋中的 C 程式碼只能包含宣告(extern int f();),而不能包含定義(int f() { return 1; } int n;)。注意:你可以使用 static inline 技巧來繞過這個限制,用於在 preamble 中定義的微小函式(參見上面的完整示例)。

Windows

要在 Windows 上使用 cgo,你還需要先安裝一個 gcc 編譯器(例如,mingw-w64),並在 PATH 環境變數中包含 gcc.exe(等),然後才能成功編譯 cgo。

環境變數

Go 的 os.Getenv() 看不到 C.setenv() 設定的變數。

測試

_test.go 檔案不能使用 cgo。


此內容是 Go Wiki 的一部分。