needhelp
← 返回博客

Bun 用 Rust 重写核心:11 天、6,778 次提交,以及真正改变的东西

作者 needhelp
Bun
Rust
Zig
JavaScript 运行时
LLM
软件工程

Bun 团队在 2026 年 5 月干了一件不常见的事:他们用一份 Rust 移植版替换了运行时的整个原生核心,而且只用了 11 天。

公告最上面的声明定下了基调。Bun 在 2025 年 12 月被 Anthropic 收购。Bun 的创造者 Jarred Sumner 用了 Claude 的一个预发布版本——他们称为 Fable 5 的“Mythos 级模型”——完成了大部分工作。这不是什么思想实验。第一个 Rust 版本 Bun v1.4.0 现在已经在 canary 渠道了。

几个核心数字:

  • 535,496 行 Zig(不含注释)变成了 Rust 代码库
  • 5 月 3 日到 5 月 14 日之间,6,778 次提交
  • 1,448 个 .zig 文件被机械式移植成 .rs
  • API 花费约 16.5 万美元(59B 未缓存输入 token,6.9 亿输出 token)
  • 峰值:4 个 worktree 上同时跑着 64 个 Claude

Rust 重写后的 Bun 启动,运行在 Claude Code 下

Bun 一直是一场 Zig 的赌注

Bun 的起点是把 esbuild 的 JS/TS 转译器从 Go 逐行移植到 Zig。2021 年 4 月 16 日,Sumner 在 Hacker News 上看到只有一页的 Zig 语言参考,写下第一行 Zig。最初的版本——转译器、压缩器、打包器、兼容 npm 的包管理器、对标 Jest 的测试运行器、Node.js 的 API 表层——是一个人用一年时间写出来的,地点是奥克兰一间拥挤的公寓,那时候 LLM 还干不了这种活。

这场赌注赢了。Bun 的 CLI 现在每月下载量超过 2,200 万次。Claude Code 和 OpenCode 把它当作自己的运行时。Vercel、Railway、DigitalOcean 都提供官方支持。

但这么大的范围本身也是问题。

欠下的债:GC 内存挨着手动内存

JavaScript 是带垃圾回收的。Bun 内嵌了 JavaScriptCore(Safari 用的引擎),还有一堆 C/C++ 库:uWebSockets、BoringSSL、SQLite、lsquic。Zig 和 C 一样,不替你管理内存。Bun 的工作就是夹在带 GC 的语言和手动管理内存的原生代码之间,而这条边界正是它大多数严重 bug 滋生的地方。

公告里列了一串 v1.3.14 里修掉的 bug 作为例子:node:zlib 里的堆上 use-after-free、node:http2 里因重入回调导致的 use-after-free、UDPSocket.sendMany 里的越界写、每次 tls.connect 都会漏的内存、CSS 解析器里的 double-free。名单还在继续,而且都很技术。

Sumner 说得很明确:这不是 Zig 的错。“要不是 Zig,我们走不到今天,这一点我永远感激。“问题在结构上:在一个带 GC 的边界上处理生命周期,对任何不为这个场景设计的语言都很难;而 Zig 给出的答案——在每个调用点显式写 defer——在很少走到的错误分支里特别容易写错。

他考虑过 C++。Bun 里大约 20% 已经是 C++,再多用一些就能拿到构造函数和析构函数。但那样还是得靠代码评审来落实代码规范,内存损坏照样会发生。

Rust 的卖点不一样:在 safe Rust 里,use-after-free、double-free、“忘记释放”都是编译错误。借用检查器和 Drop 把一个代码规范层面的问题,变成了类型系统层面的问题。

策略:转译,不 redesign

重写名声不好,而且有道理。把 535K 行推倒重来,意味着功能和修复冻结一整年。Bun 选的形态是一次机械移植:架构不变、性能目标不变、功能不变、测试套件不变——只是换成 Rust。

两个决定主导了整个工作:

  1. 一次性做完,不要增量。Sumner 把 esbuild 移植到 Zig 的经验告诉他,增量重写会留下临时脚手架,拖累大于帮助。
  2. 让它看起来像转译出来的。目标是写出读起来像源 Zig 的 Rust,等 v1.4 发布之后再逐步重构成地道的 Rust。

