日期:2026-07-10
主题:Bun 从 Zig 重写为 Rust:JavaScript 运行时的技术转向
原文:Rewriting Bun in Rust
来源:Bun Blog (via Lobsters)
领域:💻 编程语言
2026 年 5 月,Bun 团队做了一件在 JavaScript 生态中前所未有的事:将整个运行时从 Zig 全面重写为 Rust。这不仅仅是一次简单的语言更换——11 天,64 个 Claude 并行工作,6,502 次提交,从头到尾重写了一个百万行级别的运行时。Bun v1.3.14 成为了最后一个基于 Zig 的版本,而 v1.4.0 标志着 Bun 正式进入 Rust 时代。
Bun 最初选择 Zig,看中的是其简洁性、强大的 comptime 元编程能力,以及零成本的 C ABI 互操作——这对于绑定 JavaScriptCore 这样的 C++ 引擎至关重要。但随着项目从小型实验成长为服务数百万开发者的生产级运行时,Zig 的某些设计取舍开始暴露为持续性的技术债务。
Zig 的内存管理方式是该语言引以为傲的卖点——没有隐式控制流,没有隐藏的内存分配,一切手动可控。但在百万行项目中,这种"自由"变成了沉重的负担。
defer 的局限性。 Zig 使用 defer 关键字在作用域结束时执行清理代码。听起来没问题,但每条函数可能有多个退出路径,每一条都要确保 defer 覆盖到位。漏写就是内存泄漏,多写就是 double-free。Bun 团队发现,大量难以追踪的内存 bug 正是源于此。
Rust 的 Drop trait 则完全不同。当一个值离开作用域时,编译器自动插入清理代码:
impl Drop for Bytes {
fn drop(&mut self) {
if !self.pinned.is_empty() {
JSC__JSValue__unpinArrayBuffer(self.pinned);
}
}
}
在 Zig 中,同样的逻辑需要在每个调用点手动写 defer bytes.unpin()。Drop 将清理从"程序员的责任"变成了"编译器的保证",这是本次重写最核心的收益之一。Rust 重写后,Bun 改进了 LeakSanitizer 集成,追踪所有原生代码的内存分配,修复了每一个可检测的内存泄漏。一个典型例子:Bun.build() 在 Zig 版中每次调用泄漏约 3 MB,跑 2,000 次累积到 6.7 GB;Rust 版稳定在 609 MB。
comptime 的双刃剑。 Zig 的 comptime 元编程非常强大,但也容易过度使用。Bun 的 Zig 代码在编译期展开了大量逻辑,导致二进制体积膨胀。转入 Rust 后,配合链接器优化(Identical Code Folding)和 ICU 数据懒加载(zstd 字典按需解压),Bun 的二进制体积缩减了约 20%——Linux 从 88 MB 降到 70 MB,Windows 从 94 MB 降到 76 MB。
栈空间的浪费。 Zig 编译器不会发射 LLVM 的 llvm.lifetime.start/end 标记,导致 LLVM 无法在函数内复用栈空间槽位。Bun 团队过去不得不手工将特别大的函数拆成多个小函数来绕开这个限制。Rust 天然支持这些生命周期标记,TOML、JSON、YAML 等递归下降解析器的栈使用量大幅下降。Zig 版中 25,000 层嵌套的 TOML 解析会栈溢出崩溃,Rust 版能顺利完成并返回值。
这次重写最引人注目的不仅是技术决策本身,还有其执行过程。Bun 创始人 Jarred Sumner 使用 Claude Code 的动态工作流功能,调动了最多 64 个 Claude 实例并行工作:
bun --version 能跑,到 bun test <file>,再到全测试套件通过。经历了多次 false start——Claude 倾向于用"存根化"来让编译通过,而不是真正修复逻辑。最终消耗:约 69 亿 uncached 输入 token、6.9 亿输出 token、720 亿 cached token 读取,API 成本约 16.5 万美元(使用预发布的 Claude Fable 5 模型)。Sumner 估算,如果让 3 个熟悉代码库的工程师手动完成,至少需要一年,且期间无法推进其他功能开发。
零测试删减。 重写后在所有平台上运行的测试数量和文件数与 Zig 版完全一致:Linux x64 上有 1,386,826 个 expect 断言、60,624 个测试、4,174 个测试文件。没有跳过或删减任何测试用例。
Rust 支持 C/C++ 与 Rust 之间的跨语言链接时优化(Cross-language LTO),允许 JavaScriptCore(C++)和 Bun 的新 Rust 代码之间的函数内联——这在 Zig 中是不可能的。实测 HTTP 吞吐基准:
| 服务框架 | Zig 版 (v1.3.14) | Rust 版 (v1.4.0) | 提升 |
|---|---|---|---|
| Bun.serve | 169.6k req/s | 177.7k req/s | +4.8% |
| node:http | 103.8k req/s | 108.5k req/s | +4.5% |
| Elysia | 158.9k req/s | 163.3k req/s | +2.8% |
| Express | 64.5k req/s | 66.6k req/s | +3.2% |
| Fastify | 91.5k req/s | 95.9k req/s | +4.8% |
应用构建场景同样有 2%-5% 的提升:next build 从 13.62 秒降至 13.03 秒(+4.5%),tsc -b --force 从 0.94 秒降至 0.89 秒(+4.7%)。
团队总结了几类典型的移植错误。这些案例对任何做跨语言移植的工程团队都有参考价值:
debug_assert 的副作用。 Zig 的 assert 是函数,参数在所有构建中都执行。Rust 的 debug_assert! 是宏,release 构建中整个表达式被擦除。Bun 的 dev server 中有一个 insert_stale 调用被错误地放在了 debug_assert! 里,release 构建中这个方法不再执行,导致 React 热模块替换在特定场景下损坏。调试构建正常工作,release 构建静默损坏——这是最难排查的那类 bug。
切片长度奇偶性。 Bun 的 Zig 辅助函数 reinterpretSlice(u16, bytes) 使用 @divTrunc 忽略末尾的奇数 byte。Rust 的 bytemuck::cast_slice 会直接 panic。Blob.text() 处理 UTF-16 BOM 后跟奇数长度字节时,Zig 版正常返回字符串,Rust 版进程崩溃。最终修复:&buf[..buf.len() & !1]——显式截断到偶数长度。
边界检查。 Zig 编译 Bun 时使用 ReleaseFast 模式,移除边界检查。Rust 的 release 构建保留边界检查。Bun 的模块解析器将长文件名内联到全局列表中,Zig 版每个溢出块大小为 count / 4 或 2,048。Rust 移植时留下了一个占位符常量 64,将内联文件名上限从 840 万降到 27 万——真实项目很容易触顶,触顶后触发了一个从 Zig 移植来的 ptrs[4095] off-by-one 错误。在 Zig 中这个错误可能悄无声息越界写入,在 Rust 中直接 panic 暴露出来。
comptime 格式化字符串。 Zig 的 Output.pretty 使用 comptime fmt 参数,在编译期处理 <r>、<d> 等颜色标记。Rust 没有 comptime 参数,Output::pretty 在运行时处理标记字符串。结果 bun update -i 打印的 OSC 8 超链接中,紧跟在 ESC \ 后面的 <r> 被标记解析器错误地吞掉,r 被当做普通文本打印。修复方案是将 Output::pretty 改为 Rust 宏:bun_core::pretty!("<r>{}<r>", hyperlink)。
重写完成后,Bun 的 Rust 代码中约 4%(约 13,000 个 unsafe 关键字,分布在约 27,000 行 / 总计约 780,000 行 Rust 代码)位于 unsafe 块内。其中 78% 的 unsafe 块只有一行——要么是从 C++ 获取的指针,要么是对 C 库的调用。团队计划随着从"忠于 Zig 的机械移植"重构为"地道 Rust"而逐步减少 unsafe 的使用,但由于需要持续使用 JavaScriptCore 等 C/C++ 库,unsafe 比例将始终高于纯 Rust 项目。
Bun 从 Zig 到 Rust 的重写,本质上是"手动管理"到"编译器辅助"的范式迁移。Zig 提供了极致的控制力,但在百万行规模下,这种控制力的维护成本超过了它带来的灵活性。Rust 的 borrow checker 和 Drop 机制将常见的内存错误从运行时转移到了编译时,让 Bun 团队能够更有信心地持续迭代。
更引人深思的是这次重写的执行方式:一个工程师借助 AI 辅助工具,在 11 天内完成了传统团队需要一年才能做完的工作。当然,这离不开 Bun 原始 Zig 代码的清晰架构——AI 翻译的质量很大程度上取决于源材料的质量。Bun 合并后在 CI 中持续运行的覆盖引导模糊测试已经执行了超过 1,000 亿次解析器执行,只有约 15 个 PR 由 AI 自动提交修复。
就 Bun 本身而言,更小的二进制体积、更低的内存占用、2%-5% 的速度提升,都让它的"Node.js 替代者"叙事更有说服力。Prisma 已经在 Bun 的 Rust 版本上启动了 Prisma Compute 公测,Claude Code v2.1.181 也切换到了 Rust 版 Bun。
而对于更广泛的技术社区,Bun 的故事提出了一个值得思考的问题:当我们选择系统编程语言时,究竟应该在多大程度上权衡"控制力"和"约束力"?Bun 的答案已经很明确了。