Go Wiki: cgo

引言

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

https://go-lang.org.cn/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://go-lang.org.cn/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 的一部分。