CGO 入门
学下 CGO
CGO
用法
Go 调用 C
Go 可以通过如下方式引入 “C” 假包
1 | // #cgo CFLAGS: -DPNG_DEBUG=1 |
引入注释中的 C 语言语句, 这里的注释被称为前导码(preamble); 通过 #cgo 引入编译和链接选项
注意, 即便引入的 C 语言函数/变量为小写开头, 依然对 Go 是可见的, 包括静态函数; 但静态变量可能(原文如此)不可见
C 调用 Go
1 | import "C" |
export 指令将 Go 中的函数作为全局函数引入到 C 中, 因此若有多个 Go 包引入了同名函数, 则会报链接错误
1 |
|
C 与 Go 之间的数据转换
大部分 C 语言的类型 A 的 CGO 类型是 C.A, 但是 C 语言类型中可能含有空格, 此类转换可以参照下表中的对应选项. 也可以使用 typedef 将其转换为单个单词的类型
| C 语言类型 | CGO 类型 | Go 语言类型 |
|---|---|---|
char |
C.char |
byte |
singed char |
C.schar |
int8 |
unsigned char |
C.uchar |
uint8 |
short |
C.short |
int16 |
unsigned short |
C.ushort |
uint16 |
int |
C.int |
int32 |
unsigned int |
C.uint |
uint32 |
long |
C.long |
int32 |
unsigned long |
C.ulong |
uint32 |
long long int |
C.longlong |
int64 |
unsigned long long int |
C.ulonglong |
uint64 |
float |
C.float |
float32 |
double |
C.double |
float64 |
size_t |
C.size_t |
uint |
int8_t |
C.int8_t |
int8 |
uint8_t |
C.uint8_t |
uint8 |
int16_t |
C.int16_t |
int16 |
uint16_t |
C.uint16_t |
uint16 |
int32_t |
C.int32_t |
int32 |
uint32_t |
C.uint32_t |
uint32 |
int64_t |
C.int64_t |
int64 |
uint64_t |
C.uint64_t |
uint64 |
复杂类型
struct
C 语言中的结构体 struct A 的 CGO 类型是 C.struct_A, 在普通情况下 其内存布局和对齐规则与 C 语言中的一致(32 位/64 位), 但无法访问设置了特殊对齐规则的 C 结构体
若结构体中的成员名称为 Go 中的关键字, 可以通过添加下划线前缀对齐进行访问; 但若结构体中存在与其增加前缀后相同名称的成员, 则无法访问无前缀的成员
1 | /* |
Go 亦不支持访问位域和 0 长数组
union
可以通过 C.union_xxx 访问 C 中的 union xxx, 但会被直接转换为相应大小的字节数组; 可以通过强制类型转换或在 C 中编写辅助函数获得原始数据
1 | /* |
enum
可以通过 C.enum_xxx 访问 C 中的 enum xxx
1 | /* |
数组
引入的 “C” 伪包提供了一系列 C 与 Go 之间数组相互转换的函数; 需要注意的是前两个函数使用 malloc 在 C 的堆中分配内存, 需要使用 C.free 手动释放
1 | // Go string to C string |
以及 C 中数组作为函数参数会退化为指针, 直接传入数组名和数组首地址的指针是等价的; 但是 Go 则不同, Go 的数组名就代表数组本身, 可以通过 C.foo(unsafe.Pointer(&array[0])), C.foo(&C.array[0]) 传入 C 函数中
内存安全
Go 的内存会随着函数栈的动态伸缩发生移动, 若 C 语言持有移动前的指针在移动后进行访问会导致访问野指针
因此 CGO 要求传入 C 语言中的指针应该是被固定(pinned)的, Go 会定时检查这条规则
- 作为参数传递给 C 函数的指向 Go 语言分配的内存的指针指向的内存在函数调用期间会被隐式固定;
- 其他 Go 内存也可以使用
runtime.Pinner进行手动固定和解除固定; 可以多次固定/解除固定, 但最终解除固定的次数应与固定的次数相同(类似std::recursive_mutex)
这样保证了 C 不会访问失效指针, 但可能导致 Goroutine 因不能动态伸缩内存导致的阻塞.
同时需要注意应在获得指向 Go 语言分配的内存的指针后因直接传入 C 函数中, 若作为中间变量存储下来则可能导致在获取与调用 C 函数之间该指针就失效了
runtime/cgo 包的 Handle 提供了一种更高级的指针传递方法.
底层原理
编写简单的 CGO 程序如下
1 | package main |
使用
1 | go tool cgo main.go |
可以看到在 _obj 目录下生成了如下文件
1 | _cgo_.o |
其中 main.cgo1.go 为对 C 函数和变量进行替换后的 Go 代码
1 | // Code generated by cmd/cgo; DO NOT EDIT. |
其定义在 _cgo_gotypes.go 中, 可以看到最终调用 _cgo_runtime_cgocall 传入 C 函数地址和保存有调用函数的参数的结构体
1 | // Code generated by cmd/cgo; DO NOT EDIT. |
其中的 C 函数是一个中间函数, 调用原始的 C 函数, 这两者都可以在 main.cgo2.c 看到
1 | int sum(int a, int b) { return a+b; } |
小细节
C.malloc不是直接调用 C 中的malloc函数, 而是调用了一个 Go 包装的malloc; 其永远不会返回nil, 在内存不足时直接使程序崩溃, 与 Go 程序遇到内存不足时的行为保持一致- CGO 不支持调用可变参数的 C 函数
- Go 不支持直接调用 C 函数指针, 但是可以保存指针并在 C 与 Go 之间传递
- CGO 导入的 C 语言类型为 unexported, 因此不应将其作为接口的参数. 且不同包使用的相同 C 类型是不同的
- 可以使用
C.sizeof_T等效查看 C 中的sizeof(T) - 可以通过
errno.h来实现类似 Go 中返回err的操作
1 | /* |
1 | //static void noreturn() {} |
- Go 中的
string不一定以\0结尾, 但C.CString会贴心的在结尾加上一个
extern “C”
CGO 是 C 与 Go 的桥梁. 但由于名称修饰等原因, C++ 的动态库暴露出的符号不会只是函数名本身, 而是类似于 _ZN12MyNamespace10myFunctionEi 以实现函数重载, namespace 区分等功能
因此需要在头文件中形如
1 |
|
告知编译器不要对其中的函数声明对应的实现做名称修饰. 使用 ifdef 可以将该头文件直接复制到 Go 项目中让 CGO 编译(C 编译器不会接受 extern "C")
由于没有修饰, 不能使用函数重载, 也不能使用 namespace(如有同名函数或变量依然会导致冲突); 但函数的实现依然可以使用任何 C++ 的特性
1 | int x; |
此外还需要注意 extern "C" 和 extern "C" {} 的区别
1 | extern "C" int i; // declaration |
extern 甚至可以将其中的内容当做其他语言来做名称修饰, 只不过标准只要求编译器必须支持 “C”
语言链接也是函数类型的一部分
1 | extern "C" // the name f1 and its function type have C language linkage; |