关键一点:Bun 的测试套件是用 TypeScript 写的,所以它不在乎运行时是用什么语言实现的。这让他们可以把“所有测试变绿”当作做完的定义。

准备工作少但刻意。写代码之前,Sumner 花了大约 3 小时和 Claude 一起把 Zig 的模式映射到 Rust;那段对话成了 PORTING.md(后来上了 Hacker News)。第二遍梳理了整个代码库里每个结构体字段该有的 Rust 生命周期,序列化成 LIFETIMES.tsv。两份文档在动一行代码之前都先过了对抗式评审。

没动的东西:JavaScriptCore 和那些内嵌的 C/C++ 库。这次重写的是原生胶水代码和 Bun 自己写的代码,不是 JS 引擎。

执行:64 个 Claude,11 天

移植是以大约 50 个 Claude Code 里的“动态工作流”跑的,连续跑了 11 天。每个工作流都是一个循环:领一个任务,产出代码,拿去评审,落实反馈。

有意思的是评审模型。每个实现者旁边,都有两个或更多“对抗式评审者”——独立的 Claude 会话,被要求假定代码是错的,找出它失败的每一个理由。实现者从不评审,评审者从不实现。公告把这描述成对人类评审的模仿:作者想合入,所以得由另一方来查。

有几个机制值得拎出来:

  • 先试跑。他们先移植了 3 个文件,才 all-in 那 1,448 个。
  • worktree 分片。早期运行里,Claude 们互相踩脚,有人跑 git stash,有人跑 git reset。解决办法:4 个 worktree,每个 16 个 Claude,并且规定任务中途不准跑 git 或 cargo。
  • 把编译错误当工作队列。cargo check 把大约 16,000 个错误倒进一个文件,按 crate 分组,64 个 Claude 一点点啃——4 个 worktree 上各 16 个“修/评/应用”循环。
  • 循环依赖。Zig 实际上是一个编译单元;Rust 需要大约 100 个 crate。解开这些循环,才暴露出那 16,000 个错误里的大部分。
  • 隔离。那些会耗光 TCP 端口、或者 spawn 大约 1 万个进程的压测,在 systemd-run 的 cgroup 下跑。机器还是几次把磁盘写满、崩了。
