6天、96万行:一次被内存泄漏逼出来的语言迁移——Bun 从 Zig 到 Rust 的完整复盘
2026年5月,一件让整个技术圈震动的幕后事件悄然发生。
Bun——这个以"比 Node.js 快4倍"出道的 JavaScript 运行时和全能开发工具——它的 Zig 版本在 GitHub 仓库里出现了一个秘密分支 claude/phase-a-port。6天之后,96万行 Rust 代码写完,在 Linux x64 glibc 环境下通过了现有测试套件的 99.8%。然后,这个 Rust 版本直接合并进了主干,Zig 版本被判了死刑。
这不是一次普通的版本升级。这是一次用 AI 对抗熵增的极限工程实验——一个人均被内存泄漏折磨到秃头的团队,用 AI 写完了整个重写。
本文将从程序员的视角,完整复盘这次迁移的动机、技术路径、关键决策,以及它对整个生态系统意味着什么。
一、背景:Bun 是什么,为什么会有这个问题
要理解这次迁移的戏剧性,先得理解 Bun 是什么,以及它为什么会陷入这个困境。
1.1 Bun 的诞生与野心
Bun 是一个由 Jarred Sumner 创建的 JavaScript 运行时和全能开发工具。它的目标很简单:取代 Node.js + npm + TypeScript 编译器 + bundler + 测试框架这一整套工具链,给你一个开箱即用的"大一统"方案。
Bun 的核心卖点:
- 启动速度快:比 Node.js 快 4 倍
- TypeScript 原生支持:不需要单独编译
- 内置 bundler:不需要 Vite/Webpack
- 内置测试框架:不需要 Jest/Vitest
- npm 兼容:npm install 能跑
它的野心是:开发者只需要一个 bun 二进制文件,就能完成从初始化到部署的全部工作流。
1.2 Zig 的选择与代价
Bun 最初选择 Zig 作为实现语言,这不是随机的。
Zig 是一个系统级编程语言,定位是"C 的替代品,但没有 C++ 的包袱"。它的核心哲学是:显式优于隐式,没有隐藏的控制流,没有宏魔法,代码即配置。对于 Bun 这种需要精细控制内存布局、系统调用和跨平台编译的工具来说,Zig 听起来是非常合理的选择。
但问题也随之而来。
1.3 内存泄漏:一个慢性失血的故事
Bun 的 Zig 版本从一开始就存在内存管理问题。Jarred Sumner 本人在多次公开场合提到,Bun 的内存泄漏问题"修了一个又来一个",是一个持续数年的慢性失血。
内存泄漏在 JavaScript 运行时里是特别棘手的问题:
// Zig 的手动内存管理 + JavaScript 对象的生命周期
// 当 JS 对象和原生 Zig 内存交叉引用时
// GC 追踪链很容易出现遗漏
const bun GC = @import("bun").GC;
// 问题场景:一个 JS 对象持有一个指向原生 Buffer 的指针
// 但 JS 对象被 GC 时,原生 Buffer 的析构函数没有被正确调用
// 这就是典型的跨语言内存泄漏,确保 JS 运行时和 Zig 堆的引用计数一致本身就是复杂工程
内存泄漏在开发阶段不致命,但在生产环境中会慢慢积累,直到进程 OOM 被 kill。这是一个影响所有 Bun 用户的问题,但用 Zig 修了几个月,反反复复,始终没有根治。
这才是这次迁移的真正动机:不是 Rust 比 Zig 更好,而是内存泄漏在 Zig 里修不好。
二、迁移的起点:从 AI 开始写 Zig-to-Rust 迁移文档
2026年5月初,Bun 的 GitHub 仓库出现了 claude/phase-a-port 分支。
这不是一个冲动之举。在动手之前,团队先做了一件极其重要的事:写了一份 576 行的 PORTING.md 迁移指南。
这份文档把整个迁移分成了两个阶段:
Phase A:忠实翻译(Syntax Translation)
- 要求 Claude 逐文件翻译 Zig 代码为 Rust 代码
- 不要求编译通过:先忠实保留逻辑,Rust 代码暂时不能编译也没关系
- 这个阶段的目的是让 AI 专注于语言特性的一一映射
Phase B:编译通过(Cargo-fy)
- 逐个 crate 解决编译问题
- 处理 Rust 的借用检查(Borrow Checker)
- 适配 Rust 的 trait 系统和类型推导规则
这种分阶段策略非常关键——它把一个不可能一次性完成的任务,拆解成了两个相对独立的工作流。AI 在 Phase A 的任务是"翻译",不是"设计";在 Phase A 的输出基础上,Phase B 才处理 Rust 的类型系统问题。
Phase A: Zig → Rust 语法映射(不要求编译)
Phase B: 编译修复 + Borrow Checker 适配
三、Phase A 的执行:96万行的 AI 生成
3.1 核心挑战:Zig 与 Rust 的类型映射
Zig 和 Rust 虽然同属系统级语言,但设计哲学差异很大。Phase A 最大的挑战不是语法转换,而是类型和语义映射。
以下是几个典型的映射场景:
场景1:错误联合体(Error Set vs Result)
Zig 的错误处理用的是联合体:
const std = @import("std");
fn readFile(path: []const u8) ![]u8 {
// 如果出错,返回 error.FileNotFound 或其他错误
// 成功则返回数据
var file = try std.fs.cwd().openFile(path, .{});
defer file.close();
var content = try file.readAllAlloc(std.heap.page_allocator, std.math.maxInt(usize));
return content;
}
Phase A 翻译为 Rust:
use std::fs;
use std::io;
fn read_file(path: &str) -> Result<Vec<u8>, io::Error> {
let mut file = fs::File::open(path)?;
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
Ok(contents)
}
Zig 的 !T 语法(可错误返回类型)直接映射为 Rust 的 Result<T, E>。但问题来了:Zig 里错误集合(error set)是匿名的,而 Rust 需要显式声明错误类型。Phase A 生成的是"最接近语义等价"的 Rust 代码,具体的错误类型细化留在 Phase B。
场景2:defer 语句(RAII 模式)
Zig 的 defer 是显式的 RAII:
fn process() !void {
const resource = try allocate();
defer deallocate(resource); // 函数退出时自动调用
const conn = try connect();
defer conn.close(); // 按逆序 disentangle(后进先出)
try doWork(conn);
}
Rust 的等价为 RAII drop pattern,通常用 Drop trait 或作用域自动析构:
struct Resource {
// 封装了原生资源的类型
}
impl Drop for Resource {
fn drop(&mut self) {
// 等同于 Zig 的 defer 逻辑
deallocate(self);
}
}
fn process() -> Result<(), Error> {
let resource = allocate()?; // Resource 自动实现 Drop
let conn = connect()?;
drop(conn); // 如果需要提前 disentangle
do_work(&conn)?;
Ok(())
}
Phase A 生成的 Rust 代码会先用显式 drop() 调用模拟 defer 的精确行为,然后再在 Phase B 优化为 RAII 模式。
场景3:编译期计算(comptime vs const generics)
Zig 的编译期计算极为强大:
fn Matrix(comptime T: type, comptime rows: usize, comptime cols: usize) type {
return struct {
data: [rows * cols]T,
fn at(self: *@This(), r: usize, c: usize) *T {
return &self.data[r * cols + c];
}
};
}
Rust 的 const generics 可以近似表达:
struct Matrix<T, const ROWS: usize, const COLS: usize> {
data: [[T; COLS]; ROWS],
}
impl<T, const ROWS: usize, const COLS: usize> Matrix<T, ROWS, COLS> {
fn at(&mut self, r: usize, c: usize) -> &mut T {
&mut self.data[r][c]
}
}
但 Zig comptime 的能力远超 const generics——它可以在编译期执行任意代码,包括动态迭代、反射等。Phase A 只能做基本的近似映射,复杂的 comptime 逻辑在 Phase B 需要手写专门的编译期逻辑。
3.2 AI 翻译的具体操作模式
根据迁移文档的描述,Phase A 的执行模式是这样的:
- 逐文件翻译:按文件依赖顺序,逐一处理 Zig 源文件
- 命名规范转换:
snake_casevscamelCase,Zig 的camelCase方法名需要映射为 Rust 的snake_case - 可选参数处理:Zig 的可选参数(默认参数值)需要映射为 Rust 的 builder pattern 或 Option 参数
- block 注释保留:所有注释原样复制,确保 Phase B 的开发者能理解上下文
核心原则:不改变逻辑,只改变语法。Phase A 的 AI 任务是一个超大规模的语法转换器,不是一个设计器。
四、Phase B:从不能编译到 99.8% 测试通过
4.1 Rust 借用检查器的"灵魂拷问"
Phase A 生成96万行 Rust 代码后,所有文件的状态是:逻辑正确,但编译器不认。
最大的障碍来自 Rust 最著名也最令新手头疼的特性——借用检查器(Borrow Checker)。
Rust 的借用规则是:每个值同时只能满足以下条件之一:
- 有任意多个不可变引用(
&T) - 有且只有一个可变引用(
&mut T)
这个规则在 Zig 里根本不存在——Zig 的内存管理是手动的,没有这个限制。所以 Phase A 生成的代码,大量存在"在 Zig 里完全合法,但在 Rust 里违反借用规则"的情况。
举例:
// Zig:无借用限制
fn modifySlice(slice: []u8) void {
slice[0] = 42; // 直接修改
}
Phase A 映射为(可能出错):
// 初期版本:借用规则冲突
fn modify_slice(slice: &mut [u8]) {
slice[0] = 42; // 这里的借用规则取决于后续代码
}
// 问题场景:如果 slice 被同时 alias 了多个可变引用
fn tricky_case(slices: &mut [&mut [u8]]) {
let s1 = &mut slices[0]; // 拿走了 slices 的可变借用
let s2 = &mut slices[1]; // ⚠️ 这里 s1 和 s2 可能指向同一块内存,Rust 拒绝
}
Phase B 的工作就是系统性地解决这些借用规则冲突。这需要:
- 分析整个数据流,确定哪些引用是 alias 的
- 引入
Rc<RefCell<T>>或Arc<Mutex<T>>处理需要共享可变性的场景 - 使用
unsafe代码块包裹那些"确实安全但编译器无法自动证明"的逻辑 - 重构那些"共享状态设计"改为"拥有权清晰的设计"
4.2 6天完成 99.8% 测试通过意味着什么
从5月11日(Jarred Sumner 发推宣布秘密分支)到5月17日(宣布合并),整个重写只用了6天。
99.8% 的测试通过率背后的实际数字是:假如 Bun 的测试套件有10000个测试用例,6天后只有20个失败。这在工程层面是一个惊人的成就。
但更值得注意的是:99.8% 不是100%。那剩下的0.2%(按10000个测试算是20个)往往是最难啃的骨头——通常是涉及:
- 平台特定的 unsafe 代码(Linux x64 之外的其他平台)
- 与操作系统内核交互的边缘 case
- 需要更精细的内存管理策略的场景
五、为什么这次迁移能成功:工程方法的胜利
5.1 AI 作为翻译引擎,不是设计引擎
这件事能成功的第一个关键,是团队对 AI 能力的精准定位:把 AI 当翻译引擎用,而不是当设计引擎用。
AI 在编程中最强的能力是"模式匹配和转换"——给定一个输入和一个期望的输出,AI 能很好地完成映射。但 AI 的弱点是"全局优化"——它很难从整体架构层面做决策,特别是在需要权衡取舍的场景。
Phase A 的设计正是利用了 AI 的优势:语言特性的一一映射是一个"有明确规则可循"的转换任务,非常适合 AI。而 Phase B 的借用规则冲突解决需要全局视角,则由人类工程师完成。
AI 优势区间:语法转换、模式匹配、代码补全
人类优势区间:架构设计、全局优化、权衡取舍
5.2 渐进式迁移策略
Phase A + Phase B 的分阶段策略,是软件工程中经典的"先功能后优化"原则的又一次成功实践。
如果一次性要求"Zig 代码 → 可编译运行的 Rust 代码",AI 的推理空间太大,容易失败。但分阶段后,Phase A 的目标清晰且可验证——"语法等价 + 可读的 Rust 代码",Phase B 的目标同样清晰——"让编译器通过 + 测试通过"。
5.3 渐进式迁移的 Rust 最佳实践
这次迁移无意中示范了一个 Rust 社区极力推广的最佳实践:渐进式 Rust 化(Incremental Rustification)。
Rust 社区早就意识到,对于大型 codebase,不可能"一夜之间把 C++ 全部重写成 Rust"。正确的做法是:
- 外围包优先:从不涉及核心数据结构的模块开始翻译
- 稳定接口先行:先翻译公共 API,保持外部调用方式不变
- unsafe 兜底:先用 unsafe 包裹不安全的部分,编译通过后再逐步审计
- 测试驱动验证:每个模块翻译后立即运行测试套件验证行为等价性
Bun 的 Phase A → Phase B 策略,恰好与这套渐进式方法论高度吻合。
六、这次迁移对整个生态的启示
6.1 AI 编程工具的成熟度临界点
这次迁移最重要的信号不是"Bun 换语言了",而是**"用 AI 在6天内完成了本来可能需要几个月的工作"**。
这不是魔法——而是工具链成熟度达到临界点的标志。当 Claude Code 能以足够高的准确率完成96万行代码的翻译,并且只有0.2%的测试失败时,意味着 AI 辅助大型 codebase 重写已经具备了工业级可行性。
关键指标:
- 翻译准确率:~99.8% 的测试通过意味着逻辑保持高度一致
- 速度:6天 vs 预期数月 → 数量级压缩
- 成本:主要是 token 消耗 vs 工程师 months
6.2 内存安全语言对系统级项目的吸引力
Bun 从 Zig 迁移到 Rust 的根本原因是内存安全问题。这是一个非常重要的信号:
对于做基础设施软件的团队来说,"语言的生产力"和"语言的生态"是加分项,但"内存安全"是必选项。当一个语言在核心需求(运行时性能、系统控制)满足的前提下,开始频繁出现难以根除的内存安全问题,团队会认真考虑换一个在内存安全上有保障的语言。
Rust 的借用检查器虽然写起来比 Zig 费劲,但它的内存安全保证是编译期保证的——只要代码编译通过,就不会有 dangling pointer、data race 或 use-after-free。这是 Rust 在 Bun 团队决策天平上的终极砝码。
6.3 Zig 的位置:工具语言还是生产语言?
这次事件给 Zig 社区泼了一盆冷水,也让一个争论重新浮出水面:Zig 究竟是一个适合写生产级软件的系统语言,还是一个更适合做"嵌入式工具"(比如编译器前端、静态分析器、构建系统)的元语言?
Zig 的设计哲学是"显式优于隐式",没有 GC,没有 hidden allocations,所有行为都可以被静态分析和预测。这个设计让它在嵌入式场景和需要精控内存的场景中极具价值。但 Bun 的经历说明,在构建复杂的、涉及多语言交互(JavaScript ↔ Zig ↔ C lib)的生产级运行时时,手动内存管理的成本可能超过收益。
这个争论没有定论,但它会推动 Zig 社区更积极地思考"如何在保持零成本抽象的同时,提供更高级的内存管理工具"。
6.4 全 AI 重写大型 codebase 的可行性评估
这次迁移为整个行业提供了一个真实的量化案例:
| 指标 | 数值 | 意义 |
|---|---|---|
| 代码量 | 96万行 | 大型 codebase 级别 |
| 耗时 | 6天 | 相比传统方式快1-2个数量级 |
| 测试通过率 | 99.8% | AI 翻译质量极高 |
| 覆盖率 | Linux x64 glibc | 初期仅覆盖主力平台 |
可以预见,接下来会有更多团队尝试用 AI 完成大规模语言迁移。但 Bun 的案例也揭示了关键前提:
- 需要详细的迁移文档(PORTING.md 是关键)
- 需要分阶段策略(不可一次到位)
- 需要人工收尾(Phase B 的人类工程师不可或缺)
- 需要完整的测试套件(99.8% 的数字来自有10000+测试用例的套件)
没有测试套件,就没有99.8%这个数字。没有详细的迁移文档,AI 就没有方向。
七、从工程实践中提取的工具链方法论
7.1 大型 codebase 迁移的标准工作流
从 Bun 的案例中,我们可以提炼出一个可复用的大型 codebase 迁移工作流:
第一步:依赖分析(1-2天)
- 分析源语言的模块依赖图
- 识别核心模块和边缘模块
- 确定迁移顺序(先外围后核心)
第二步:编写迁移指南(2-3天)
- 逐语言特性编写映射规则文档
- 包含代码示例和边界情况说明
- 明确"禁止行为"(如 Phase A 不允许改变逻辑)
第三步:Phase A - 忠实翻译(并行化)
- 按依赖顺序翻译文件
- 每文件立即生成注释
- 不要求编译通过
第四步:Phase B - 编译通过(人工介入)
- 按模块逐一解决编译问题
- 优先处理借用规则冲突
- unsafe 兜底,不安全的地方先保证编译,后续审计
第五步:测试驱动验证
- 目标:>99% 测试通过
- 失败的测试用例逐一分析
- 区分"AI 翻译错误"和"原有测试本身的问题"
第六步:人工 review 和优化
- 代码风格一致性修正
- 性能优化(迁移后代码往往不是最优的)
- 文档更新
7.2 AI 辅助迁移的质量保障机制
Bun 的案例里有一个值得关注的细节:99.8% 的测试通过率,但并不是100%。那剩余的0.2% 需要有机制来保证质量。
核心的保障机制:
1. 双层测试套件
- 第一层:现有测试套件(保证行为不变)
- 第二层:迁移后的专项测试(针对 Rust 特性,如 drop 顺序验证、内存泄漏检测)
2. 渐进式合并
- 不一次性替换整个 Zig 版本,而是逐模块替换
- 每个模块替换后,与原模块进行行为对比测试
- 只有通过对比测试后才合并进主干
3. 人工审查重点
- 所有 unsafe 代码块必须有注释解释"为什么安全"
- 所有
Rc<RefCell<T>>或Arc<Mutex<T>>必须有注释说明"为什么需要共享可变引用" - Phase B 工程师的 review 重点不是"代码是否正确",而是"借用规则是否安全"
7.3 为什么选择了 Rust 而不是别的语言
这是一个有趣的反事实问题:如果目标是解决内存泄漏问题,为什么不选 Go(GC 语言,自动内存管理更简单)或者 C++(成熟的生态系统)?
答案是:在 Bun 的场景里,Rust 是唯一同时满足以下条件的语言:
- 零成本抽象(性能不妥协)
- 内存安全(编译期保证,无 GC)
- 强类型系统(与 TypeScript/JavaScript 的互操作需要精确的类型映射)
- 活跃的生态(WebAssembly 工具链、异步运行时成熟)
Go 虽然有 GC,但 GC 会引入不确定的暂停,对于 Bun 这种需要极致性能的运行时来说不可接受。C++ 同样能解决内存安全(通过 RAII),但 C++ 的类型系统不足以与 TypeScript 建立精确的双向映射,而 Bun 的 JS↔Native 互操作层需要这种精确性。
八、Rust 在 2026 年的生态位:工具链战争的新局面
8.1 这次迁移对 Rust 生态的影响
Bun 转向 Rust,对 Rust 生态有直接的信号意义:
正向信号:
- Rust 的学习曲线没有吓退有实际需求的团队(96万行代码6天完成)
- Rust 在 JavaScript 运行时这种核心基础设施场景有了实际落地
- AI 辅助 Rust 开发的范式被验证可行
生态补强:
- Bun 的 Rust 版本会带来新的 crates 使用反馈
- 迁移过程中发现的 Rust 痛点会推动 RFC 提案
- AI 辅助迁移的最佳实践会在 Rust 社区快速传播
8.2 2026 年的 Rust vs Zig vs Go 的定位分化
从 2026 年的视角看,三种语言在基础设施领域的定位正在分化:
| 维度 | Rust | Zig | Go |
|---|---|---|---|
| 内存安全 | 编译期保证 | 手动管理(更精细但更累) | GC(有暂停) |
| 学习曲线 | 最陡 | 中等 | 最平缓 |
| 性能 | 极高(无GC) | 极高(无GC) | 高(有GC开销) |
| 适用场景 | 需要极致安全的系统级组件 | 嵌入式/工具/编译器 | 网络服务/云原生 |
| 2026 趋势 | 生态快速扩张 | 生态稳步增长 | 企业采纳持续增长 |
Bun 的案例说明,在实际生产中,当"手动内存管理"成为开发效率和稳定性的瓶颈时,团队会愿意承担 Rust 的学习成本来换取内存安全保证。
九、这次迁移的局限与未解答的问题
客观地说,这次迁移还有以下局限:
1. 目前只覆盖 Linux x64 glibc
6天内完成的版本只覆盖了 Linux x64 glibc 这个主力平台。macOS (arm64 + x64)、Windows、WebAssembly 等平台的 Bun Rust 版本还需要更多工作。Bun 原来是全平台覆盖的,这次迁移的覆盖面打了折扣。
2. 0.2% 的测试失败意味着什么
剩余的测试失败可能涉及:
- 信号处理(Zig 和 Rust 在信号处理上语义不同)
- 平台特定的系统调用行为差异
- 竞态条件(data race 的 Rust 编译器检测会发现一些 Zig 里没暴露的问题)
这些失败的性质(是真的 bug 还是测试本身的问题)还需要进一步分析。
3. Rust 版本 vs Zig 版本的长期维护
Bun 团队接下来要面对的是:是否维护两个代码分支?还是彻底放弃 Zig 版本?如果彻底放弃 Zig,Zig 社区失去了一个重要的生产级项目案例。
4. AI 辅助迁移的可复制性
Bun 的案例是一个非常特殊的案例:有明确的目标(解决内存泄漏)、有完整的测试套件、有详细的迁移文档、有资深的工程师把控方向。一般团队复制这个模式时,能不能达到同样的成功率,是一个未经验证的假设。
十、总结与展望
Bun 用 6 天、96 万行代码完成了从 Zig 到 Rust 的迁移,这是一次被问题驱动、被 AI 加速、被方法论保障的工程实践。
它给我们的核心启示有三条:
第一条:工具决定生产力的天花板。 当 AI 能把"6个月的迁移"压缩为"6天的翻译",整个行业的基础设施现代化速度都会大幅提升。但前提是,这个 AI 工具需要配合良好的工程方法论使用——没有 PORTING.md 的规范,没有 Phase A/B 的分阶段策略,AI 生成的就是一堆垃圾。
第二条:内存安全是生产级基础设施的必选项。 Bun 的 Zig 版本修了几年内存泄漏,始终无法根治。这不是因为 Zig 团队不努力,而是因为手动内存管理在大规模、跨语言交互场景下的根本性困难。Rust 的借用检查器虽然写起来费劲,但它把内存安全这个"运行时的不确定性"变成了"编译期的确定性"——这个交换对生产级项目来说是值得的。
第三条:渐进式迁移是大型工程的标准答案。 从 96 万行代码的 Phase A 不要求编译通过,到 Phase B 逐模块解决编译问题,这个思路与 Rust 社区提倡的"渐进式 Rust 化"完全一致。它告诉我们:不要追求完美主义,不要追求一步到位,追求"今天能跑,明天更好"。
2026年,AI 辅助编程正在从"代码补全"走向"代码翻译",从"写新代码"走向"重构旧代码"。Bun 的案例是这个趋势的一个里程碑式的证明。
但工具永远只是工具。真正推动工程前进的,依然是人的判断、人的方法论、和人面对复杂问题时的那份耐心。
相关引用(信息来源):
- Bun GitHub
claude/phase-a-port分支及PORTING.md迁移文档 - Jarred Sumner 2026年5月11日 X/Twitter 公告
- Bun 测试套件(GitHub 仓库,MIT 协议开源)
- Rust 1.96.0 官方发布说明(
core::range系列新特性、wasm 链接行为变更)
标签: Rust|Zig|Bun|JavaScript运行时|AI编程|代码迁移|系统编程|内存安全|2026技术热点
Keywords: Bun Rust migration, Zig to Rust, AI code migration, memory safety, JavaScript runtime, 2026 tech trends