题图

本文永久链接https://tonybai.com/2026/07/09/ai-rewrite-bun-in-rust

大家好,我是Tony Bai。

两个月前,Bun 要把整个项目从 Zig 迁移到 Rust 的消息在程序员圈子里炸开了锅。有人质疑这是不负责任的"推倒重来",有人猜测这只是社区的以讹传讹,官方始终没有正面回应。

昨天,Bun 创始人 Jarred Sumner 在官网亲自发文,第一次完整讲述了这件事的来龙去脉——而真相比传闻更加疯狂:这不是一次团队协作的常规重写,而是一个人、64个并行运行的 Claude 实例、11天时间,啃下了53万行 Zig 代码,产出超过100万行 Rust 代码,而且是在 Bun 每月 2200 万次下载、Claude Code 和 OpenCode 等一票工具依赖它跑在生产环境的前提下完成的。

这篇文章不只是一次版本更新公告,更像是一份"AI agent 能不能挑起关键基础设施重写"的实战报告。在这里,我们把这篇长文的关键信息梳理了一遍,分享给大家!

起因:作者说他"每天带着崩溃报告睡不着觉"

Bun 最早诞生于 2021 年,Sumner 用 Zig 语言在一年时间里独立完成了第一版——一个野心巨大的项目:JS/TS/CSS 的转译打包工具链、兼容 npm 的包管理器、类 Jest 的测试运行器、Node.js 兼容层、HTTP/WebSocket 客户端等等全部塞进一个运行时里。Zig 的极简与高性能,让 Sumner 得以在 LLM 时代之前以一己之力把这个项目做出来。

但代价也很明显。Zig 和 C 一样不做自动内存管理,Bun 又必须同时打交道 JavaScriptCore 的垃圾回收内存和手动管理内存两套体系,这种混合场景几乎没有语言是专门为它设计的。文章里列出了 Bun v1.3.14 修复的一批真实 bug,几乎清一色是:node:zlib 的 use-after-free、node:http2 因为哈希表 rehash 导致的悬空指针崩溃、UDPSocket 的越界写、crypto.scrypt 的内存泄漏、TLS 会话缓存每次调用泄漏约 6.5KB……这份清单还在继续增长。

作者的原话很直接:这类问题可以一直靠"一个个修"撑下去,但对得起用户的方式应该是从根子上系统性地杜绝它们重复出现。

其实,他们已经做得不算差了——给 Zig 编译器打了 AddressSanitizer 补丁、每次提交都跑 ASAN 测试、用 Fuzzilli(V8/JavaScriptCore 同款引擎模糊测试工具)7×24小时模糊测试运行时 API、有大量端到端的内存泄漏测试。但这些手段本质上都是"事后发现",而不是"从源头杜绝"。

为什么不是C++,而是Rust

文章里专门花了篇幅论证"为什么不选C++"。Bun 本身已经有约 20% 的代码是 C++(JavaScriptCore、uWebSockets、BoringSSL、SQLite 等都是嵌入的 C/C++ 库),迁移到 C++ 是个自然的候选项——能拿到构造/析构函数,能删掉大量 extern "C" 胶水代码。

但作者认为这治标不治本:即便加上 ASAN,内存越界和泄漏这类问题依然会在 C++ 里反复出现,最终还是要靠风格指南加代码审查去人肉把关,而人肉把关的执行力从来靠不住。

选择 Rust 的核心逻辑很朴素:清单里那一长串 bug,绝大多数是 use-after-free、双重释放、错误路径忘记释放——这些在安全 Rust 里直接是编译错误,配合 Drop 机制自动做类似 RAII 的资源清理。编译器报错,显然是比风格指南更靠谱的反馈机制。

但现实的阻力也摆在那儿:Bun 去掉注释后有 53.5万行 Zig 代码,正常人力重写需要一个小团队投入满满一年,还得冻结这一年内的所有 bugfix、安全补丁和新功能开发——这对一个被生产环境广泛依赖的项目来说是不可接受的。团队原本的备选方案,是给 Zig 代码手写一套类 Rust 智能指针,但作者自己都承认,这套东西的开发体验比原生 Rust 差得多,却得不到像 Rust 编译器那样的安全保证。

