使用 C2Go
内存与 Go GC
判断一块 C 数据应由 Go GC 管理,还是继续使用普通 C 生命周期。维护者C2Go contributorsC 程序通常自己决定内存何时释放;Go 则由垃圾回收器决定对象何时可以回收。C2Go 需要知道某个 C 指针属于哪一种生命周期,否则 Go GC 可能看不到仍在使用的对象。
这里的 managed/unmanaged 描述的是 GC 如何看待指针,不是一套自动 ownership 系统;谁创建、谁释放、callee 是否保留指针,仍然需要由 API 明确约定。
阅读本章时只需要先回答两个问题:
- 这块内存由 Go GC 回收,还是由代码、系统或原生库释放?
- 这块内存内部是否保存了需要被 Go GC 继续跟踪的指针?
两种内存的直观区别
| 使用场景 | 选择 | 原因 |
|---|---|---|
使用 gc_malloc 创建、需要跨 Go 调用继续存活的对象 |
managed | Go GC 必须知道对象类型和内部指针位置 |
| Go 管理的链表、树或其他含指针对象 | managed | 写入指针时需要 Go write barrier |
普通 malloc/free 缓冲区 |
unmanaged | 生命周期仍由 C 代码手动控制 |
| 系统调用、动态库或第三方 C ABI 使用的结构体 | unmanaged | 必须保持原生 C 内存与 ABI 语义 |
| 函数中的普通局部变量 | 保持默认 | 只能在当前调用期间使用,不能把地址长期保存 |
当前 C2Go 默认使用普通 C 的 unmanaged 语义。只有确实需要 Go GC 跟踪的数据才应显式标为 managed。
需要 Go GC 管理的对象
下面的链表节点会保存另一个节点的指针,并且可能跨多次 Go/C 调用存活,因此将其声明为 managed:
#include <c2go.h>
struct managed Node {
struct Node *next;
int value;
};
struct Node *new_node(int value) {
struct Node *node = gc_malloc(
c2go_typeinfo(struct Node), sizeof(*node));
node->next = 0;
node->value = value;
return node;
}
这里有三件事协同工作:
managed告诉编译器节点中哪些字段是 GC 指针;c2go_typeinfo(struct Node)提供该类型的布局;gc_malloc在 Go heap 上创建这个带类型的对象。
给 node->next 赋值时,编译器会生成 Go runtime 需要的 write barrier。仅仅写上 managed 不会分配内存,也不会把栈变量自动搬到堆上。
保持普通 C 生命周期
传给操作系统、动态库或现有 C API 的数据通常应保持 unmanaged:
#include <c2go.h>
struct unmanaged NativeBuffer {
void *data;
size_t length;
};
extern int vendor_write(struct NativeBuffer *buffer);
普通 malloc、calloc、realloc 和 free 也保留手动释放语义。不要在这类裸缓冲区中保存需要 Go GC 跟踪的 managed 指针;GC 不知道缓冲区内部的字段布局。
像 vendor_write 这样只有声明、没有定义的函数已经是 native import,不需要显式 unmanaged 标记。
unmanaged 可以在接口边界上显式表达意图。对于没有标记的普通 C 类型,它也是当前默认行为。
最常见的错误
把局部变量地址长期保存
void attach(struct Node *node) {
struct Node local;
node->next = &local; // local 会随函数返回而失效
}
Go 不会像 Go 编译器处理 Go 源码那样,自动把这个 C 局部变量提升到 heap。源码诊断只能覆盖一部分直接写法,发布前还必须运行栈指针逃逸审计。
混合两种内存而不说明转换
把 managed 指针写入 unmanaged 存储、转换成整数,或通过不兼容布局的类型访问,都可能让 GC 丢失指针信息。C2Go 会对此产生 warning 或 error。不要通过整数中转或关闭 warning 来绕过它;应重新设计边界,或者把数据复制到生命周期清晰的表示中。
让 GC 无法识别字段位置
包含 GC 指针的布局不能随意 packing、重叠或改变 alignment。Managed record 的特殊布局会产生警告;无法安全扫描的 unmanaged record 会被拒绝。公开此类类型前,应检查生成的 Go 类型并测试每个 target。
实际选择原则
先保留普通 C 写法。只有当对象同时满足“由 Go heap 持有”或“内部指针必须由 Go GC 跟踪”时,才引入 managed 类型和 gc_malloc。与操作系统、动态库和原生 ABI 交互的内存继续保持 unmanaged,并维持明确的创建与释放关系。
属性、pragma bitmask 和转换诊断的完整语法放在 C2Go 扩展速查;下一章用 GC 感知分配构造更完整的 managed 对象。