Go Wiki:cgo

簡介

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

https://go.dev.org.tw/blog/cgo 也有不錯的入門文章

基礎

如果 Go 原始檔匯入 "C",它正在使用 cgo。Go 檔案將可以存取出現在 import "C" 行之前註解中的任何內容,並會連結到其他 Go 檔案中的所有其他 cgo 註解,以及建置程序中包含的所有 C 檔案。

請注意,cgo 註解和匯入陳述式之間不能有空白行。

若要存取來自 C 端的符號,請使用套件名稱 C。也就是說,如果你想從 Go 程式碼呼叫 C 函式 printf(),你可以寫 C.printf()。由於目前不支援可變引數方法(例如 printf)(問題 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 程式碼可以使用其函式。請注意:如果你使用匯出,則無法在前言中定義任何 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 提供了以下三個函式來轉換這兩種字串

要記住的一件重要事情是 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 位元組切片

若要建立由 C 陣列支援的 Go 切片(不複製原始資料),需要在執行階段取得這個長度,並使用類型轉換為一個非常大的陣列的指標,然後將其切片成你想要的長度(如果你使用 Go 1.2 或更新版本,也記得設定上限),例如(請參閱 https://go.dev.org.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 端釋放陣列,使用切片的任何 Go 程式碼的行為都是不確定的。

常見陷阱

結構體對齊問題

由於 Go 不支援封裝結構體(例如,最大對齊為 1 位元的結構體),因此您無法在 Go 中使用封裝 C 結構體。即使您的程式通過編譯,它也不會執行您想要的操作。若要使用它,您必須將結構體讀取/寫入為位元組陣列/切片。

另一個問題是,某些類型的對齊需求低於其在 Go 中的對應類型,而且如果該類型碰巧在 C 中對齊但在 Go 規則中沒有對齊,則該結構體根本無法在 Go 中表示。範例如下 (問題 7560)

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

Go 的 complex64 對齊為 8 位元組,而 C 只有 4 位元組(因為 C 在內部將複數浮點數視為 struct { float real; float imag; },而不是基本類型),因此這個 T 結構體根本沒有 Go 表示。對於這種情況,如果您控制結構體的配置,請移動複數浮點數,使其也對齊到 8 位元組會更好,如果您不願意移動它,請使用此表單將其強制對齊到 8 位元組(並浪費 4 位元組)

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

但是,如果您不控制結構體配置,您將必須為該結構體定義存取器 C 函式,因為 cgo 無法將該結構體轉換為等效的 Go 結構體。

//export 和前言中的定義

如果 Go 原始檔使用任何 //export 指令,則註解中的 C 程式碼只能包含宣告(extern int f();),不能包含定義(int f() { return 1; } int n;)。注意:您可以使用 static inline 技巧來解決此限制,以解決前言中定義的微小函式(請參閱上方以取得完整範例)。

Windows

要在 Windows 上使用 cgo,您還需要先安裝 gcc 編譯器(例如 mingw-w64),並在編譯 cgo 之前將 gcc.exe(等)放入您的 PATH 環境變數中。

環境變數

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

測試

_test.go 檔案無法使用 cgo。


此內容為 Go Wiki 的一部分。