分享一个还在 RC 阶段的项目:把 C 编译成不依赖 cgo 的 Go package

8 月 3 日
 kkhaike

最近把一个持续开发中的项目整理到了可以公开试用的阶段:C2Go Toolchain。

项目地址:https://github.com/c2gohq/c2go_toolchain

中文文档:https://c2go.buymecompile.top/zh-cn/

当前预发布版本:v0.20260802.0-rc.1

先用一句话说明它在做什么:

C2Go 基于 Clang/LLVM ,把 C 源码提前编译成可由 Go 工具链直接构建的 .go + .s package ;生成结果不使用 import "C",使用方可以在 CGO_ENABLED=0、没有宿主 C 编译器的环境中完成构建。

它不是把 C 文本逐句改写成 Go 源码,也不是在应用启动后嵌入一个 C 编译器。C 的编译发生在生成阶段,最终产物进入普通的 Go build 流程。

为什么做这个项目

Go 调用已有 C 实现时,最成熟的方案当然还是 cgo 。但我想探索的是另一条路径:

C2Go 就是围绕这些目标做的一套编译器、绑定生成器和 libc 兼容层。

它具体生成什么

目前的管线大致如下:

C 源码
   │
   ▼
c2go-clang + c2go-lto
   │  Plan 9 汇编 + JSON manifest
   ▼
c2go-bind
   │  生成 .go + .s
   ▼
普通 Go package
   │
   ▼
go build / go test (可使用 CGO_ENABLED=0 )

几个主要仓库的分工是:

一个最小例子

先下载与当前系统匹配的 SDK 、把 bin 加入 PATH。下面以 macOS arm64 为例,其他平台只需要换成对应的 target triple 。

mkdir hello-c2go
cd hello-c2go
mkdir .c2go

go mod init example.com/hello-c2go
go get github.com/c2gohq/c2go_libc@v0.20260802.0-rc.1

export C2GO_TARGET=aarch64-apple-darwin

创建 .c2go/input.c:

#include <stdint.h>
#include <c2go.h>

c2go_extern int add(int a, int b) {
    return a + b;
}

编译 C ,并生成汇编和 manifest:

c2go-clang --target="$C2GO_TARGET" \
  -fc2go \
  -fc2go-package=example.com/hello-c2go/translated \
  -O2 \
  -fc2go-emit-plan9-asm=.c2go/translated.s \
  -fc2go-emit-manifest=.c2go/translated.json \
  .c2go/input.c

生成 Go package:

mkdir -p translated

c2go-bind \
  --out=translated \
  --sidecar=.c2go/translated.json \
  .c2go/translated.s

创建 main.go:

package main

import (
    "fmt"

    "example.com/hello-c2go/translated"
)

func main() {
    fmt.Println(translated.Add(20, 22))
}

现在可以直接交给 Go 工具链:

CGO_ENABLED=0 go run .

输出:

42

这里的 c2go_extern 表示把已经定义的 C 函数导出给 Go 。C 的 int 仍然是 32 位,因此生成的实际签名是 func Add(a, b int32) int32;示例中的无类型常量可以直接传入。

和普通 C 不太一样的地方

C2Go 需要同时面对 C 的内存模型和 Go runtime ,因此明确区分 managed 与 unmanaged 数据:

这部分不是为了创造一门全新的 C 方言,而是把原本藏在 cgo glue 或人工约定里的边界信息明确交给编译器检查。

对普通使用场景,可以在需要 Go managed 语义的区域使用:

#pragma c2go managed push

/* managed C code */

#pragma c2go managed pop

无参数的 managed 默认启用全部 managed 能力;只有需要精细控制时才使用更具体的属性。

目前做到什么程度了

v0.20260802.0-rc.1 已发布以下四种 SDK:

系统 架构 Target triple
Linux amd64 x86_64-unknown-linux-goabi
Linux arm64 aarch64-unknown-linux-goabi
Windows amd64 x86_64-pc-windows-goabi
macOS arm64 aarch64-apple-darwin

当前版本对应 Go 1.25.x 。项目已经用一份经过适配的 musl 生成 c2go-libc ,并持续用 Go 测试覆盖字符串、内存、数学、文件 I/O 、动态加载等路径。

这仍然是 pre-1.0 的 RC 版本,更适合尝鲜、移植实验和帮助发现边界问题,暂时不建议不经审计就直接用于生产系统。

先把最重要的限制讲清楚

当前 C2Go 不会像 Go 编译器一样,把发生逃逸的 C 栈局部变量自动提升到堆。

如果一个局部变量的地址被保存进堆对象、全局变量、异步任务或其他会长期持有它的状态,那么函数返回或 goroutine 栈移动后,这个地址可能失效。同步调用链中,在对象生命周期内传递地址且 callee 不保留它,并不等于逃逸;问题在于指针是否可能比所属栈对象活得更久。

因此,准备发布的代码需要把独立的 c2go-lto 全程序逃逸审计设成门禁;确实需要长期保留的数据,应按所有权放入带正确类型信息的 Go heap ,或放入相应 native allocator 管理的内存。普通编译成功本身不能替代这项审计。

此外,当前还有这些明确边界:

完整说明在平台与当前限制和栈指针逃逸审计两章。

开源协议

具体权利边界以每个仓库中的 LICENSE、NOTICE 和文件头为准。

想听听 V2EX 各位的意见

目前我最想收集的不是“它能不能替代所有 cgo”——现阶段答案显然是否定的——而是真实 C 项目带来的问题:

如果你手上有一个体量适中、测试比较完整的纯 C 库,也欢迎拿它试一下。遇到编译、ABI 或文档问题,可以直接在对应仓库提 issue 。

相关链接:

2463 次点击
所在节点    Go 编程语言
8 条回复
ranxianglei
8 月 3 日
支持一下,否则一会站长就给你移动到推广了 ,别问我怎么知道的
sduoduo233
8 月 3 日
https://gitlab.com/cznic/ccgo

和这个比呢?这个项目也是把 c 项目转成纯 go 的
kkhaike
8 月 3 日
@sduoduo233 纯 go 限制有点多,之前做这个项目的目的是为了把很多 simd 移植到 go ,这个 纯 go 做不到,使用汇编的话限制就很少了。
zoharSoul
8 月 3 日
感觉挺好的
但是正文用 ai 润色后有点看不下去....
我现在看见 不是....而是.... 就头疼
383394544
8 月 3 日
感觉挺好的,但这个长文分段架构没啥人味 😂 在技术论坛的分享文应该短又真诚,长文可以放 README.md 让有兴趣的人慢慢看。
383394544
8 月 3 日
不是在说层主用 AI 写文章,只是觉得很多人现在行文风格也被 AI 文带坏了。
kkhaike
8 月 4 日
@383394544 /doge 主要 ai 确实比我写得好。。。
zhsj
8 月 15 日
建议拿 sqlite 做测试,然后和别人的比比 https://github.com/cvilsmeier/go-sqlite-bench 这里好几个 c 转 go 的项目

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1231606

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX