Go Wiki:cgo

简介

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

https://golang.ac.cn/blog/cgo 中还有一篇很好的介绍文章

基础知识

如果 Go 源文件导入 "C",则它正在使用 cgo。Go 文件将能够访问紧接在 import "C" 行之前的注释中显示的任何内容,并且将与其他 Go 文件中的所有其他 cgo 注释以及构建过程中包含的所有 C 文件进行链接。

请注意,cgo 注释和 import 语句之间不得有空行。

要访问源自 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 切片(不复制原始数据),需要在运行时获取此长度,并使用类型转换将其转换为指向非常大数组的指针,然后将其切片到你想要的长度(还要记住设置 cap,如果你使用的是 Go 1.2 或更高版本),例如(参见 https://golang.ac.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 端释放,则使用该切片的任何 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 之前在 PATH 环境变量中拥有 gcc.exe(等)。

环境变量

Go os.Getenv() 无法看到由 C.setenv() 设置的变量

测试

_test.go 文件无法使用 cgo。


此内容是 Go Wiki 的一部分。