timeline
    title Bun 的 Zig → Rust 重写(2026 年 5 月,11 天)
    2026-05-03 : 移植分支开启(PR #30412)
    2026-05-04 : 第一批 100 文件的草稿
    2026-05-06 : 约 16,000 个编译错误,64 个 Claude
    2026-05-08 : 第一次 CI 运行(972 个失败文件)
    2026-05-09 : Linux x64 变绿
    2026-05-11 : Windows 变绿(最后一个平台)
    2026-05-14 : 六个平台全绿,合入

数字层面:峰值每分钟 1,300 行代码,最忙的一小时(5 月 6 日)695 次提交,最忙的一分钟 58 次。每一次提交落地前,都过了两个对抗式评审者的审。最终 diff 是 +1,009,272 行。零测试被跳过或删除。

CI 才是它变真的那一刻。第一次跑的两天后,失败的测试文件从 972 个掉到 23 个。再过一天半,Linux 全绿。Windows 最后在 5 月 11 日变绿;5 月 14 日的第 54202 号构建,六个平台全绿。

Rust 到底换来了什么

声明的目标是稳定性,而公告用硬数字撑住了这一点。

Bug。 Bun v1.4.0 修掉了 128 个在 v1.3.14 里能复现的 bug。

内存。 Drop 取代了每个调用点上的 defer。公告里的例子:在一个进程里把同一个 60 模块的项目打包 2,000 次。在 v1.3.14 里,每次构建永久漏大约 3 MB;在 v1.4.0 里,内存平了。

构建次数 v1.3.14 v1.4.0
500 1,914 MB 526 MB
1,000 3,506 MB 586 MB
1,500 5,097 MB 608 MB
2,000 6,745 MB 609 MB

Sumner 写道,之前在 Zig 里修这个问题的尝试没能合入,因为缺少和 Drop 对等的机制,让人很难有信心合。

二进制体积。 光是 Rust 重写就砍掉了 Windows 3.8 MB、macOS 5.5 MB、Linux 6.8 MB——主要因为去掉了过量的 Zig comptime。进一步的链接器优化(相同代码折叠、裁掉 ICU 数据)让 Linux 和 Windows 上总体积缩了约 20%。

版本 平台 体积
v1.4.0 Windows 76 MB
v1.3.14 Windows 94 MB
v1.4.0 Linux 70 MB
v1.3.14 Linux 88 MB

速度。 C/C++ 和 Rust 之间的跨语言 LTO,让编译器能跨语言内联。Bun 测出了 2–5% 的提升:

  • Bun.serve:169.6k → 177.7k req/s(+4.8%)
  • express:64.5k → 66.6k(+3.2%)
  • next build:13.62s → 13.03s(+4.5%)
  • tsc -b --force:0.94s → 0.89s(+4.7%)

Prisma 把它的 Compute 公测搬到了 Rust 重写版上。Alexey Orlenko 说:“我们遇到过内存泄漏,还有一个在 VM 暂停又恢复之后恢复不了的数据库连接池。Rust 重写版出来后,我们用同样的失败模式测了一遍,它完美扛住了。”

Claude Code v2.1.181(6 月 17 日)及之后都用上了 Rust 版。在 Linux 上启动快了 10%。Sumner 对用户能感知到的变化的评价是:“Boring is good.”(无聊是好事。)

移植翻车的地方

把 535K 行忠实地转译一遍,不可能零回归。公告记录了 19 个已知回归,全都修了。有代表性的几个,是两种语言之间极小的语义缝隙:

  • debug_assert! 里的副作用。 Zig 的 assert 是个函数,所以参数在每个构建里都会跑。Rust 的 debug_assert! 是个宏,在 release 里整段被抹掉——一个会改动 HMR 状态的 insert_stale 调用,悄无声息地不跑了。(#30678)
  • 奇数长度的切片。 Zig 的辅助函数会忽略末尾多出的一个奇数字节;bytemuck::cast_slice 碰到它会 panic。一个 UTF-16 BOM 后面跟奇数个字节的 Blob.text() 开始崩。(#31188)
  • 边界检查。 Zig 的 ReleaseFast 会去掉边界检查;Rust 的 release 保留。一个占位的常量把文件名 intern 的天花板从 840 万掉到 27 万,真实项目撞上了。(#31503)
  • comptime 格式字符串。 Zig 在编译期求格式字符串,所以颜色标记在参数代入之前就没了。Rust 没有 comptime,于是标记解析器把 OSC 8 超链接里的一个反斜杠也吃掉了。(#30693)

这类 bug,只有两种语言长得一样、其实不一样的时候才会冒出来。

这意味着什么

去掉 LLM 这层不谈,这里有一个实在的工程故事:一个项目撞上了手动内存管理在它的规模上能给出的天花板,于是选了 Rust 的借用检查器作为工具,让一整类 bug 从“被劝阻”变成“不可能”。

LLM 这层,是会让人吵起来的部分。Sumner 说得很坦率:这事本来要三个对代码库有完整上下文的工程师干大约一年,而且他们根本不会去做——现实的选择是“什么都不做,继续修上面那些 bug”。结果是一个工程师盯着 64 个 Claude,11 天、约 16.5 万美元干完了。

Bun 的 Rust 代码里,大约 4% 在 unsafe 块里(约 78 万行总量里,约 2.7 万行、约 1.3 万个 unsafe 关键字),而且这些块里 78% 只有一行。合并之后,他们跑了 11 轮来自 Claude Code Security 的安全评审,还给每个解析器加了 24/7 覆盖引导的模糊测试——至今跑了一千亿次,大约 15 个 PR。

我会盯着的是可维护性问题。Sumner 说这份 Rust 读起来像那份 Zig,还拿 canMergeSymbols 做了个并排对比来证明。赌的是:一份忠实的移植可以按文件逐个评审,而这正是一个人敢在一百万行的 diff 上签字的原因。

他的收尾这句话,会很有意思地老去:“One engineer can do a lot more today than a year ago.”(今天一个工程师能做的,比一年前多太多了。)

参考

分享本页