编程 Bun 用 Claude 11天完成 Zig → Rust 全量重写:代码、哲学与工程权衡的完整复盘

2026-07-19 12:44:49 +0800 CST views 14

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 stashgit 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 天):

指标数量
未缓存输入 Token59 亿
输出 Token6.9 亿
缓存输入 Token720 亿
按 API 定价约 16.5 万美元

测试覆盖数据:

平台expect() 调用次数测试用例数测试文件数
Debian 13 x641,386,82660,6244,174
macOS 14 arm641,259,95358,8504,175
Windows 2019 x641,007,54457,3374,173

0 个测试被跳过或删除。

4.2 内存占用改善

Rust Drop trait 解决了 Zig 中难以表达的资源清理逻辑。在同一进程中把同一个 60 模块项目打包 2,000 次:

构建次数Bun v1.3.14(Zig)Bun v1.4.0(Rust)
5001,914 MB526 MB
1,0003,506 MB586 MB
1,5005,097 MB608 MB
2,0006,745 MB609 MB

v1.3.14 中,每次构建永久泄漏约 3 MB。开发服务器等长时间运行的工具最终会耗尽内存。

4.3 二进制体积缩小

平台v1.3.14v1.4.0缩小比例
Linux88 MB70 MB~20%
Windows94 MB76 MB~19%

macOS 上缩小 5.5 MB。主要原因:Zig 代码中大量 comptime 参数导致二进制膨胀,Rust 的泛型具化策略更紧凑。

4.4 性能提升

Linux x64 (EC2, Xeon Platinum 8488C) 基准测试:

HTTP 吞吐量(req/s):

服务器v1.3.14v1.4.0提升
Bun.serve169.6k177.7k+4.8%
node:http103.8k108.5k+4.5%
express64.5k66.6k+3.2%
fastify91.5k95.9k+4.8%

应用/CLI 工作负载:

工作负载v1.3.14v1.4.0提升
next build13.62s13.03s+4.5%
tsc -b --force0.94s0.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 的解决方案是三件套:

  1. 百万级断言的与语言无关测试套件:TypeScript 写的测试,覆盖所有行为,验收标准与运行时语言无关
  2. 对抗式审查智能体:独立的 Claude 上下文,只看 diff,任务就是找出问题
  3. 修复流程而非修补代码:出问题时,不是手动打补丁,而是修复生成代码的 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。

推荐文章

GROMACS:一个美轮美奂的C++库
2024-11-18 19:43:29 +0800 CST
rmux Test
2026-05-22 18:48:45 +0800 CST
浏览器自动播放策略
2024-11-19 08:54:41 +0800 CST
CSS 媒体查询
2024-11-18 13:42:46 +0800 CST
JS中 `sleep` 方法的实现
2024-11-19 08:10:32 +0800 CST
JavaScript 策略模式
2024-11-19 07:34:29 +0800 CST
JavaScript设计模式:发布订阅模式
2024-11-18 01:52:39 +0800 CST
程序员茄子在线接单