真正的转折点,是作者决定花一周时间测试:Anthropic 的新模型,到底能不能把 Bun 重写成 Rust?

“一开始我没抱太大希望。但才过了几天,测试套件的通过率就开始猛涨,新 Rust 代码和原始 Zig 代码库的对应关系也清晰到让我意外。我的判断从’这值得一试’变成了’我打算把它合并进主干’。”

“Claude,把Bun重写成Rust”——这句话背后的工程学

这是全文信息量最大的部分。作者强调,直接给 Claude 一句"重写成 Rust,别出错"然后祈祷,是一种不负责任的做法。真正需要想清楚的只有两个大问题,剩下的全是战术细节:

问题一:增量重写,还是一次性全部重写?

作者根据自己早年把 esbuild 转译器从 Go 手动移植到 Zig 的经验判断:一次性全量重写更优。增量重写会留下大量"过渡代码",短期到中期都会很痛苦。

问题二:怎么保证重写后架构、性能、功能集不变,又能真正吃到 Rust 借用检查器的红利?

答案是:先做一次"忠实的机械翻译",让新代码在结构上尽量贴近原 Zig 代码,把降低 unsafe 用量、往地道 Rust 风格靠拢的工作留到 v1.4 发布之后再逐步重构。

核心机制:写代码的和审代码的,必须是两个"人"

真正撑起这次重写质量的,是一套"实现者-对抗性审查者分离"的机制。作者的类比很直接:写代码的人天然想让代码尽快合并,这个动机会让他对自己代码的问题视而不见。Claude 也不例外——负责实现的 Claude 想让代码通过,负责审查的 Claude 的唯一任务就是挑毛病。

具体配置是1个实现者 + 至少2个对抗性审查者:审查者只拿到代码diff,看不到实现者的推理过程,而且被明确告知"默认这段代码是错的",去找出它为什么不对。

文章公开了三个被这套机制真实拦下来的 bug 案例,很能说明问题:

  • 异步关闭的悬空指针:一段 Rust 代码把持有管道的 Box 传给了异步的 uv_close,但 Box 会在函数返回时提前释放,libuv 之后再回调时就是use-after-free,接着还会造成双重释放。审查者揪出后改用 Box::leak 处理。
  • 负数时间戳的 nsec 溢出:把浮点秒数拆分成 timespec 时用了 trunc(),对 1970 年之前的负数文件时间会算出非法的负 nsec,审查者建议换成 floor()
  • unwrap_or 的立即求值陷阱:CSS color-mix() 解析代码里用 unwrap_or 处理百分比缺省值,但 unwrap_or 的参数是立即求值的,会在该跳过的情况下提前 panic,审查者建议换成惰性求值的 unwrap_or_else

这三个 bug 都能正常编译、看起来也都合理——如果没有专门"找茬"的第二个 Claude,大概率会被直接合入主干。

11天里到底发生了什么

整个重写通过大约50个"动态工作流"(dynamic workflow)在 Claude Code 里连续跑了11天完成。每个工作流本质上都是一个"取任务→写代码→多方审查→应用反馈"的循环,作者形容自己大部分时间在"看着这些工作流跑,读输出找问题,然后去改工作流本身"。

准备阶段

动手写代码前,作者先花了大约3小时和 Claude 讨论怎么把 Zig 的模式映射到 Rust,产出了一份 PORTING.md(这份文档后来还上了 Hacker News)。紧接着又让 Claude 跑了一个专门的工作流,逐个分析代码库里每个结构体字段该用什么样的生命周期标注,产出 LIFETIMES.tsv,同样经过对抗性审查。

试运行

先只挑3个文件做完整流程验证,确认没问题后才铺开到全部 1448 个 .zig 文件。

踩过的坑也很真实

一开始多个 Claude 实例互相"抢跑",有的在别人commit之前跑了 git stashgit reset --hard,直接把彼此的工作搞乱了。作者后来把规则改成:严禁任何不针对单个文件的 git 命令,也不允许跑慢命令(比如 cargo)。为了提速,最后拆成4个独立 worktree、每个跑16个 Claude 实例,峰值同时有约64个 Claude 在跑。

