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 的一部分。