Bun 用 Claude 11天完成 Zig → Rust 全量重写:代码、哲学与工程权衡的完整复盘
2026年5月11日,前端社区被一条推文炸开了。
Bun 创始人 Jarred Sumner 在 X 上写道:
"Bun v1.3.14 将于明日发布。如果我们合并 Rust 重写版本,这将是 Zig 的最后一个版本。"
就这么一句。四年前,Bun 因为选择了 Zig 而显得特立独行;四年后,Zig 版本被它的创造者用一条推文宣告了终结。
这场从 Zig 到 Rust 的迁移,实际上只花了大约 6 天,涉及 96 万行代码,在 Linux x64 glibc 环境下通过了现有测试套件的 99.8%。而 6 天前,Jarred 还在 Hacker News 上说"这堆代码最后被全部扔掉的概率非常高"。
更令人瞠目结舌的是:整个重写,是由 Claude Code 在 11 天内完成的,消耗了价值约 16.5 万美元的 API 调用,最终合并为一个超过 600 万行代码(含测试)的巨型 PR。
这不是一次普通的技术选型变更。这是一次对"人类如何写代码"这一前提本身的公开展示与质疑。
本文将从技术架构、代码实现、工程流程三个维度,完整复盘这场史无前例的 AI 驱动语言迁移,并探讨它对整个软件工程行业的深层启示。
一、背景:Bun 为何需要离开 Zig
1.1 Bun 的野心与现实
Bun 不仅仅是一个 JavaScript 运行时。它的目标清单几乎涵盖了现代前端工具链的每一个角落:
- JavaScript、TypeScript 和 CSS 的转译器、压缩器和打包器
- 兼容 npm 的包管理器
- 类 Jest 的测试运行器
- 兼容 Node.js 和 TypeScript 的模块解析
- HTTP/1.1 和 WebSocket 客户端
- Node.js API 实现(fs、net、tls 等数十个模块)
- SQLite 客户端
- Web API 填充(fetch、Response、Headers 等)
2019 年,Jarred Sumner 在奥克兰一间狭窄的公寓里,用 Zig 语言、没有任何 LLM 辅助,花了一年写出了这个庞然大物。Zig 让他得以在极短时间内实现一个野心勃勃的项目——这种成就依赖于 Zig 的零成本抽象和直接内存控制。
但随着 Bun 的用户量突破每月 2200 万次下载,Claude Code 和 OpenCode 等顶级 AI 编程工具以 Bun 为运行时,这套架构的稳定性问题开始浮出水面。
1.2 那份令人窒息的 Bug 清单
Bun v1.3.14 中修复的 Bug 列表,揭示了 Zig 与 JavaScript 引擎混合使用时的根本性矛盾:
内存安全问题(use-after-free 类):
// 问题代码示例(Zig):
// 在 node:zlib 中,对流调用 .reset() 时,线程池上还有异步 write() 在执行
// 此时如果底层 ArrayBuffer 被 GC 回收,Zig 层面的指针就成了悬挂指针
const allocator = std.heap.page_allocator;
const buffer = try allocator.alloc(u8, size);
defer allocator.free(buffer);
// 但 JS 引擎侧的 GC 可能在 defer 执行前就回收了 buffer
// 后续的 C 库调用会访问已释放的内存
// 问题:UDPSocket.send() 中,valueOf() 回调在捕获载荷与实际发送之间
// 分离了 ArrayBuffer,导致释放后使用
const payload = array_buffer.valueOf(); // 用户回调可能在这期间修改了 buffer
send(socket, payload, length); // 此时内存布局可能已改变
内存泄漏类:
// 问题:crypto.scrypt 中,输出缓冲区分配失败时
// 回调和受保护的密码/盐值缓冲区从未释放
fn scrypt(...) !void {
var password_buffer: []u8 = undefined;
errdefer {
// 这个 errdefer 没有覆盖所有错误路径
// 如果某些内部错误提前返回,buffer 泄漏
allocator.free(password_buffer);
}
// ...
}
// 问题:fs.watch() 的监听器在调用 .close() 后从未被垃圾回收
// 原因:引用计数下溢导致每个监听器被永久固定为 GC 根节点
双重释放类:
// 问题:CSS 解析器中,background-clip 含厂商前缀和多层背景时
// 导致双重释放崩溃
// 两个代码路径都认为自己拥有某个内部数据结构的所有权
每一类问题的根源都指向同一个核心矛盾:Zig 的手动内存管理与 JavaScript 引擎的垃圾回收器,在同一个运行时中运行,却没有共享的所有权语义。
Zig 没有构造函数/析构函数,清理工作需要在每个调用点用 defer 显式写出:
// Zig 的清理机制
const bytes: ArrayBuffer = try .fromPinned(global, value);
defer bytes.unpin(); // 必须在每个可能的退出路径上手动调用
// 容易忘记(内存泄漏),或在错误处理中执行两次(双重释放)
对比 Rust 的 Drop trait:
// Rust 的清理机制
impl Drop for Bytes {
fn drop(&mut self) {
if !self.pinned.is_empty() {
JSC__JSValue__unpinArrayBuffer(self.pinned);
// 编译器保证在值离开作用域时自动调用
// 绝不会忘记,也绝不会执行两次
}
}
}
1.3 Zig 生态的困境
Bun 团队 fork 过 Zig 编译器,添加了 LLVM 并行代码生成优化,使 macOS 和 Linux 的 debug 编译速度提升了 4 倍。但这些优化始终无法 upstream 回 Zig 官方——原因在于 Zig 社区有一条严格的"no-AI policy":禁止 AI 生成 issue、PR 甚至评论。
Zig 基金会成员 Loris Cro 公开表示,大量 LLM 贡献只会制造"幻觉 PR"、"垃圾噪音"以及动辄上万行、根本无法维护的提交。
讽刺的是,当 Anthropic(整个 AI coding 浪潮最激进的推动者之一)收购 Bun 后,这种"哲学冲突"被无限放大:一边是 Zig 社区全面封禁 AI 生成代码,另一边是 Bun 团队开始用 Claude agent 大规模把 Zig 本身迁移出 Zig。
二、迁移策略:11 天的工程方法论
2.1 为什么选全量重写而非增量迁移
Jarred 在早期把 esbuild 的 transpiler 从 Go 移植到 Zig 时积累了经验(没有 LLM,花了三周)。他的结论是:增量迁移会产生大量临时代码,徒增痛苦,最终还是得全部清理掉。
因此,这次迁移的策略是:先用 Claude 做一次"看起来像把 Zig 代码编译成 Rust"的机械重写,最小化行为变更,直接用现有的 Bun 测试套件作为验收标准。等 Bun v1.4 发布后,再逐步重构,减少 unsafe 的使用,让代码更地道 Rust。
2.2 PORTING.md:576 行的迁移宪法
在让 Claude 开始翻译代码之前,Jarred 和 Claude Fable 5 花了约 3 小时讨论如何把 Zig 代码库的模式紧密映射到 Rust。这段对话被整理成了一份 576 行的 PORTING.md 文档,成为整个迁移的"宪法":
# Bun Zig to Rust Porting Guide
## Phase A: Faithful Translation
- 逐文件忠实保留 Zig 的逻辑
- Rust 代码暂时不能编译也没关系
- 不做任何优化或重构
## Phase B: Fix compilation errors
- 逐个 crate 解决编译、构建和运行问题
## File Naming
- `foo.zig` → `foo.rs`
- 路径结构保持一致
## Crate Organization
- 原始 Zig 代码库是单个编译单元
- 新 Rust 代码库拆分为约 100 个 crate
- 必须避免循环依赖
## Prohibited Dependencies
- tokio / rayon / hyper / futures 禁止使用
- async fn 禁止使用
- 所有 unsafe 必须写明 SAFETY 注释
## When Unsure
- 宁可留下 TODO,也不要让 AI 自行猜测
- 保持与 Zig 原版行为一致
2.3 LIFETIMES.tsv:生命周期标注的工程化
最大的挑战是:如何给手动管理内存的 Zig 代码加上 Rust 的生命周期约束?
Jarred 让 Claude 运行了一个专门的动态工作流:
我:启动一个动态工作流,分析代码库中每个结构体字段的合适生命周期。
这个工作流要读取每个文件中的每个结构体字段,追踪控制流。
先找出那些在 Rust 中生命周期比较复杂的字段,
为每个字段提议一个生命周期,
然后用 2 个对抗式审查智能体审查这些生命周期,
最后应用反馈,把结果序列化成 LIFETIMES.tsv,供其他 Claude 查阅。
结果:生成了数十万行生命周期标注数据,精确到每一个结构体的每一个字段。
2.4 并行化:64 个 Claude 同时写代码
最初的尝试遇到了问题:多个 Claude 同时工作导致 git 冲突——git stash、git reset --hard 互相干扰。
解决方案:4 个独立工作树,各 16 个 Claude 并发。每个工作树处理独立的文件子集,最后合并到一个分支上编译验证。
这个架构最终实现了峰值速度:每分钟约 1,300 行新代码,每一行都经过两个独立的对抗式审查者审查,提交前经历一轮修复。
三、编译器错误的处理:把错误当工作队列
3.1 16,000 个错误的工程化分解
代码翻译完成后,Rust 编译器报错约 16,000 个。这对人类工程师来说是天文数字,但对 64 个并行工作的 Claude 来说是可以处理的。
处理流程:
# 1. 将所有编译错误写入文件,按 crate 分组
cargo check 2>&1 | tee errors.log
# 2. 错误分配:64 个 Claude 各认领一个 crate
# 3. 逐个 crate 修复:1 人修复 → 2 人审查 → 1 人应用
# 4. 循环直到所有 crate 编译通过
最棘手的类别:循环依赖。 原始 Zig 是一个编译单元(相当于一个 crate),但新的 Rust 代码库被拆分成了约 100 个 crate。拆分过程中不可避免地产生了循环依赖。
解决方案:又跑了一个专门的工作流,分析存在循环依赖的代码应该归属哪个 crate,记录所有结论;再跑一个工作流完成重构。
3.2 对抗式审查的三个真实 Bug
Claude 的对抗式审查(adversarial review)实际发现了三个严重的编译通过但语义错误的 bug。以下是真实案例:
Bug 1: 异步关闭中的 use-after-free
// 审查发现的 Bug(编译通过):
for stdio in [spawned_stdout, spawned_stderr] {
match stdio {
StdioResult::Buffer(mut pipe) => {
// pipe: Box<uv::Pipe> — 交给 libuv 去关闭
pipe.close(Subprocess::on_pipe_close)
// 问题:uv_close 是异步的
// libuv 会在下一个事件循环 tick 才调用 on_pipe_close 释放内存
// 但 Box 在这个 match 分支末尾就被 drop 了
// libuv 拿到的是已释放的内存,on_pipe_close 又会再次释放——use-after-free + double-free
}
StdioResult::Fd(fd) => fd.close(),
StdioResult::Unavailable => {}
}
}
// 正确写法:
for stdio in [spawned_stdout, spawned_stderr] {
match stdio {
StdioResult::Buffer(mut pipe) => {
// 泄漏 Box,让 libuv 持有有效指针
Box::leak(pipe).close(Subprocess::on_pipe_close)
}
// ...
}
}
Bug 2: 负时间戳的 nsec 溢出
// 审查发现的 Bug(编译通过):
let t = file_mtime_seconds(); // 可能是负数(1970年之前的文件)
let sec = t.trunc(); // -1.5 → -1,朝零舍入
TimeLike {
sec: sec as i64,
nsec: ((t - sec) * 1e9) as i64, // -0.5 * 1e9 = -5e8 → 负数 nsec!
}
// 正确:nsec 必须是 [0, 1e9) 范围内的非负整数
// 正确写法:
let sec = t.floor(); // -1.5 → -2
let nsec = ((t - sec) * 1e9).round() as i64;
// 结果:-2 + 0.5 = -1.5, -1.5 * 1e9 = -1.5e9
// floor 后:sec = -2, nsec = ((-1.5) - (-2)) * 1e9 = 5e8
Bug 3: 贪婪的 unwrap_or
// 审查发现的 Bug(编译通过):
let p1 = first.percentage.unwrap_or(1.0 - second.percentage.unwrap());
// 问题:unwrap_or 会立即求值参数!
// 即使 first.percentage 是 Some,second.percentage.unwrap() 仍然会执行
// 如果 second.percentage 是 None,立刻 panic
// 正确写法:
let p1 = first.percentage.unwrap_or_else(|| 1.0 - second.percentage.unwrap());
// unwrap_or_else 接受闭包,惰性求值——只在需要时才执行
这三个 bug 有一个共同特征:都能编译通过,看上去都很合理。如果没有独立的审查者,它们会直接进入生产环境。
四、实际效果:数据说话
4.1 测试覆盖
合并前消耗的 Token 统计(11 天):
| 指标 | 数量 |
|---|---|
| 未缓存输入 Token | 59 亿 |
| 输出 Token | 6.9 亿 |
| 缓存输入 Token | 720 亿 |
| 按 API 定价 | 约 16.5 万美元 |
测试覆盖数据:
| 平台 | expect() 调用次数 | 测试用例数 | 测试文件数 |
|---|---|---|---|
| Debian 13 x64 | 1,386,826 | 60,624 | 4,174 |
| macOS 14 arm64 | 1,259,953 | 58,850 | 4,175 |
| Windows 2019 x64 | 1,007,544 | 57,337 | 4,173 |
0 个测试被跳过或删除。
4.2 内存占用改善
Rust Drop trait 解决了 Zig 中难以表达的资源清理逻辑。在同一进程中把同一个 60 模块项目打包 2,000 次:
| 构建次数 | Bun v1.3.14(Zig) | Bun v1.4.0(Rust) |
|---|---|---|
| 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 |
v1.3.14 中,每次构建永久泄漏约 3 MB。开发服务器等长时间运行的工具最终会耗尽内存。
4.3 二进制体积缩小
| 平台 | v1.3.14 | v1.4.0 | 缩小比例 |
|---|---|---|---|
| Linux | 88 MB | 70 MB | ~20% |
| Windows | 94 MB | 76 MB | ~19% |
macOS 上缩小 5.5 MB。主要原因:Zig 代码中大量 comptime 参数导致二进制膨胀,Rust 的泛型具化策略更紧凑。
4.4 性能提升
Linux x64 (EC2, Xeon Platinum 8488C) 基准测试:
HTTP 吞吐量(req/s):
| 服务器 | v1.3.14 | v1.4.0 | 提升 |
|---|---|---|---|
| Bun.serve | 169.6k | 177.7k | +4.8% |
| node:http | 103.8k | 108.5k | +4.5% |
| express | 64.5k | 66.6k | +3.2% |
| fastify | 91.5k | 95.9k | +4.8% |
应用/CLI 工作负载:
| 工作负载 | v1.3.14 | v1.4.0 | 提升 |
|---|---|---|---|
| next build | 13.62s | 13.03s | +4.5% |
| tsc -b --force | 0.94s | 0.89s | +4.7% |
4.5 Unsafe 的真实数量
社区争议的焦点之一: Theo (t3.gg) 指出 Rust 移植版有超过 13,000 个 unsafe 调用。
实际数据(Bun v1.4.0):
- 约 78 万行 Rust 代码
- 约 13,000 个
unsafe关键字,散布在约 2.7 万行代码中 - 约 4% 的代码在
unsafe块里 - 其中 78% 的
unsafe块只有一行(调用 C 库或访问 C 指针)
这与纯 Rust 项目(如 uv,73 个 unsafe)没有可比性——Bun 嵌入了 JavaScriptCore、uWebSockets、usockets、lsquic、BoringSSL、SQLite 等大量 C/C++ 库。
五、已知回归:AI 重写的真实代价
这次重写引入了 19 个已知回归,每个都已被修复。大多数回归的根源在于:两段代码在语法上一模一样,但语义截然不同。
5.1 debug_assert! 内部的副作用
// Zig:assert 是函数,参数每次构建都会执行
if (dev.framework.react_fast_refresh) |rfr| {
assert(try dev.client_graph.insertStale(rfr.import_source, false)
== IncrementalGraph(.client).react_refresh_index);
}
// Rust:debug_assert! 是宏,发布构建中整个表达式被擦除
if let Some(rfr) = &dev.framework.react_fast_refresh {
debug_assert!(
dev.client_graph.insert_stale(&rfr.import_source, false)?
== react_refresh_index
);
}
insert_stale 负责把文件添加到 HMR 图中。发布构建中这段代码不再执行,导致特定场景下 React 热重载失效。
5.2 奇数长度的切片
Bun 的 Zig reinterpretSlice(u16, bytes) 用 @divTrunc 转换,忽略末尾的奇数个字节(buf[0..buf.len & ~1])。bytemuck::cast_slice 遇到奇数长度直接 panic。
// 错误:
let text = bytemuck::cast_slice::<u8, u16>(&buf)?;
// buf.len = 3 时,panic: slice length is not a multiple of element size
// 正确:
let text = std::str::from_utf8(&buf[..buf.len() & !1])?;
// 忽略末尾的奇数字节
5.3 边界检查的差异
Zig 的 ReleaseFast 编译去除边界检查,Rust 的 Release 构建保留边界检查。移植过程中,一个占位符值导致了生产问题:
// Zig 原始代码(故意用了一个占位符)
// 内部化文件名的上限从 840 万降到了 27 万
// 真实项目确实会达到这个上限
// 同时,一个 ptrs[4095] 越界访问在 Zig ReleaseFast 下静默写入越界地址
// Rust 直接 panic
六、启示录:AI 驱动软件工程的工程学
6.1 速度 vs. 信任
Jarred 的原话:
"我预计开源软件会走向完全相反的方向——未来甚至可能变成'禁止人类贡献代码'。人类依然会负责讨论问题、决定优先级,但真正写代码、提交 PR、回复和处理反馈、完成实现的工作,最终都会由 LLM 来完成。"
Bun 的这次重写,正是这句话的第一次大规模公开演练。它证明了 AI 能够以人类无法企及的速度完成跨语言迁移——6 天 vs 3 周(当年手工移植 esbuild 的时间)。
但它也暴露了 AI 重写的典型问题:缺少人类审查(虽然有对抗式审查,但最终还是"Claude 写、Claude 审、Claude 合")、unsafe 泛滥、流程变成黑箱。
6.2 质量保证的新范式
传统软件工程依赖"人工代码审查"。但当一个 PR 包含 100 万行代码时,这个方法彻底失效。
Bun 的解决方案是三件套:
- 百万级断言的与语言无关测试套件:TypeScript 写的测试,覆盖所有行为,验收标准与运行时语言无关
- 对抗式审查智能体:独立的 Claude 上下文,只看 diff,任务就是找出问题
- 修复流程而非修补代码:出问题时,不是手动打补丁,而是修复生成代码的 prompt 和流程
6.3 unsafe 的工程意义
Rust 中 unsafe 不是"写得不安全",而是"这一块需要人工保证安全"。13,000 个 unsafe 中,78% 只有一行——调用 C 库或访问 C 指针。
这种精确性让 unsafe 的维护有了明确边界:
// 每个 unsafe 块都有 SAFETY 注释
unsafe {
// SAFETY: This pointer is valid because it came from Box::leak()
// in the match arm above, and we have transferred ownership to libuv
libc::free(ptr as *mut libc::c_void)
}
对比 Zig:整个代码库都是"unsafe"——没有编译器强制的不变量,全靠程序员自觉和风格指南。
6.4 对 CTO 和工程经理的启示
当 CTO 说"我们要把代码库从 X 语言重写成 Y 语言"时,Jarred 的实验给出了一个新答案:不问"需要几个月",而是问"Claude 需要几天"。
但这个答案附带了前提条件:
- 有一套完整的行为测试套件(TypeScript 是好选择,因为它与运行时语言无关)
- 有对抗式审查机制(人类或 AI 都行,关键是独立)
- 有足够的预算(16.5 万美元不是小数目)
- 团队愿意接受 19 个回归(并在合并后快速修复)
速度上天的时代,信任只能自己想办法落地。
七、后续:Rust 重写之后
合并 Rust 移植版之后,Bun 团队持续推进了以下工作:
安全审查: Claude Code Security 进行了 11 轮安全审查,所有发现的问题均已处理。
模糊测试: 为 Bun 中的每个解析器(JavaScript、TypeScript、JSX、CSS、JSON5、JSONc、TOML、YAML、Markdown、INI、Bun Shell 脚本、semver 范围、.patch 文件、CSS 颜色值)加了全天候覆盖率引导模糊测试。已执行 1000 亿次测试,产生约 15 个 PR。
unsafe 减少计划: 从忠实的 Zig 移植版逐步重构为更地道的 Rust,预计 unsafe 比例会继续下降。
生产验证: Prisma 在 Bun Rust 移植版上启动了 Prisma Compute 公测,报告内存泄漏问题已完全解决。Claude Code v2.1.181 及更高版本使用了 Bun 的 Rust 移植版,Linux 上启动速度快了 10%。
结语
Bun 的 Zig → Rust 重写,是 2026 年软件工程领域最具争议也最具启发性的事件之一。
它展示了 AI 在大规模代码迁移中的真实能力——速度、并行度、吞吐量都远超人类工程师。它也暴露了 AI 驱动的极限编程的典型陷阱——编译通过不等于语义正确,数量不等于质量,速度不等于信任。
但无论如何,这扇门已经被彻底撞开了。Jarred Sumner 用一条推文宣告了 Zig 版本的终结,也用 11 天的实验宣告了一个新时代的开始:未来,当你的 CTO 说"我们要把代码库从 X 语言重写成 Y 语言"时,他不会再问"需要几个月",而是会问"Claude 需要几天"。
作为工程师,我们需要准备的不是如何对抗这个趋势,而是如何在这个趋势中找到可靠的工程实践——更严格的测试、更独立的审查、更透明的工作流程,以及对"代码所有权"这一概念的全新理解。
Bun 的故事还远未结束。Rust 版本刚刚合并,生产环境的长期表现还需要时间检验。但它已经留下了足够的工程遗产,让整个行业不得不重新审视一个问题:
代码,到底是谁写的?
参考来源:Jarred Sumner 原文 "Rewriting Bun in Rust" (bun.com/blog/bun-in-rust),GitHub oven-sh/bun 仓库,Hacker News 讨论,Bun v1.3.14 / v1.4.0 Release Notes。