三十万行代码说扔就扔,Rust编译器被Zig按在地上摩擦,你敢信?
Roc团队耗时487天将30万行Rust编译器重写为Zig,新编译器在功能上与旧版持平,并解锁了热加载、零分配模式匹配、极速增量编译等特性。这次重写源于现有架构问题,选择Zig主要基于构建速度、内存控制、生态契合度和内存安全辅助等考量。实际效果显示,Zig的增量编译速度达到惊人的35毫秒,内存控制实现了零解析反序列化,虽然在内存安全方面各有优劣,但整体符合预期。Roc 0.1.0版本计划今年晚些时候发布。
功能对等里程碑达成
Roc编译器重写项目最近跨过了一个关键节点。新编译器终于能跑通Rocci Bird这个WASM-4游戏了,虽然只有不到一千行代码,但能运行它意味着新编译器已经实现了大量功能。更让人惊喜的是,用新编译器构建出的Wasm二进制文件只有31KB,而旧编译器生成的版本体积翻了一倍多。
这个里程碑意义重大。新编译器把构建体积压缩了一半还多,这对Wasm场景来说简直是质的飞跃。虽然还没到正式发布的程度,但整个团队都松了口气。毕竟一个编译器能不能用,最直接的检验标准就是能不能跑起来真实项目。
热加载和交叉编译双双拿下
新编译器在开发时自动支持热代码加载。你跑着一个Web服务器,修改代码后,下次请求进来就会自动用新代码处理。这在Python这类解释型语言里是标配,但在高性能编译语言里可不多见。
部署时跑roc build server.roc就能得到一个LLVM优化的独立二进制文件。交叉编译也简单得很,roc build --target=x64musl就能生成Alpine Linux上跑的静态二进制。而且不管你在Mac还是其他系统上执行这个命令,输出的字节都完全一致。
模式匹配里直接写插值
看一段HTTP请求处理代码:
roc
match (verb, path) {
("GET", "/users/${id}/${page}") => match page {
"" | "profile" => ok(id)
"settings" => ok(with_default(user_agent, id))
"posts/${post_id}" => ok("Post ID: ${post_id}")
_ => not_found
}
("GET", "/users/${id}") => ok(id)
("POST", "/posts/new") => created(with_default(...))
_ => not_found
}
"/users/${id}"这种语法是在模式匹配里直接做字符串插值,编译时就能保证类型安全。更离谱的是,这段代码执行时零堆内存分配。大部分支持热加载的语言,这种逻辑平均每行代码就要分配一次内存。
为什么从头重写
Roc不是系统级语言,它用引用计数做自动内存管理,还实现了Perceus优化和机会性突变。闭包捕获不堆分配,因为Roc是第一个实现多态反函数化的非学术语言。
这个反函数化系统对运行时性能帮助巨大,但实现起来极其痛苦。旧实现里的bug让人头疼,直到Ayaz Hafiz用OCaml原型证明了问题根源在编译器多个阶段的架构上。要修好就得重写大部分编译器。
恰好好几个贡献者本来也计划重写各部分,与其搞忒修斯之船式地慢慢替换,不如直接全盘重写。编译器领域从头重写其实是常态,自举编译器都这么干。虽然Joel Spolsky说过重写的代价臭名昭著,但主流编译器基本都经历过这个阶段。
为什么选Zig不选Rust
决定重写后,下一个问题是用Rust还是Zig。团队对两者都熟悉,最终选了Zig。
构建时间是重要考量。用cargo构建越来越慢,增量构建都受不了。Zig的重写预期会快得多。
内存控制方面,编译过程用各种分配器,尤其是竞技场分配器,还有大量结构体数组布局。Rust生态默认全局分配器,Zig生态到处传分配器参数,结构体数组支持也是标配。
生态相关性的考量很有趣。Rust生态整体大得多,但对我们需要的特定东西,Zig生态里反而有现成的。比如绕过LLVM C++库直接生成LLVM位码,Zig编译器里就有实现。
内存安全辅助这块,Rust把不安全代码隔离在unsafe块里。但我们用了1200个unsafe,而整个代码库30万行。Zig对处理不安全代码有更多特性支持,这正是我们需要的。
没有借用检查的日子
先说说内存安全。微软2019年有个著名报告,每年安全更新修复的漏洞里约70%是内存安全问题。但细分下来,83.6%的漏洞用Rust或Zig效果一样,都能处理越界读写、未初始化读取这些。真正有区别的是16.4%的释放后使用漏洞。
Zig的ReleaseSafe模式运行时检查能捕获释放后使用,有性能开销,可能panic。Rust的借用检查在编译期做。TigerBeetle数据库用Zig ReleaseSafe模式,刚经历了极其严格的Jepsen测试,只发现两个安全bug,跟内存安全无关。
Rust的内存安全缺口在unsafe代码里。Unsafe Rust跟Zig ReleaseFast有同样的内存不安全风险,但没有运行时检查。Deno和Rocket都用Rust,都有过内存安全CVE。我们的旧编译器里21个内存损坏bug都来自编译器生成的机器码,不是编译器自身逻辑。新Zig编译器有10个内存损坏bug,其中8个也是生成代码的问题,只有2个是编译器自身释放后使用。
构建时间的真相
Zig的zig build --watch -fincremental重建约45万行代码只要35毫秒。但Zig 0.16.0稳定版在这个代码库上有bug,修复在0.17.0里。我们决定等下一个稳定版。
看实际数据:Rust 1.85.0上冷构建32.4秒,增量10秒。Rust 1.97.0冷构建25.4秒,增量3.4秒。Rust团队18个月把增量构建从10秒降到3.4秒,已经很厉害了。但Zig的35毫秒还是甩开一个数量级,而且Zig代码量还多50%。
零解析反序列化
新编译器的磁盘缓存系统用了种技术:所有数据结构用32位索引代替指针,通常是结构体数组形式。这样数据能直接写磁盘,不用序列化成其他格式。反序列化时直接加载字节到内存,做些重定位就能用。
这种策略让roc check第二次运行时,如果源码没变,所有解析类型检查数据结构直接从磁盘跳进内存。roc test也缓存纯函数测试结果。文件级粒度,改一个文件只重做那个文件和相关依赖的工作。
这个技术有安全风险。任何索引都可能查错数组,拿到随机字节。Rust的借用检查不处理"哪个索引对应哪个数组"的问题。compact_arena crate能帮忙,但前提是你预先知道需要多少数组。我们不知道,所以用不了。
Safe Rust好用是因为假设unsafe代码少且隔离。但我们这种unsafe到处都是的情况,选个比Unsafe Rust更安全的语言更有吸引力。
生态契合度
Bun那篇文章说Rust的Drop trait能帮他们处理JavaScript互操作挑战。但我们正好相反,Drop是痛点,因为Rust生态默认全局分配器和隐式释放。我们要的是每个模块和编译阶段分开的竞技场分配器。Zig生态到处传分配器,正合我们意。
LLVM是优化器的关键依赖,但API经常大改。升级LLVM版本是Roc的痛苦来源。但LLVM有稳定的位码格式,自己写个位码序列化器就能绕过API不兼容。我只见过一个实现:Zig编译器里。新Roc编译器直接复用了这段Zig代码。
想念Rust的地方
测试里自动分配释放让我想念。Zig测试里要写很多初始化释放代码,Rust完全不用。
参数多态和特设多态也让人想念。Zig用comptime处理类似事情,但Rust的Allocator trait用起来更顺手。
私有结构体字段也没有。虽然理解设计理由,但看到diff里有人直接访问不该访问的字段,还是希望编译报错。
偶尔还怀念所有函数变量常量都用snake_case。
喜欢Zig的地方
没有宏这一点特别好。虽然想念特设多态,但comptime和普通函数能解决大量问题。
对数据布局的控制太棒了。u7和u5这种非2的幂次整数类型开箱即用,压缩结构体也是。内联函数可以在调用点而不是声明点指定。
构建工具链无可匹敌。Uber用Zig构建工具但不用Zig语言本身。构建Alpine Linux和Wasm的自包含二进制极其顺畅。
错误处理策略深得我心。堆分配失败是普通用户态错误,而不是panic。Roc也有类似的错误累积策略。
Roc下一步
新编译器0.1.0版本计划今年晚些时候发布,这是Roc第一个带编号的正式版本。现在可以试Nightly构建,但还有各种bug和不完整功能。
Roc编程语言基金会是501(c)(3)非营利组织,捐款在美国可以抵税。如果你知道有组织想赞助我们的工作,欢迎联系。
回头看这18个月,487天的重写比Bun从Zig到Rust的11天慢了476天。但我们的重写本来就要做大量架构变更,不是直接移植。不同项目需求不同,Bun的挑战跟Roc完全不一样。对我们来说,Zig的选择带来了预期中的收益,构建速度、内存控制、生态契合度都符合期待。
期待0.1.0版本的发布,那将是Roc真正走向成熟的第一步。
这次重写告诉我们,技术选型没有放之四海皆准的答案,只有最适合具体场景的选择。
原文期刊 / 2026年7月17日 / How Our Rust-to-Zig Rewrite is Going / Roc编译器团队 /