高峰期,Claude 平均每分钟写1300行代码——当然,每一行都要经过两个独立审查者审查、并完成一轮修复才会提交,这个阶段"完全跑不起来"是常态而不是意外。

编译错误也走同样的模式:cargo check 一次性暴露出约1.6万条编译错误(大量来自作者把单一编译单元拆分成约100个 crate 后暴露的循环依赖问题),按 crate 分组,每个 crate 一个实现者+两个审查者+一个整合者的小循环,64个 Claude 同时啃不同的 crate。

这中间还闹过一次乌龙:Claude 把"让所有 crate 编译通过"理解成了"把编译不过的函数直接 stub 掉",还开始写很长的注释来解释这些"权宜之计"为什么没问题。作者给审查者加了一条硬规则:

“如果你需要写一整段注释来证明这个权宜之计是合理的,那说明代码本身就是错的——去把代码修对。”

加上这条规则之后,这类问题很快消失。

测试套件的收敛过程也很戏剧化:CI 第一次跑完时有972个测试文件失败,两天后降到23个,再过一天半,Linux 平台全绿——这是整个项目第一次让人感觉"这事真能成"。最终6个平台(Linux x64/arm64、macOS x64/arm64、Windows x64/arm64)全部转绿,PR才被合并进主干。

硬核数字:这次重写到底花了多少代价

原文给出了相当详细的统计:

  • 11天(5月3日启动,5月14日合并),6778次提交
  • 净变化 +1,009,272 行代码
  • 三个测试平台合计跑了超过 370万次 expect() 断言,0个测试被跳过或删除
  • 消耗 59亿 未缓存输入 token、6.9亿 输出 token、720亿次缓存输入token读取
  • 按 API 定价折算,成本约为 16.5万美元
  • 作者的估算是:换成人力,需要3名对代码库有完整上下文的工程师投入约一年,而且这一年里 Node.js 兼容性改进、bug修复、安全更新、新功能开发全部要停摆——这是团队完全无法接受的代价,所以现实中的备选方案其实是"什么都不做,继续无限期地修这份bug清单"。

这次重写用的是 Claude Fable 5(Mythos 级模型)的预发布版本,靠 Claude Code 的动态工作流能力让64个 Claude 实例并行跑了11天。

重写完之后,Bun 到底变好了多少

内存

得益于 Rust 的 Drop 自动清理机制(对应 Zig 里容易漏写、容易重复写的 defer),团队借助改进后的 LeakSanitizer 集成,修复了此前一直没能在 Zig 版本里彻底解决的内存泄漏问题。一个典型案例是 Bun.build() 重复调用的内存泄漏:v1.3.14 每次构建大约泄漏3MB,跑2000次构建后内存占用高达6.7GB;v1.4.0 里内存占用会自然收敛,2000次构建后稳定在约609MB。

体积

初版 Rust 重写就在 Windows/macOS/Linux 上分别减小了3.8MB/5.5MB/6.8MB(很大程度是因为原 Zig 代码里用了太多 comptime)。叠加链接器优化(相同代码折叠)、ICU资源精简等改动后,Linux 和 Windows 上的整体二进制体积缩小了约20%(Windows 从94MB降到76MB,Linux从88MB降到70MB)。

栈空间

Rust 编译器生成的 LLVM IR 会对不再使用的栈变量插入生命周期结束标记,让 LLVM 能复用栈空间——这类优化此前在 Zig 里得靠手动把大函数拆成一堆小函数来实现。

性能

得益于 Rust 与 C/C++ 之间支持的跨语言链接时优化(LTO),在 Linux x64(EC2 Xeon Platinum 8488C)上做的对比测试显示,HTTP吞吐提升 2.8%-4.8%(Bun.serve 从16.96万req/s提升到17.77万req/s),next buildvite buildtsc -b 等典型工作负载耗时缩短 2.2%-4.7%。

生产验证

Prisma 在其新推出的 Prisma Compute 公测版中已经跑在这个 Rust 重写版本上,官方引用了 Prisma 工程师的说法——此前在虚拟机暂停/恢复场景下出现的内存泄漏和连接池无法恢复问题,换到 Rust 版本后被彻底解决。Claude Code 自 v2.1.181(6月17日发布)起也切换到了 Rust 版 Bun,Linux 上冷启动时间提速约10%,从 517ms 降到 464ms。

