从 Rust 到 Zig:一次系统编程语言迁移的真实账本

从 Rust 到 Zig:一次系统编程语言迁移的真实账本

RustZig系统编程语言迁移编译软件工程

数据源:HN + Lobsters + web research · HN

2026 年 7 月 15 日,Roc 语言作者 Richard Feldman 发布了一篇名为《How Our Rust-to-Zig Rewrite is Going》的文章,宣布 Roc 编译器的 Zig 重写版本已达到与原 Rust 版本的功能对等。487 天,30 万行 Rust 代码迁移为 Zig,这是项目的一个里程碑——但远不止于此。

几乎同一时间,Bun 团队分享了他们从 Zig 到 Rust 的重写经验,仅用了 11 天。两个项目在系统编程语言之间做出了相反的选择,而各自的理由都站得住脚。这一巧合让整个技术社区陷入了关于「哪种系统编程语言更好」的热烈讨论,但真正有价值的问题可能是:在什么条件下,选择 A 比选择 B 更合理?

为什么重写?

Roc 是一种函数式编程语言,其编译器最初用 Rust 编写。Feldman 列出的重写动机并不指向 Rust 本身的缺陷,而是指向编译器架构层面的问题。

核心痛点来自「多态去函数化」(polymorphic defunctionalization)——一项通过 lambda set specialization 实现闭包零堆分配的关键优化。原实现在 Rust 中暴露出跨多个编译器阶段的架构问题,修复它意味着重写编译器的大部分。同时,多个贡献者出于不同的原因已经在计划重写各自负责的模块。团队意识到与其走「忒修斯之船」路线,不如从零开始。

Feldman 对此有过一段坦率的自白:「我一直认为 Roc 的编译器不应该自举(self-host),所以『重写的收益可能超过其臭名昭著的成本』这个念头,老实说我从来没想过。」但当他发现几乎所有主流编译器在历史上都经历过从零重写后,这个决定变得合理了。

这里有一个容易被忽略的前提:Roc 团队决定重写时,Rust 的表现并不是触发因素——真正的触发器是架构层面的全面改造需求。当他们意识到无论如何都要重写大部分代码后,语言选择才成为一个开放式问题。

为什么是 Zig?

团队只在 Rust 和 Zig 之间做了认真考量,因为只有这两种系统语言是团队成员足够熟悉的。Feldman 在文章和过往的播客中详细讨论过四个决策维度:

编译速度。 Cargo 的构建时间是 Roc 团队的长期痛点,尤其在增量编译场景下。代码库越大,问题越严重。Zig 的 -fincremental 承诺将增量构建压缩到毫秒级,这在理论上是一个数量级的飞跃。

内存控制。 Roc 编译器大量使用 arena 分配器和 struct-of-arrays 布局。Rust 生态几乎一致假设全局分配器(包括 soa_rs),而 Zig 的整个生态都围绕可传递的粒度分配器设计。对于需要为每个模块和编译阶段维护独立 arena 的编译器来说,Zig 的默认范式更接近实际需求。

生态相关性。 Rust 的包生态远比 Zig 庞大,但对编译器开发这个垂直场景来说,两边可用的现成代码都不多。而团队真正需要的东西——比如一个解耦于 LLVM C++ 库的手写 LLVM bitcode 序列化器——恰好存在于 Zig 编译器的源码中。Feldman 写道:「当我在 2019 年写下编译器的第一行代码时,我不会猜到这一点会成真:『未来,这个项目最丰富的可复用代码金矿,将是一个用你还没听说过的语言编写的开源编译器。』」

内存不安全代码的辅助。 Rust 的设计哲学是将 unsafe 隔离在少数关键区域,然后用 miri 或 Valgrind 审查。但 Roc 编译器的 unsafe 使用量远超典型 Rust 项目——约 1200 处 unsafe(30 万行代码)。相比之下,rustc 本身的 350 万行代码中约有 4 万处 unsafe 出现(不过这个数字包含了测试和注释,后文会谈到社区对此的争议)。Feldman 的判断是:当 unsafe 不可避免地在代码中普遍存在时,Zig 的运行时安全检查(ReleaseSafe 模式)比 Rust 的「隔离 + 审查」策略更有吸引力。

拿到了什么?付出了什么?

编译速度:理论很美,现实还在等

这是最微妙的结论。Feldman 给出了以下数据:

版本代码行数冷构建增量构建
原 Rust 编译器 (1.85.0)354K32.4s10.0s
原 Rust 编译器 (1.97.0)354K25.4s3.4s
Zig 重写 (0.16.0, 功能对等)320K39.6s8.6s
Zig 重写 (0.17.0, 当前)464K32.1s0.035s

Zig 的 -fincremental 在 0.17.0 预发布版上确实做到了 35 毫秒的增量构建——比 Rust 快 100 倍。但问题在于,Zig 0.16.0 稳定版存在一个 bug,导致 -fincremental 在 Roc 的代码库上无法工作。团队选择等待下一个稳定版发布,这意味着在功能对等的那个时间点上,Zig 的构建速度实际上比 Rust 更慢。

Feldman 对此态度务实:「如果我们在同样 18 个月内继续使用 Rust,增量构建时间也会从 10 秒降到 3.4 秒——Rust 贡献者在编译性能上的投入令人敬佩。」但他同时指出,35ms 和 3.4s 属于不同的性能级别,而他从未听说过 Rust 有任何与 -fincremental 可比拟的路线图规划。

前提条件:这个优势目前只在 x86-64 Linux 上可用,ARM Mac 尚未支持。如果你的团队主力开发机是 MacBook,这个优势暂时与你无关。

内存安全:编译器特有的问题

Roc 编译器在两个版本中都记录了因内存损坏导致的 bug:

指标RustZig
内存损坏类 bug2110
非内存损坏类 bug2,575421
总计2,596431

初看之下,Rust 版本的内存损坏 bug 数量是 Zig 的两倍多。但 Feldman 做了一个关键的拆解:这 21 个 Rust 版本的内存损坏 bug 全部是编译器产生的错误机器码导致的(miscompilation),没有一个发生在编译器的自身逻辑中——这恰恰证明了 Rust 的 borrow checker 在工作。而 Zig 版本的 10 个内存损坏 bug 中,8 个是 miscompilation,2 个是编译器自身的 use-after-free 错误(都发生在错误报告的文件名渲染中,症状是文件名显示为乱码)。

Feldman 的评估直白而冷静:如果当初选择了 Rust,那 2 个 use-after-free 会被 borrow checker 拦截;如果选择了 Zig 的 ReleaseSafe 模式,它们会在运行时 panic。但三种选择的实际影响差异——「两个 bug 报告:错误消息中的文件名渲染失败」——对项目来说微乎其微。

这里有一个值得注意的对照:Bun 团队在反向迁移(Zig→Rust)时提到,对于需要同时管理垃圾回收值和手动管理内存的项目,use-after-free 和 double-free 是「大量 bug 的来源」。Feldman 承认这一点,但指出 Roc 编译器不涉及 JavaScript 互操作或追踪式 GC,因此这个痛点不适用。不同的项目有不同的需求。

零解析反序列化:索引替代指针的红利

Roc 编译器的新缓存系统借用了 Zig 编译器的一项技术:所有编译器数据结构使用 32 位索引而非指针,并以 struct-of-arrays 形式组织。这样做的直接收益是:

  • 内存占用更小,访问更快(对现代 CPU 缓存友好)
  • 数据结构可以直接写入磁盘,无需序列化为中间格式
  • 反序列化时只需 mmap 加载字节、做少量重定位,实际速度受限于 I/O 而非解析

这意味着 roc check 的第二次运行可以几乎瞬间完成——所有已解析和类型检查的数据结构直接从磁盘跳入内存。Feldman 说这项技术来自游戏编程的常见实践,并通过 Zig 编译器首次接触到。

但索引系统带来了安全代价:就像指针可能指向错误地址一样,索引可以被错误地用于查找不匹配的数组,导致读取到随机数据。Rust 的 borrow checker 不解决「哪个索引对应哪个数组」的问题——这从来不在它的设计范围内。在编译时数组数量无法预知(取决于模块数量)的场景下,Rust 的 compact_arena 等工具也无能为力。

社区争议:三场有代表性的辩论

「unsafe 到底有多普遍?」

Rust 核心团队成员 Ralf Jung(ralfj)在 Lobsters 讨论中直接质疑了 Feldman 引用的「rustc 有 4 万处 unsafe」数字。他指出,这个数字是 unsafe 关键词在整个 Rust 标准库和编译器中出现的次数,包括测试和注释。实际编译器中 unsafe 的使用不足 2000 处,且其中大部分属于标准库(这是任何语言都需要的底层运行时)。

