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 提供了以下三个函数,可以从一种形式转换为另一种形式
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 切片(不复制原始数据),需要在运行时获取此长度,并使用类型转换将其转换为指向非常大数组的指针,然后将其切片到你想要的长度(还要记住设置 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 的一部分。