19个已知回归:机械翻译不是万能的

作者很坦诚地承认,这次重写不可能做到零回归,一共留下了19个已知问题(均已修复),而且几乎都出在"两种语言里长得一样、语义却完全不同"的代码模式上。原文给了四个很典型的例子:

  1. debug_assert! 的副作用消失:Zig 的 assert 是一个函数,参数在所有构建模式下都会执行;Rust 的 debug_assert! 是宏,release 构建下整段表达式会被直接擦除。有一处判断里顺带调用了一个有副作用的函数,结果在 release 构建下这个调用直接消失,导致某些场景下热更新(HMR)失效。
  2. 奇数长度切片:Zig 里一个自定义函数用整除截断处理了尾部多余的奇数字节,Rust 里对应的 bytemuck::cast_slice 遇到奇数长度直接 panic,一度导致处理特定 UTF-16 字节序标记的 Blob.text() 崩溃。
  3. 边界检查差异:Zig 在 macOS/Linux 上用的是关闭边界检查的 ReleaseFast 模式,Rust release 构建默认保留边界检查。一处占位符式的缓冲区大小设置把上限从840万骤降到27万,触发了一个原本潜藏的越界写。
  4. comptime 格式化字符串:Zig 里颜色标记的解析发生在编译期格式化之前;Rust 没有编译期参数,导致参数里如果恰好包含类似标记的字符串(比如以反斜杠结尾的终端超链接),会被错误地当成格式标记处理掉。

这些案例其实是这篇文章相当有价值的部分——它证明"机械翻译"不是无脑复制粘贴,两门语言在看似等价的语法背后,运行时语义可能完全不同,这类陷阱恰恰是纯人工审查也很容易漏掉的地方。

重写之后:安全审查仍在继续

合并主干后,团队又做了11轮来自 Claude Code Security 的安全审查,并逐一处理了发现的问题。同时上线了对 Bun 全部解析器(JavaScript、TypeScript、CSS、JSON5、YAML、TOML等)的7×24小时覆盖率引导模糊测试,模糊测试发现的bug会自动提交给 Claude 生成修复 PR,再由人工审核——截至发稿,模糊测试已执行超过1000亿次,产出约15个PR。

截至目前,Bun 的 Rust 代码里约有4%仍在 unsafe 块内(约78万行代码里有约1.3万个 unsafe 关键字,覆盖约2.7万行),其中78%的 unsafe 块只有一行——大多是从 C++ 拿到的指针,或者调用某个 C 库。团队预期这个比例会随着后续从"忠实 Zig 移植"逐步重构为"地道 Rust 风格"而继续下降,但由于 Bun 会持续依赖 JavaScriptCore 等 C/C++ 库,unsafe 占比注定高于纯 Rust 项目。

Bun v1.3.14 是最后一个 Zig 版本,v1.4.0 将是第一个 Rust 版本,目前已开放 canary 渠道试用(bun upgrade --canary)。

写在最后

这篇文章之所以值得程序员社区认真读一遍,并不只是因为 Bun 这个项目本身的技术选型变化,而是它第一次用如此详尽的一手数据,回答了一个大家都在猜测的问题:AI agent 到底能不能承担起一个被广泛依赖的基础设施项目的完整语言重写?

从这次实践看,答案不是简单的"能"或"不能",而是取决于工程方法论:

  • 一次性重写而非增量迁移的决策
  • 机械翻译优先于理想化重构的取舍
  • “实现者与审查者分离、审查者带着找茬心态工作"的对抗性审查机制
  • 把编译错误和CI失败当作可以被大规模并行消化的"任务队列”
  • 贯穿全程、绝不放松的"零测试跳过"底线

这些恰恰是任何一个人类工程团队做大型重写时同样需要遵守的纪律,只是这一次,执行这些纪律的主力换成了64个同时运转的 Claude 实例,而人类工程师的角色,变成了架构决策者和流程监工。

16.5万美元、11天、0个测试被删除——这组数字或许会成为未来讨论"AI能不能重写关键基础设施代码"时,一个绕不开的参照系。

资料链接 - https://bun.com/blog/bun-in-rust


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。