Rust重写Bun:64 Claude 并行 100 万行代码
你可能已经看到了——Bun 创始人 Jarred Sumner 用 64 个并行 Claude 实例,11 天把 53 万行 Zig 代码重写成了 100 万行 Rust。这件事在社区里炸了锅,但多数讨论停留在「AI 真牛」的表面结论上。
真正值得拆解的不是「能不能」,而是「怎么做到的」。
一、为什么 Bun 要从 Zig 迁移到 Rust?
Bun 的 bug 修复列表读起来像一堂「内存安全警示课」。v1.3.14 修复的这批 bug,几乎全是 use-after-free、双重释放、错误路径忘记释放:
node:zlib在异步.write()未完成时调用.reset()→ 堆释放后使用node:http2的 JS 回调触发哈希表重哈希→内部流指针失效 → use-after-freeUDPSocket.send()的valueOf()回调在载荷捕获与实际发送之间分离了 ArrayBuffercrypto.scrypt的输出缓存分配失败时,回调和密码/盐缓存永不释放tlsSocket.setSession()每次调用泄漏一个 SSL_SESSION(~6.5KB/次)
Jarred 的原话很直接:「我每天带着崩溃报告睡不着觉。」
Bun 的问题在于混合内存管理——JavaScriptCore 的垃圾回收内存与 Zig 的手动管理内存共存,这种场景几乎没有语言是专门设计的。Zig 没有构造/析构函数,清理代码靠 defer 在每个调用点手写,出错的概率完全取决于开发者的细心程度。
二、为什么不选 C++?
Bun 已经有约 20% 的代码是 C++(JavaScriptCore、uWebSockets、BoringSSL 都是嵌入的 C/C++ 库)。往 C++ 迁移是最自然的选择——能拿到构造/析构函数,能删掉大量 extern "C" 胶水代码。
但 Jarred 的判断很明确:C++ 治标不治本。ASAN、模糊测试、风格指南这些「事后发现」的手段,都不能从源头杜绝内存问题。TigerBeetle 的 TigerStyle 和 Google 的 3.1 万字 C++ 风格指南都是先例——风格指南的执行力永远靠不住。代码审查是「最好努力」,编译器报错才是「必然执行」。
Rust 的核心逻辑朴素到让人无法反驳:清单里那一长串 bug,在安全 Rust 里直接是编译错误。
三、关键决策:一次性全量 vs 增量迁移
这是全文信息量最大的决策点。
Jarred 根据早年把 esbuild 转译器从 Go 手动移植到 Zig 的经验判断:一次性全量迁移更优。增量迁移会遗留大量「过渡代码」,短期和中期都会很痛苦。
第二个决策是 怎么保证质量:先做一次忠实的机械翻译,让新代码在结构上尽量贴近原 Zig 代码,把降低 unsafe 用量、往地道 Rust 风格靠拢的工作留到 v1.4 发布后再逐步重构。
这两个决策定了调,剩下的都是战术细节。
四、工程力学:50 个 Dynamic Workflow × 64 个 Claude × 11 天
Jarred 用了约 50 个 Dynamic Workflow,连续运行 11 天。每个 workflow 是一个循环——pop task → implement → review → apply feedback,覆盖了:
- 生成 PORTING.md:Zig 模式→Rust 模式的映射文档
- LIFETIMES.tsv:全代码库结构体字段的生命周期分析
- 机械翻译 1,448 个
.zig文件到.rs - 修复每个 crate 的编译错误
- 让
bun test、bun build等子命令跑起来 - 让整个测试套件通过
峰值时开了 4 个 workflow,每个独占一个 git worktree,各跑 16 个 Claude,总计 64 个并行实例。峰值产出:每分钟 58 次提交、1,300 行代码。
中间踩了多个坑。Claude 会互相 git stash 踩踏,会写冗长的注释来 justify workaround,会在编译错误阶段选择 stub out 而非真正修复。Jarred 的策略是 edit the workflow prompt,不手修代码——告诉 workflow「禁止 git stash」「禁止 cargo」「禁止长注释」,问题就自动消失。
# 每个 Workflow 的循环逻辑(伪代码)
while task = todo_list.pop():
result = implementer(task)
feedback = await adversarial_review(result) # 至少 2 个
apply_feedback(feedback, result)
五、核心机制:实现者-对抗性审查者分离
这是整篇文章最值得抄走的设计。
Jarred 的类比很直接:写代码的人天然想让代码尽快合并,这个动机会让他对自己代码的问题视而不见。Claude 也一样——负责实现的 Claude 想让代码通过,负责审查的 Claude 的唯一任务就是挑毛病。
配置很简单:1 个实现者 + 至少 2 个对抗性审查者。审查者只拿到代码 diff,看不到实现者的推理过程,被明确告知「默认这段代码是错的」。
文章公开了三个被这套机制真实拦截的 bug:
场景 1:异步关闭的悬空指针
实现者把持有管道的 Box 传给了异步 uv_close。
Box 在函数返回时提前释放,libuv 回调时就是 use-after-free + 双重释放。
审查看穿后改用 Box::leak 处理。
场景 2:负数时间戳的 nsec 溢出
实现者用 trunc() 拆分浮点秒数到 timespec。
对 1970 年之前的负数文件时间算出非法的负 nsec。
审查者建议换用 floor()。
场景 3:unwrap_or 的立即求值陷阱
CSS color-mix() 解析用 unwrap_or 处理百分比缺省值。
unwrap_or 参数立即求值,在应跳过时提前 panic。
审查者建议换成惰性求值的 unwrap_or_else。
三个 bug 都能正常编译、看起来也都合理——如果没有专门找茬的第二个 Claude,大概率被直接合入主干。这套机制是这次迁移质量的关键保障。
六、三阶段管线:机械翻译 → 编译错误 → 测试全绿
Phase A — 机械翻译。 先做 3 个文件的试迁移验证流程,然后扩展到 1,448 个 .zig 文件。由于 Bun 的 git 仓库太大,最终方案是 4 个 worktree 各 16 个 Claude。
Phase B — 修复 ~16,000 个编译错误。 最难的问题是循环依赖。Zig 代码库是单编译单元(1 个 crate),迁移时拆成 ~100 个 crates,循环依赖产生了海量错误。额外跑了一个 workflow 来分类代码应该放哪个 crate。修复时按 crate 分组,每组找 1 implementer + 2 reviewers + 1 fixer 处理。
# Phase B 的工作队列
16,000+ 编译器错误 → 按 crate 分组 → 64 个 Claude 并行修复
每个 crate: cargo check → 修复 → 2 个审查者 → 1 个应用者
Phase C — 让测试套件全绿。 测试套件包含内存泄漏测试、集成测试(next dev 热更新的长时间测试)、压力测试(耗尽 TCP socket、读写 GB 级数据、spawn 10k 进程)。使用了 systemd-run(cgroups)做隔离。
Linux x64 的 60 个测试分片先绿,Windows 最后才通过。从第一个测试绿到全平台全绿用了约 3 天。
七、核心数据与成本
| 指标 | 数值 |
|---|---|
| Zig 代码(不含注释) | 535,496 行 |
| Rust 代码 | 1,009,272 行 |
| 耗时 | 11 天(5/3 → 5/14 合并入主干) |
| 并行 Claude 实例 | 64 个(4 worktrees × 16) |
| 总提交 | 6,778 次 |
| 峰值产出 | 58 提交/分钟,1,300 行/分钟 |
| API 成本 | $165,000 |
| Token 消耗 | 5.9B uncached input + 690M output |
| 缓存读取 | 72B cached input tokens |
| 删减测试用例 | 0 个 |
| 等效人力 | ~3 人年(全周期冻结功能开发) |
| 二进制体积缩减 | ~20% |
| 已知回归 | 19 个(全部已修复) |
| Rust unsafe 占比 | ~4%(13,000 个 unsafe 关键字) |
等效 3 人年、$16.5 万 API 成本、11 天——这个 ROI 放在任何商业决策里都是压倒性的。
八、迁移后成果与 19 个回归
合并后的成果:
- 内存泄漏彻底消除。 所有之前靠 ASAN 和模糊测试「事后发现」的问题,在安全 Rust 里直接是编译错误
- 二进制体积缩减约 20%,多平台吞吐量提升
- 24/7 覆盖引导模糊测试 已上线,覆盖所有解析器(JS/TS/JSX/CSS/JSON5/TOML/…),自动提交 PR
- 11 轮 Claude Code Security 安全审查 完成并修复所有发现
19 个回归的根因值得关注——大多来自「语法相同但语义不同」:
// Zig: assert 是函数,参数在 release 中也执行
assert(try dev.client_graph.insertStale(...));
// Rust: debug_assert! 是宏,release 中整行擦除
debug_assert!(dev.client_graph.insert_stale(...)? == ...);
// → insert_stale 在 release 中不执行,HMR 损坏
其他回归包括 bytemuck::cast_slice 对奇数长度 slice 直接 panic(Zig 的 reinterpretSlice 会忽略尾字节)、编译器检查更严格导致的边界差异。
九、给实践者的启示
这次迁移不仅是技术案例,更是 AI Agent 工程化的里程碑。几点值得思考的:
1. 对抗性审查是质量的关键。 不是「AI 写代码」厉害,是「AI 写 + AI 审」这个协作模式厉害。实现者和审查者分离、分开上下文窗口,是目前最适合大规模 LLM 代码生产的模式。
2. 工作流的 prompt 比代码更重要。 Jarred 几乎所有问题解决方式都是 edit the workflow prompt——告诉 Claude 不要做什么,而不是手动修复输出。「edit the loop,not the output」本身就是工程方法论。
3. 测试套件是最后的防线。 0 被删减、138 万 expect 断言——没有这套百万级测试体系,没有人敢合并 100 万行 AI 代码。测试覆盖率不是锦上添花,是 AI 代码生产的必要基础设施。
4. 成本已经不是拦路虎。 $16.5 万做 3 人年的工作量,在商业决策层面已经不需要犹豫。真正的问题不是「贵不贵」,而是「你的代码库有足够的测试覆盖率来兜底吗」。
5. 语法相同 ≠ 语义相同。 19 个回归几乎全是跨语言「看起来一样但行为不同」的陷阱——debug_assert! 的宏语义、不同语言的边界检查、slice 处理的直觉差异。
原文:https://bun.sh/blog/bun-in-rust(Jarred Sumner,2026-07-08)
更多推荐


所有评论(0)