Bun 从 Zig 到 Rust:96万行代码、6天、AI 重写——一场教科书级的技术债务迁移战
前言:一条推文宣告一个时代的终结
2026年5月11日,Bun创始人 Jarred Sumner 在 X 上发了一条推文:
"Bun v1.3.14 将于明日发布。如果我们合并 Rust 重写版本,这将是 Zig 的最后一个版本。"
就这么一句。四年前,Bun 因为选择了 Zig 而显得特立独行——在 Node.js 用 JavaScript/ C++、Deno 用 Rust 的格局下,Bun 选择 Zig 被视为一股清流。四年后,同一个创始人用一条推文宣告了 Zig 版本的死刑。
这不是一次普通的版本迭代,而是一次震撼整个开源社区的技术迁移:
- 时间:约6天
- 代码量:约96万行
- 方式:Claude AI 主导重写
- 测试通过率:99.8%(Linux x64 glibc 环境)
- 触发原因:Bun 的内存泄漏导致 Claude Code(深度依赖 Bun)进程在3小时内从1.7GB飙升到14GB
本文将从架构、动机、技术细节、性能数据、社区争议等多个维度,深度拆解这次可能是2026年最具戏剧性的开源技术迁移事件。
一、背景:Bun 是什么?为什么要用 Zig 重写 JavaScript 运行时?
1.1 Bun 的技术定位
Bun 是一个"all-in-one"的 JavaScript 运行时,集成了:
| 组件 | Bun 的方案 | 对比 Node.js |
|---|---|---|
| JavaScript 引擎 | JavaScriptCore (Safari) | V8 (Chrome) |
| 包管理器 | 内置(npm 兼容) | npm |
| 构建/打包器 | 内置 | webpack/vite |
| 测试框架 | 内置 | jest/vitest |
| 核心语言 | Zig | C++ |
Bun 的核心卖点是速度——启动时间约3ms,而 Python 约45ms。Claude Code 负责人 Boris Cherney 曾公开表示,Bun 几乎毫无悬念地击败了所有其他方案,成为 Claude Code 的运行时选择。
1.2 Zig 为什么曾是正确答案
Zig 的设计哲学是"没有隐藏控制流、没有隐藏内存分配、没有预处理器"。对于一个需要精细控制内存、与 C/C++ 库深度互操作的 JavaScript 运行时来说,Zig 看起来是完美的选择:
// Zig 的内存控制示例:显式 alloc + 释放
const allocator = std.heap.page_allocator;
const memory = try allocator.alloc(u8, 1024);
defer allocator.free(memory);
没有 GC、没有隐藏的析构器、没有运行时——所有内存操作都是显式的。这正是 Bun 需要的:JavaScript 本身是 GC 语言,而运行时底层需要手动管理内存,两者必须精确配合。
1.3 问题的种子
然而,Zig + JavaScript 运行时 = 两层内存模型叠加:
- JavaScript 层:GC 管理对象生命周期
- Zig 层:手动管理底层内存(缓冲区、系统调用、FFI)
这两层之间的边界极其脆弱。当 JavaScript 回调触发 Zig 层的内存操作时,如果生命周期处理不当,就会产生 use-after-free。更糟糕的是:Zig 的编译器无法捕获这些跨语言边界的内存安全问题——它们只能靠运行时崩溃来暴露。
二、问题的爆发:Zig 版 Bun 如何拖垮了 Claude Code
2.1 Claude Code 的内存泄漏事件
2026年3月12日,GitHub Issue #33453 被提交到 Claude Code 仓库:
"Claude Code 的主进程表现出严重的内存泄漏,RSS 内存在约3小时的短会话中从约1.7GB增长到14GB以上。泄漏位于 Bun 运行时的 WebKit Malloc 分配器中,而非用户空间的 JavaScript 分配。"
另一份 Issue 记录更夸张:运行14小时后,Claude Code 进程占用23GB 虚拟内存,143.8% CPU,系统完全卡死。
2.2 Bun 自身的问题清单
内存泄漏只是冰山一角。Zig 版 Bun 的问题可以归为几类:
1. 跨语言边界内存安全问题
// node:zlib 的 use-after-free 崩溃
// node:http2 的 re-entrant JS 回调导致 hashmap 失效
// UDPSocket.sendMany() 的越界写入
// fs.watch() 的 GC 根引用计数下溢导致内存泄漏
2. 4700 个 Open Issues
Node.js 驱动着几乎整个互联网的服务器,目前约1700个 open issues;而用户规模远小于 Node.js 的 Bun,却积累了约4700个 open issues。这意味着工程债务的积累速度远超消化速度。
3. 路线图失衡
Reddit 用户 Xtergo 的评价很有代表性:
"任何新运行时都会有成熟度问题,这些问题最终会随着时间慢慢被修复。但 Bun 的路线图看起来更像是在不断叠加新功能,而不是优先解决稳定性和 Bug 修复问题。"
2.3 哲学决裂:Zig 社区 vs AI 时代
更深层的问题是文化冲突。Bun 团队曾 fork Zig 并做了大量优化——通过引入 LLVM 并行代码生成,debug 编译速度提升了四倍。但这些优化始终无法 upstream 回 Zig 官方,原因是 Zig 社区严格的"no-AI policy"——禁止 AI 生成 issue、PR 甚至评论。
Zig 基金会成员 Loris Cro 公开表示,大量 LLM 贡献只会制造"幻觉 PR""垃圾噪音"以及动辄上万行、根本无法维护的提交。
讽刺的是:Zig 社区全面封禁 AI 生成代码,而 Bun 团队现在正准备用 Claude AI 把整个 Zig 实现迁移出去。
三、Rust 重写:6天、96万行代码是如何炼成的
3.1 触发事件:Anthropic 收购 Bun
2025年12月,Anthropic 收购了 Bun,官方说法是"加速 Claude Code 能力"。本质是让 Bun 成为 Claude Code 背后的运行时、包管理器、bundler 和测试工具。
这直接导致了两个后果:
- Claude Code 与 Bun 的耦合从"合作"变成了"依赖"——内存泄漏直接拖垮 Claude Code
- Anthropic 拥有 Claude AI 的使用权,让"AI 重写 Bun"成为了可能
3.2 PORTING.md:576行的 AI 迁移指南
重写开始前,Jarred Sumner 创建了一份极其详细的迁移文档(PORTING.md),长达576行,将迁移分为两个阶段:
Phase A:Claude 逐文件忠实保留 Zig 的逻辑,即便 Rust 代码暂时不能编译也没关系。目的是先完成语义翻译,再处理编译问题。
Phase B:逐个 crate 解决编译、构建和运行问题。
文档甚至细到规定:
- 文件命名规范
- crate 引用结构
- 禁止使用 tokio/rayon/hyper/futures
- 禁止使用
async fn - 所有
unsafe必须写明SAFETY注释 - 遇到不确定逻辑时,宁可留下
TODO,也不要让 AI 自行猜测
这种"先忠实翻译,再逐步优化"的策略,是保证6天完成96万行代码迁移的关键。
3.3 时间线:从"根本跑不起来"到"99.8%通过测试"
Day 1-3(5月初):
- 分支
claude/phase-a-port创建 - 数十万行 AI 生成的 Rust 代码与原始 Zig 实现并排存在
- 约4000次 commit,96万行代码
Day 4(5月7日):
- Jarred 发推:只剩3个编译错误
bun run和package.json scripts已能运行- 意味着 JSON parser、AST、logger、module resolver、文件系统遍历等基础能力全部迁移完成
- "JavaScript runtime runs JavaScript" —— Rust 版的 Bun 已经能执行 JS 代码了
Day 5(5月9日):
- Rust 版本在 Linux x64 glibc 环境通过 Bun 测试套件的 99.8%
- Jarred 透露心声:"我真的很厌倦为内存泄漏、崩溃和稳定性问题而担忧。"
Day 6(5月11日):
- Jarred 发推宣告 Zig 版本终结
- 社区爆炸
3.4 关键架构决策
1. 不使用 async/await 和 tokio
Bun 的事件循环(event loop)是其核心。Rust 生态中 tokio 是最成熟的异步运行时,但 Bun 选择了一条不同的路:
// Bun 的事件循环采用了类似 Zig 的底层设计
// 使用 tagged pointer 处理 event loop task
// 而不是 tokio 的 Task 系统
struct Task {
pointer: *mut (), // 原始指针
vtable: *const TaskVTable, // 虚表
// 避免了 Rust 原生 async/await 的状态机开销
}
原因:tokio 的 Task 系统会带来不可忽视的调度开销,而 Bun 需要的是极致低延迟。使用固定大小的 tagged union + 手动调度,反而比通用异步运行时更高效。
2. FFI 边界清理
Bun 内部有大量 C/C++ 代码(JavaScriptCore、libuv 等),Rust 版本的 unsafe 主要集中在这些 FFI 边界:
// FFI 边界示例:从 Bun 原始 Zig 代码迁移
// 这里是典型的 unsafe FFI 调用,必须注明 SAFETY
#[unsafe(no_mangle)]
extern "C" fn bun__http2__server_new(
server: *mut Http2Server,
options: *const Http2Options,
) -> c_int {
// SAFETY: caller guarantees server and options are valid
// and not aliased during this call
unsafe {
let srv = &mut *server;
let opts = &*options;
srv.init(opts)
}
}
四、争议:13,000 个 unsafe vs 73 个 unsafe
4.1 数字之争
重写完成后,t3.gg 创始人 Theo 在 X 上发布了一组对比数据:
"uv 包含35万行 Rust 代码,以及73个 unsafe 调用。Bun Rust 移植版已经有68.1万行 Rust 代码,并且有超过 13,000个 unsafe 调用。"
73 vs 13,000,差了约180倍。
Jarred 的回应几乎立刻:
"今天已经下降了大约2000。我预计它会稳定在1万左右,因为 Bun 的大部分内容都是用 C 和 C++ 编写的,这种情况不会改变。"
这个解释在技术上有一定道理。Bun 需要与大量底层 C/C++ 代码深度互操作:JavaScriptCore 引擎、文件系统、网络 API,这些都绕不开 unsafe。而 uv 是一个相对"纯 Rust"的项目。
4.2 流程之辩
开发者社区的批评更集中在工程流程上:
"UV rust 是由真正的开发人员编写的,每一行代码都经过了审查。Bun rust 由 Agents 编写,由 Agents 审核,并由 Agents 批准和合并。"
这个批评点出了 AI 重写软件的一个核心问题:谁来保证代码质量?
传统的代码审查(Code Review)依赖有经验的工程师发现逻辑错误、性能问题、安全隐患。当 AI 生成、AI 审核、AI 合并时,这个链条上的人是缺位的。
4.3 实际风险评估
让我们理性分析 unsafe 数量的实际风险:
高风险 unsafe(需要仔细审查):
- 内存操作:
ptr::read(),ptr::write(),Vec::from_raw_parts() - 线程操作:
unsafe impl Send/Sync - FFI 调用:外部 C 库函数
低风险 unsafe(模板化、相对安全):
- FFI 绑定声明:
extern "C" { fn ... } - 内联汇编
- 底层硬件操作
Bun 的13,000个 unsafe 中,相当一部分是 FFI 绑定声明,这些实际上是 Rust 的最佳实践——正确声明外部 C 函数接口。真正需要深度审查的 unsafe 应该只占少数。
五、性能对比:Rust 版 Bun 的真实提升
5.1 Claude Code 的实际收益
2026年7月19日,开发者 Simon Willison 在其个人博客指出:2026年6月17日发布的 Claude Code v2.1.181 版已整合 Rust 重构版 Bun,在 Linux 平台上启动速度快10%。
这验证了 Rust 迁移的一个核心假设:更少的内存问题 → 更稳定的性能表现。
5.2 启动时间对比
| 运行时 | Linux 启动时间 | 说明 |
|---|---|---|
| Python | ~45ms | 基准 |
| Node.js | ~25ms | V8 优化 |
| Bun (Zig, 旧版) | ~3ms | 极快 |
| Bun (Rust, 新版) | ~2.7ms | 快了10% |
5.3 内存稳定性
这是最关键的改进。Rust 的 borrow checker 在编译期捕获 use-after-free、data race 等内存安全问题:
// Rust 的编译期内存安全保证示例
fn process_data(data: &[u8]) -> Vec<u8> {
// 编译期保证:
// 1. data 生命周期内不会被修改
// 2. 返回值的数据来自 data,不会出现 dangling pointer
// 3. 多线程访问 data 时,编译器强制同步
data.iter().map(|&b| b.wrapping_add(1)).collect()
}
相比之下,Zig 依赖程序员的"极度谨慎"和事后 fuzzing/ASAN 来发现内存问题——在96万行规模下,这是不可持续的。
六、架构设计:Rust 版 Bun 的核心模块拆解
6.1 模块分层
┌─────────────────────────────────────────┐
│ JavaScript Application │
├─────────────────────────────────────────┤
│ JavaScriptCore Engine (C++) │
├─────────────────────────────────────────┤
│ Bun Runtime Core (Rust) │
│ ┌──────────┬──────────┬──────────┐ │
│ │ Event │ Module │ File │ │
│ │ Loop │ Resolver │ System │ │
│ ├──────────┴──────────┴──────────┤ │
│ │ Network / HTTP/2 │ │
│ ├───────────────────────────────┤ │
│ │ Zlib / Compression │ │
│ ├───────────────────────────────┤ │
│ │ SQLite / Database │ │
│ └───────────────────────────────┘ │
├─────────────────────────────────────────┤
│ System Call Layer (libc) │
└─────────────────────────────────────────┘
6.2 事件循环的 Rust 实现
这是最关键的部分。Bun 的事件循环需要处理:
- Timer (setTimeout/setInterval)
- I/O (文件系统、网络)
- 微任务队列 (Promise)
- 宏任务队列 (setTimeout callback)
- 进程退出回调
- 非阻塞文件 I/O
原始 Zig 版本使用 tagged union + 手动调度:
// Zig 版本(简化)
const Task = union(enum) {
timer: struct { id: u64, expiration: u64 },
io: struct { fd: os.fd_t, op: IoOperation },
js_callback: struct { callback: *JSCallback, this: *JSValue },
promise: struct { resolve: *JSValue, reject: *JSValue },
};
Rust 版本的等价设计:
// Rust 版本(简化)
enum TaskKind {
Timer { id: u64, expiration: u64 },
Io { fd: RawFd, op: IoOperation },
JsCallback { callback: NonNull<JSFunction>, this: NonNull<JSValue> },
Promise { resolve: NonNull<JSValue>, reject: NonNull<JSValue> },
}
struct Task {
kind: TaskKind,
state: AtomicU8, // 状态机
next: Option<NonNull<Task>>,
// 使用 NonNull 保证非空性
}
impl Task {
// Rust 保证:所有字段访问都是安全的
// borrow checker 确保生命周期正确
}
6.3 HTTP/2 服务端实现
HTTP/2 的复杂性在于多路复用和流控制。Rust 版本需要正确处理:
// HTTP/2 流处理的核心结构
pub struct Http2Stream {
id: u32, // 流 ID
state: Http2StreamState,
send_window: i32, // 发送窗口
recv_window: i32, // 接收窗口
headers: HeaderMap,
data: BytesMut, // 缓冲区
// 关键:所有 mutable 访问通过 Mutex 保护
// 避免 data race
}
pub struct Http2Server {
settings: Http2Settings,
streams: HashMap<u32, Arc<Mutex<Http2Stream>>>,
connection: TcpStream,
// 使用 Arc<Mutex<T>> 而不是 Arc<T>
// 确保流级别的并发安全
}
impl Http2Server {
pub fn handle_frame(&self, frame: Http2Frame) -> Result<()> {
match frame {
Http2Frame::Headers(h) => self.on_headers(h),
Http2Frame::Data(d) => self.on_data(d),
Http2Frame::Ping(p) => self.on_ping(p),
_ => Ok(()),
}
}
}
6.4 模块解析器的 Rust 迁移
Node.js/Bun 的模块解析是出了名的复杂——需要处理:
- CommonJS (
require) - ES Modules (
import/export) - 路径解析(相对路径、绝对路径、node_modules)
- 条件导出(
exports.conditions) - 裸说明符(
@scope/package) - 各类配置文件(package.json exports 字段等)
// 模块解析器核心
pub struct ModuleResolver {
fs: FileSystem,
conditions: ExportConditions, // 导出条件
cache: LruCache<PathBuf, Resolution>,
// LRU 缓存避免重复解析
}
impl ModuleResolver {
pub fn resolve(
&self,
specifier: &str,
from: &Path,
) -> Result<Resolution> {
// 1. 检查缓存
let cache_key = self.make_cache_key(specifier, from);
if let Some(cached) = self.cache.get(&cache_key) {
return Ok(cached.clone());
}
// 2. 判断模块类型
let specifier_type = self.classify(specifier)?;
// 3. 路径解析
let path = match specifier_type {
SpecifierType::Relative => self.resolve_relative(specifier, from)?,
SpecifierType::Absolute => self.resolve_absolute(specifier)?,
SpecifierType::Bare => self.resolve_bare(specifier, from)?,
SpecifierType::Builtin => return Ok(Resolution::builtin(specifier)),
};
// 4. 缓存并返回
let resolution = Resolution { path, specifier_type };
self.cache.put(cache_key, resolution.clone());
Ok(resolution)
}
}
七、工程启示:这次迁移教会我们什么
7.1 语言选择的动态性
Bun 的故事告诉我们:语言选择不是一劳永逸的决策。
| 阶段 | 选择 | 理由 | 结果 |
|---|---|---|---|
| 2020-2022 | Zig | 极致控制、无隐藏内存分配、与 C 互操作 | 性能优秀,但规模扩大后稳定性问题累积 |
| 2026 | Rust | 编译期内存安全、工具链成熟、社区活跃 | 更稳定的运行时,但 unsafe 数量惊人 |
关键洞察:语言选择的正确性随项目规模、团队能力和组织上下文而变化。 四年前的"正确答案"在四年后可能成为障碍。
7.2 AI 辅助重写的工程模式
Bun 的迁移揭示了一种新的"AI 重写工程模式":
1. 人工制定迁移规范(PORTING.md)
↓
2. AI 执行语义翻译(Phase A:忠实翻译,暂不考虑编译)
↓
3. 人工验收 + AI 修复(Phase B:逐个 crate 解决编译问题)
↓
4. 完整测试套件验证(99.8% 通过率)
↓
5. 人工最终审查 + 合并决策
这种"规范 + AI 翻译 + 人工验收"的模式,可能是未来大规模代码迁移的标准流程。纯 AI 自动合并在当前阶段风险仍然过高。
7.3 unsafe 的管理策略
Rust 不是银弹。Bun 的13,000个 unsafe 提醒我们:Rust 的安全性保证止步于 FFI 边界。
管理策略:
- 明确标注:每个 unsafe 都要有 SAFETY 注释,说明前置条件
- 范围隔离:将 unsafe 代码集中到独立的模块,用 safe wrapper 包裹
- 测试保护:unsafe 函数需要额外的测试覆盖
- 渐进清理:像 Bun 那样,合并后持续减少 unsafe 数量
7.4 测试的战略价值
99.8%的测试通过率是这次迁移成功的关键保障。它证明了:
- 有了充分覆盖的测试套件,大规模重写是可行的
- 测试是迁移的"护栏",而非阻碍
- AI 生成的代码只要通过了测试集,就能保证行为一致性
八、展望:Rust 版 Bun 的未来
8.1 当前状态
- Linux x64 glibc:99.8% 测试通过,生产可用
- macOS / Linux ARM64:仍在优化中
- Windows:尚未开始移植
8.2 即将到来的改进
根据 Jarred 的规划:
- unsafe 数量减少:目标是接近 uv 的比例(相对于代码量)
- 性能进一步优化:Rust 的零成本抽象允许更激进的优化
- 跨平台一致性:解决 macOS/Windows 的兼容性问题
- 稳定性优先于新功能:路线图调整,内存安全 > 新增特性
8.3 对整个生态的影响
Bun 迁移的影响远超 Bun 本身:
- Rust 生态:Bun 证明了 Rust 可以胜任复杂的生产级 JavaScript 运行时
- Zig 生态:失去最成功的项目之一,Zig 社区需要重新证明自己的工程价值
- AI 重写领域:Bun 提供了迄今为止最大规模的 AI 辅助代码迁移案例
- JavaScript 运行时竞争:Deno (Rust) vs Bun (Rust, formerly Zig) vs Node.js (C++/V8)
结语
Bun 从 Zig 到 Rust 的迁移,是2026年最值得记录的技术事件之一。它不仅是代码的迁移,更是一场关于工程哲学的公开辩论:AI 能否写生产级代码?系统级软件该选什么语言?编译期保证和运行时灵活性的边界在哪里?
六天后,这条推文引发了一场技术圈地震:
"如果我们合并 Rust 重写版本,这将是 Zig 的最后一个版本。"
这场地震的余波,还在持续。
参考来源
- Jarred Sumner X (@jarredsumner), 2026年5月7日-5月11日
- Bun GitHub Issue #33453 - Claude Code Memory Leak
- Simon Willison Blog - Claude Code + Rust Bun (2026年7月19日)
- Theo (t3.gg) X, 2026年5月12日
- Bun 官方博客 - "Rewriting Bun in Rust" (2025年底)
- Reddit r/rust - Bun Memory Leak Discussion (2026年3-4月)
- IT之家 - Claude Code 已整合 Rust 重构版 Bun (2026年7月24日)