我们的Rust到Zig重写进展如何

Lobsters Hottest 新闻

摘要

Roc编译器团队已将他们30万行Rust代码库重写为Zig,经过18个月实现了功能对等,生成了更小的WebAssembly二进制文件并提高了性能。

<p><a href="https://lobste.rs/s/axdfjx/how_our_rust_zig_rewrite_is_going">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/07/16 11:56

# 我们 Rust 到 Zig 重写的进展 来源:https://rtfeldman.com/rust-to-zig --- 在过去一年半的时间里,构建 Roc (https://www.roc-lang.org/) 编译器的团队一直在将我们 30 万行 Rust 代码重写为 Zig (https://ziglang.org/),原因我会在下面简要回顾。最近我们刚刚达成一个激动人心的里程碑:实现了与原编译器功能对等! 鉴于 Bun 项目最近分享了一篇经验报告 (https://bun.com/blog/bun-in-rust),关于他们反向重写(从 Zig 到 Rust,尽管这只是我们重写差异的冰山一角)的经历,现在似乎是时候反思一下我们从 Rust 迁移到 Zig 的进展如何了。 ## 达到功能对等 (https://rtfeldman.com/rust-to-zig#passing-feature-parity) 达成这个里程碑意味着我们可以将 Brendan Hansknecht (https://github.com/bhansconnect) 制作的那款可爱的 2024 年 WASM-4 (https://wasm4.org/) 游戏——Rocci Bird (https://github.com/lukewilliamboswell/roc-wasm4/blob/d4161199b0a8afd55d24c30dae304b8a0358f433/examples/rocci-bird.roc)(美术由 Luke DeVault 提供)——迁移到新编译器上。这是一个很好的例子,因为整款游戏不到一千行 Roc 代码,你可以在 itch.io (https://itch.io/) 上玩,也可以直接通过 WebAssembly (https://webassembly.org/) 在这里玩: 点击或轻触游戏,然后按空格键(或轻触)来拍翅。在移动设备上没有右箭头键,所以刷新页面即可重新开始游戏。 Rocci Bird 的更新后源代码 (https://github.com/lukewilliamboswell/roc-wasm4/blob/4ec6b695d66530bc9de41bc80b112038e3c1ea12/examples/rocci-bird.roc) 比原版 (https://github.com/lukewilliamboswell/roc-wasm4/blob/a769ade51cbd4613b4fca468764c9034f9c8070c/examples/rocci-bird.roc) 更简洁一些,而且 `roc build --opt=size` 现在会输出一个 31KB 的 wasm 二进制文件。(原编译器生成的二进制文件大小是它的两倍以上。)Rocci Bird 绝不是一个庞大的代码库,但要让它能运行,需要在新编译器中实现大量功能。当我们最终达成目标时,看到那些笨重的紫色像素,我不禁露出了笑容! 需要说明的是,这是一个里程碑,但并非正式发布。(我们计划今年晚些时候推出 0.1.0 版本。)尽管如此,能达成这一里程碑还是非常棒的,我非常感激所有齐心协力促成此事的人!我想特别感谢一些在语言和编译器发展到这一步中给予特别帮助的人: - Anthony Bullard (https://github.com/gamebox) 和 Sam Mohr (https://github.com/smores56) 合作开发了新的解析器 - Jared Ramirez (https://github.com/jaredramirez) 负责新的类型检查器(以及其他许多工作!) - Ayaz Hafiz (https://github.com/ayazhafiz/) 负责新的 lambda 集解析系统 (https://github.com/ayazhafiz/cor/),以及原编译器的大量工作 - Aurélien Geron (https://github.com/ageron) 手动更新了他最初创建的 Roc Exercism 课程 (https://exercism.org/tracks/roc) 中的 108 个入门练习 - Stephan (https://github.com/stephdin) 让编译器的新“echo”平台在浏览器中运行,现在任何人都可以从 roc-lang.org (https://roc-lang.org/) 主页通过一个 2.5MB 的 WebAssembly 二进制文件来编写并运行基本的 Roc 程序! - Niclas Åhdén (https://github.com/niclas-ahden),Roc 最高产的生产用户,耐心地提交了有用的 bug 报告,并针对升级过程提供了可操作的反馈 - JRI98 (https://github.com/JRI98) 系统地重现和调查 fuzzer 错误及其他 bug,关闭不再重现的问题等 - Jasper Woudenberg (https://github.com/jwoudenberg) 在使用新编译器的用户空间包上迭代 API 设计 - Folkert de Vries (https://github.com/folkertdev)、Brendan Hansknecht (https://github.com/bhansconnect)、Brian Carroll (https://github.com/brian-carroll)、Josh Warner (https://github.com/joshuawarner32)、Agus Zubiaga (https://github.com/agu-z) 和 Jelle Teeuwissen (https://github.com/JTeeuwissen) 构建了原编译器的基础,没有他们,新编译器根本不会存在 - 最后,我把新编译器无可争议的最大贡献者留到了最后:Anton-4 (https://github.com/Anton-4) 和 Luke Boswell (https://github.com/lukewilliamboswell/)——他们做的事情多到我数不过来:编译器工作、内置函数、平台、包、示例、修复 bug、在 Roc Zulip 上帮助初学者……列出来可以再写一整篇文章!看到你们取得的成就,真是令人难以置信。 非常感谢大家!我很荣幸你们为这个项目投入了如此多宝贵的时间。还要感谢我们过去和现在的赞助商——rwx (https://www.rwx.com/)、Lambda Class (https://lambdaclass.com/)、ohne-makler (https://www.ohne-makler.net/)、martian (https://withmartian.com/)、tweede golf (https://tweedegolf.nl/)、Vendr (https://www.vendr.com/)、NoRedInk (https://www.noredink.com/) 以及许多慷慨的个人赞助者 (https://github.com/sponsors/roc-lang/)——他们通过支持我们的贡献者 (https://roc-lang.org/donate) 帮助我们走到了今天。 说到时间:我们为期 487 天的重写比 Bun 的 11 天重写 (https://bun.com/blog/bun-in-rust)(从他们大约 50 万行 Zig 重写为 Rust)多花了 476 天。这种差异有很多原因,与 Rust 或 Zig 无关,包括他们的重写是直接移植,而我们之所以决定重写,正是因为我们要做大量改动。他们使用的技术 (https://bun.com/blog/bun-in-rust#claude-rewrite-bun-in-rust) 在我们的案例中行不通。 我们所做的改动清单很长,也意味着比较我们原来的 Rust 代码库和新的 Zig 代码库并不完全对等。尽管如此,我们已经到了一个不错的节点,可以反思重写进展如何,既包括它为 Roc 程序员解锁了哪些新功能,也包括我们对 Rust 和 Zig 的体验对比。 让我们开始吧! ## 热代码加载 + 交叉编译二进制 (https://rtfeldman.com/rust-to-zig#hot-code-loading-cross-compiled-binaries) Roc 的新编译器在开发过程中会自动进行热代码加载。例如,我可以运行 `roc server.roc` 启动一个 Web 服务器,然后在它运行时修改一些代码。下次该服务器处理请求时,将会自动使用新代码来处理。以下是在服务器和简单 2D 游戏中的实际演示: 下载热加载演示视频。 (https://rtfeldman.com/assets/hot-loading.mp4) 热加载对于像 Python 这样的解释型语言来说是标准行为,但对于像 Roc 这样的高性能编译型语言来说却并不常见。当我准备部署时,`roc build server.roc` 会得到一个 LLVM 优化过的、自包含的二进制文件,我可以直接放到机器上运行。 Roc 还支持交叉编译;构建一个能在 Alpine Linux 上运行的静态二进制文件只需 `roc build --target=x64musl`,而且这个命令在 Mac 或其他任何系统上运行时,都会为相同的输入源代码字节产生相同的输出字节——并非所有编译器都能保证这一点 (https://xeiaso.net/notes/2026/anubis-wasm-vendor-binary/)。 ## 带字符串插值的模式匹配 (https://rtfeldman.com/rust-to-zig#pattern-matching-with-string-interpolation) 那个视频中的 HTTP 请求处理逻辑如下: `` 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}"` 这种语法并非通过运行时解析模板字符串 (https://expressjs.com/en/guide/routing/#route-parameters) 实现的,而是一个新的语言特性:模式匹配中的字符串插值。 这不仅在编译时是类型安全的,而且整个代码片段执行了零堆分配 (https://nnethercote.github.io/perf-book/heap-allocations.html)。我预计通常带有热代码加载的编程语言在这里平均每行代码会分配接近 1 次……但 Roc 在人体工程学、类型安全和性能方面都志存高远! 你可以在新的 roc-lang.org (https://www.roc-lang.org/) 主页上尝试这种语法——向下滚动一点,页面上就有一个编译器的 WebAssembly 构建版本,你可以用它来试试这门语言。 > 顺便说一句,如果你对关于我们如何利用新编译器对纯函数进行编译时执行,从而将 HTTP 请求路由降至零分配的技术细节感兴趣,请在 Roc Zulip (https://roc.zulipchat.com/) 上告诉我。 ## 为什么要从头重写? (https://rtfeldman.com/rust-to-zig#why-a-scratch-rewrite) 与 Rust、C 和 Zig 不同,Roc 不是系统语言;它具有自动内存管理(使用引用计数,既是为了避免追踪式收集器的停顿,也是为了利用 Perceus 优化 (https://www.microsoft.com/en-us/research/wp-content/uploads/2020/11/perceus-tr-v4.pdf) 和机会性突变(类似于 Koka (https://koka-lang.github.io/koka/doc/book.html#sec-fbip) 的做法))。如果 Roc 每次闭包捕获都需要一次堆分配(像大多数非系统语言那样),那么它的堆分配会多得多,但我们的闭包捕获不进行堆分配,因为 Roc 是第一个实现通过 lambda 集特化进行多态去函数化 (https://www.cs.princeton.edu/~mpmilano/publication/lss/) 的非学术语言。 这听起来像是一个小众优化,但在像 Roc 这样的函数式语言中,去函数化结果类似于内联 (https://en.wikipedia.org/wiki/Inline_expansion),因为它能解锁大量后续优化。虽然这个系统对 Roc 的运行时性能极为有利,但对我们来说实现正确却异常困难。我们在原始实现中与棘手的 bug 斗争了很久 (https://shows.acast.com/software-unscripted/episodes/664fde448c77cc0013b3338a),直到 Ayaz Hafiz 在 OCaml (https://github.com/ayazhafiz/cor/) 中原型设计了一个新架构后,我们才终于能在新编译器中把它做对。 Ayaz 的原型表明,我们问题的根源在于跨越多个编译阶段的架构,而修复它需要重写编译器的大部分内容。这就是我们最初决定重写的原因之一——另外,几位贡献者也独立提到他们打算出于其他原因重写编译器的各个部分。我们意识到反正也要重写几乎整个编译器,所以考虑全面重写作为替代忒修斯之船 (https://en.wikipedia.org/wiki/Ship_of_Theseus) 方法的方案是有意义的。 编译器在这方面很特别:从头重写在成功的项目中是常态。这通常是实现自托管 (https://en.wikipedia.org/wiki/Self-hosting_(compilers)) 的唯一方式,尽管并非所有编译器都重写为自己的语言;例如 TypeScript 重写为 Go (https://devblogs.microsoft.com/typescript/typescript-native-port/)。我的立场一直是 Roc 的编译器不应该自托管 (https://www.roc-lang.org/faq#self-hosted-compiler),所以坦白说,我从未想过有一天重写的好处可能会超过其臭名昭著的代价 (https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/)。 我们讨论得越多,就越觉得应该做当今几乎所有主流编译器都做过的事情:从头重写。 ## 为什么选择 Zig? (https://rtfeldman.com/rust-to-zig#why-zig) 一旦我们决定从头重写,下一个问题就是是否再次选择 Rust。基于我们对 Rust 和 Zig 的体验(我们已经在标准库中大量使用 Zig (https://www.youtube.com/watch?v=jIZpKpLCOiU) 作为原语),我们决定这次用 Zig 构建整个编译器 (https://gist.github.com/rtfeldman/77fb430ee57b42f5f2ca973a3992532f)。 我喜欢 Rust,教过一门相关课程 (https://frontendmasters.com/courses/rust/),并且每天在工作中愉快地使用它(在 Zed (https://zed.dev/))。尽管互联网评论可能让我们相信,但一种语言最适合一个项目,而另一种语言最适合另一个项目,这是极其正常的。没有放之四海而皆准的解决方案! 我已经在其他地方深入讨论过我们选择 Zig 的原因——在文章中 (https://gist.github.com/rtfeldman/77fb430ee57b42f5f2ca973a3992532f)、在播客 (https://www.youtube.com/watch?v=E0n82muHMcM) 中等等——而且我们只认真考虑过 Rust 和 Zig,因为那是我们团队足够熟悉的仅有系统语言。在决定 Rust 和 Zig 时,我们考虑的最重要因素是: - **构建时间。**我们的 `cargo` 构建时间是一个主要的痛点,即使是增量构建,而且随着代码库的增长越来越糟。我们预计 Zig 重写后的构建时间会快得多。 - **内存控制。**我们在整个编译过程中使用多种不同的内存分配器,尤其是 arena(区域分配器),并且到处使用结构体数组(struct-of-arrays)布局。Rust 的生态系统始终假设有一个全局分配器,包括 soa_rs (https://docs.rs/soa-rs/latest/soa_rs/)。而 Zig 的整个生态系统都假设使用细粒度分配器,并且结构体数组支持是标准功能。 - **生态相关性。**总体而言,Rust 的生态系统比 Zig 大得多……但这两个生态系统中几乎没有一个包与我们特定的需求相关。对于我们希望直接获取的小众功能——比如比包装 LLVM 的 C++ 库更快地发出 LLVM 位码的方法——Zig 中存在的此类代码比 Rust 中更多。 - **内存不安全协助。**Rust 旨在将内存不安全代码隔离在罕见的 `unsafe` 块中,并使用像 miri (https://github.com/rust-lang/miri) 或 Valgrind (https://valgrind.org/) 这样的工具来审查它们。但对我们来说,内存不安全代码并不罕见(稍后详述),我们最终在 30 万行 Rust 代码中使用了大约 1200 次 `unsafe`(作为对比,rust (https://github.com/rust-lang/rust/) 在 350 万行代码中使用了大约 4 万次 `unsafe`;请记住,对于像 `roc` 和 `rustc` 这样发出机器代码的编译器,做内存不安全的事情是工作的很大一部分)。Zig 有比 Rust 更多的功能来使内存不安全代码正确工作 (https://zackoverflow.dev/writing/unsafe-rust-vs-zig/),而这正是我们最需要帮助的领域。 经过一年半的重写,我们对 Zig 优势的期望与现实情况是否吻合?我们失去了 Rust 的哪些功能,现在我方再也无法使用它们了? ## 没有借用检查的生活 (https://rtfeldman.com/rust-to-zig#life-without-borrow-checking) 首先从内存安全开始。有一份著名的 2019 年微软演示文稿 (https://github.com/Microsoft/MSRC-Security-Research/blob/master/presentations/2019_02_BlueHatIL/2019_01%20-%20BlueHatIL%20-%20Trends%2C%20challenge%2C%20and%20shifts%20in%20software%20vulnerability%20mitigation.pdf) 在第 10 页中指出: > 每年通过安全更新解决的安全漏洞中,约 70% 仍然是内存安全问题。 演示文稿的下一页按内存安全问题的类型进行了细分,具体到 Rust 和 Zig 的情况如下: - 2018 年通过安全更新解决的漏洞中,83.6% 完全不受 Rust 或 Zig 选择的影响,因为这两种语言都会处理所有这些场景。

相似文章

2026 年的 Zig 与 Rust

Lobsters Hottest

本文在 2026 年的背景下对比了 Zig 和 Rust,认为编程代理通过自动化生成 Rust 代码,削弱了 Zig 在人机交互体验上的优势。

Bun 的 Rust 重写已合并

Lobsters Hottest

Bun,JavaScript 运行时和包管理器,已合并其核心从 Zig 到 Rust 的重写,可能提升性能和可维护性。

重返Zig

Lobsters Hottest

作者描述了从Zig到Rust再回到Zig的历程,探讨了编程语言中稳定性与表达力之间的权衡。

Zig 构建速度正在提升

Mitchell Hashimoto

Zig 0.15 相比 0.14 在编译时性能有显著提升,构建脚本编译时间从约 7 秒降至约 1.7 秒,完整构建时间从 41 秒降至 32 秒,且仍使用 LLVM。本文重点介绍了自托管后端和增量编译方面的进展。

以Zig风格构建你的项目

Lobsters Hottest

作者详细介绍了构建一个名为bygge-zig的工具,该工具使用Zig构建系统来编译Rust项目,用更少的代码行复制了Cargo的功能,并突出了其中的差异和挑战。