Feldman 的回应承认了这个区分,但指出 Roc 的代码库也混合了编译器和标准库代码,因此他的对比是同类比较。更大的问题在于:为什么 Roc 编译器需要比 rustc 多这么多的 unsafe? 他将其归因于缓存系统和索引替代指针的架构选择。

「编译器真的需要大量 unsafe 吗?」

steveklabnik(Rust 文档团队前成员)在 HN 上质疑了 Feldman 的另一句话:「对于像 roc 和 rustc 这样生成机器码的编译器来说,做内存不安全的事情是工作的主要部分之一。」

steveklabnik 认为这不准确:「生成机器码本身不需要 unsafe——那只是把字节写下来。只有当你执行这些机器码时才有潜在的不安全性。」Feldman 同意了这个区分,但同时指出大多数编译器在实践中既生成机器码也执行它(编译时求值、运行测试、热代码加载等),所以他的表述反映的是实践而非理论极限。

两人最终达成共识:const fn 的解释执行不需要 unsafe,但热代码加载和运行时测试执行确实需要。

「这个重写能推广吗?」

HN 上有多条评论指向同一个问题:Roc 团队的选择有多大程度的可推广性?一条高赞评论指出,Zig 的增量构建确实是「杀手级特性」,但质疑 Rust 是否会在中短期内补齐这个差距。另有评论认为,对于大多数不需要编写编译器的团队来说,Rust 的生态成熟度和 borrow checker 的安全保障仍然是不可替代的优势。

Lobsters 上的讨论更偏技术,有多位评论者提供了在 Rust 中实现类似索引优化和 arena 分配的具体方案,认为 Feldman 对 Rust 能力的评估偏保守。

什么场景下这种迁移是合理的?

从 Feldman 的文章和社区讨论中,可以归纳出几条前提条件。这些条件衡量的是迁移的收益/成本比是否倾向合理一侧:

迁移可能是合理的选择,当且仅当:

  • 你的代码已经在计划大规模重写(架构性调整,而非仅仅是语言切换)
  • 你的 unsafe 使用密度远超典型 Rust 项目,使得「隔离审查」策略失效
  • 你对内存布局有精细控制需求(多 arena、struct-of-arrays、零拷贝序列化)
  • 编译速度是你的日常工作流瓶颈,且你的目标平台支持 Zig 的增量编译
  • 你需要的核心依赖恰好在 Zig 生态中存在,而 Rust 生态中没有等价物

迁移可能不值得,当且仅当:

  • 你的项目重度依赖 Rust 的 trait 系统和泛型抽象
  • 你需要严格的 SemVer 兼容性保证(Zig 目前明确不以向后兼容为目标)
  • 你的 unsafe 代码是极少数且隔离良好的,borrow checker 覆盖了绝大部分代码
  • 你的项目涉及 JavaScript GC 互操作或类似的混合内存管理(此时 Rust 的 Drop 和 borrow checker 反而是优势)
  • 团队规模较大,需要编译器强制执行的接口契约

元观察:Feldman 这篇文章本身的价值

抛开 Rust vs Zig 的技术辩论,这篇文章在社区引起广泛讨论的一个原因是它的写作方式。Feldman 既列出了选择 Zig 的理由,也坦率地记录了 Zig 不如 Rust 的地方——从他怀念的 Rust trait 系统和私有字段,到对 Zig 缺乏死代码检测的遗憾,再到对 Rust 向后兼容性升级体验的怀念。

他在文章结尾写道:「我可以一边怀念 borrow checker 带来的那种『只有 unsafe 块里才需要担心某些问题』的安心感,一边不愿意在这个项目中为它支付相应的成本。」这种不站队的态度,在系统编程语言的「宗教战争」氛围中是稀缺的。

对于 Roc 编译器的下一个里程碑——计划在今年晚些时候发布的 0.1.0 版本——Feldman 表示「无比期待」。在 -fincremental 的 35 毫秒承诺兑现之前,他和团队选择继续等待下一个 Zig 稳定版。

本文的素材来自公开信息和社区讨论。如果你对这个话题有更深入的一手经验,欢迎指出文中的不足。


参考链接:

  • Richard Feldman,《How Our Rust-to-Zig Rewrite is Going》
  • Hacker News 讨论:《How Our Rust-to-Zig Rewrite Is Going》
  • Lobsters 讨论:《How Our Rust-to-Zig Rewrite is Going》