Bun 从 Zig 到 Rust 的史诗级重写:Claude Code 6天生成96万行代码,AI驱动的跨语言迁移如何重塑软件开发
一、事件全景:发生了什么?
2026年5月14日,Bun 团队在 GitHub 上合并了一个震惊整个技术社区的 Pull Request——PR #30412: Rewrite Bun in Rust。这个 PR 包含了:
- 6755 个提交
- 2188 个文件变更
- 新增代码超过 96 万行
- 耗时仅 6 天
这是整个 JavaScript 运行时生态历史上最大规模的语言迁移之一,也是 AI 驱动的大型工程实践中最公开、最具争议的一次实验。
Bun 创始人 Jarred Sumner 在 X(原 Twitter)上的一句宣告更是直接判了 Zig 版的死刑:
"Bun v1.3.14 将于明日发布。如果我们合并 Rust 重写版本,这将是 Zig 的最后一个版本。"
就这样,一个四年历史的、用 Zig 从零打造的 JavaScript 运行时,在不到一周的时间里被完全用 Rust 重写。而执行这场重写的不是一支百人团队——是 Claude Code。
这个事件的技术冲击波还在持续扩散。本文将从技术动因、迁移架构、代码实践、性能表现、争议分析、行业影响六个维度,深入剖析这场划时代的工程实践。
二、为什么要从 Zig 迁移到 Rust?
2.1 Bun 与 Zig 的技术绑定历史
Bun 自 2022 年诞生之日起就选择了 Zig 作为核心实现语言。这个选择在当时是合理的:
- Zig 的优势:作为系统级编程语言,Zig 提供了接近 C 的性能、更现代的语言特性,以及与 C/C++ 无缝互操作的能力。对于需要深度对接 V8、JavaScriptCore、BoringSSL 等底层 C 库的 JS 运行时来说,Zig 是一个天然的选择。
- 全家桶理念:Bun 的设计哲学是"一个工具搞定一切"——runtime、package manager、bundler、test runner 全部内置。这需要底层语言提供极高的灵活性和控制力,Zig 在这方面表现出色。
但四年下来,问题逐渐暴露。
2.2 核心问题:内存泄漏吞噬开发效率
根据 Claude Code 开发团队的反馈和社区讨论,Bun 在长期运行中暴露了严重的内存稳定性问题:
- Claude Code 主进程在 3 小时运行后内存膨胀至 14GB+
- 长时间运行导致系统卡死、CPU 过载
- Bun GitHub 仓库的 issue 数量高达 4700+,远超同类项目
- 多次针对性优化仅降低约 5% 内存使用,无法根治
问题的根源在于 Zig 的手动内存管理模型。Zig 不提供垃圾回收,依赖开发者手动管理分配器(allocator),通过 defer、deinit 等模式释放资源。在一个与 V8、JavaScriptCore、系统网络栈、文件系统等多层 C 库深度交互的运行时中,内存生命周期管理极其复杂。任何一个 alloc/deinit 的不匹配,都会导致缓慢的内存泄漏。
Bun 团队花大量时间在排查内存问题上。这些问题往往难以复现——它们只在特定的工作负载、特定的运行时长、特定的平台组合下才暴露。传统的 Valgrind、ASan 等工具能帮助定位部分问题,但在如此复杂的系统中,漏网之鱼永远存在。
2.3 Rust 的吸引力:编译器级别的安全保证
Rust 的核心卖点是 所有权系统(Ownership System)和 借用检查器(Borrow Checker)。这两个机制在编译时就能捕获大部分内存安全问题:
- 内存泄漏:Rust 的 RAII 模式确保资源在作用域结束时自动释放
- 悬垂指针:借用检查器在编译时阻止 dangling reference
- 数据竞争:
Send/Synctrait 在编译时保证线程安全 - Use-after-free:所有权语义从根本上消除了这类问题
Jarred Sumner 在补充说明中明确提到,这次重写"顺手修了一些内存泄漏和 flaky tests(Rust 最擅长的领域)"。这直接验证了 Rust 在这类问题上的优势——不是人更聪明了,而是编译器帮你守住了底线。
2.4 生态和工具链的考量
除了安全保证,Rust 在以下方面也对 Bun 有吸引力:
| 维度 | Zig | Rust |
|---|---|---|
| 编译器诊断 | 基础 | 世界级(rustc 的错误提示是标杆) |
| 工具链成熟度 | 相对年轻 | cargo 生态极其成熟 |
| 跨平台交叉编译 | 良好 | 通过交叉编译目标支持全面 |
| 社区和招聘 | 小众 | 庞大且快速增长 |
| FFI 互操作 | 原生优秀 | 通过 unsafe FFI 支持良好 |
| 静态分析 | 有限 | clippy、miri 等工具链完善 |
| 第三方库 | 精简但有限 | crates.io 超过 14 万个 crate |
Bun 团队特别强调了一个点:迁移后的二进制体积缩小了 3-8MB。对于追求极致性能的运行时来说,这直接改善了冷启动速度和部署成本。
三、迁移架构:PORTING.md 的工程智慧
这场迁移之所以能在 6 天内完成,核心不是 AI 多聪明,而是 Bun 团队精心设计了一套严格的迁移规则。这些规则集中体现在仓库中的 PORTING.md 和 .claude/workflows 文件里。
3.1 两阶段迁移策略
Phase A: 忠实翻译(Fidelity Translation)
├── 逐文件将 Zig 代码翻译为 Rust
├── 目标是精确捕捉逻辑,不要求立刻编译通过
├── 保持与原 Zig 文件相同的目录结构
└── 生成的是"草稿",允许存在编译错误
Phase B: 工程化修复(Engineering Cleanup)
├── 按 crate 粒度解决编译问题
├── 处理所有权、生命周期、平台差异
├── 性能优化和 unsafe audit
└── Windows 平台专项修复
这个设计非常聪明。 它把一个看似不可拆分的"跨语言重写"问题,分解成了两个可独立并行推进的阶段:
- Phase A 是 高吞吐的机械劳动——把 Zig 的控制流、数据结构、函数调用逐行翻译成 Rust 语法。这正是 AI 擅长的领域。
- Phase B 是 需要深度判断的工程工作——解决所有权冲突、优化内存布局、处理平台差异。这需要人类工程师和工具链的配合。
3.2 严格的约束规则
PORTING.md 中包含大量约束条件,这些约束是迁移质量的关键保证:
语言约束:
- 禁止使用
tokio、rayon等 async runtime(Bun 有自己的事件循环) - 禁止将同步代码改写为
async fn - 尽量减少第三方 crate 的引入
unsafe代码必须加注释说明原因
架构约束:
- 保持原有的数据结构设计不变
- 保持原有的模块组织方式不变
- 保持原有的 allocator 语义不变
- FFI 边界的 unsafe 只是从 Zig 世界搬到 Rust 世界
类型映射规则:
- Zig 的
[]u8→ Rust 的&[u8]或&mut [u8] - Zig 的
?T→ Rust 的Option<T> - Zig 的 allocator 参数 → Rust 的 RAII 或显式分配
- Zig 的
comptime→ Rust 的const fn或宏
3.3 Claude Workflows:AI 参与的流水线
Bun 仓库中的 .claude/workflows 目录揭示了一个成熟的 AI 辅助开发流水线:
.claude/workflows/
├── translate-zig-to-rust # Phase A: Zig 到 Rust 翻译
├── validate-translation # 对照规则验证翻译质量
├── fix-compilation # Phase B: 修复编译错误
├── lifetime-classification # 生命周期标注分类
├── unsafe-audit # unsafe 代码审查
├── windows-bughunt # Windows 平台专项
└── benchmark-regression # 性能回归检测
每个 workflow 都有明确的输入、输出和验证标准。这不是"让 AI 自由发挥",而是 把 AI 纳入一条精心设计的工程流水线。
关键洞察:开发者写的不再只是代码,还有 约束——迁移手册、类型映射规则、测试策略、review 标准、CI 检查、不允许改变的架构边界。在未来,"定义约束"将成为开发者越来越重要的产出。
四、技术深潜:代码层面的迁移实践
4.1 内存管理模型的转换
Zig 和 Rust 在内存管理上的差异是这次迁移的核心挑战。让我们通过实际代码来看看这两种语言在相同场景下的表达方式。
Zig 的 allocator 模式:
// Zig: 显式 allocator 传递
fn parseJson(allocator: std.mem.Allocator, input: []const u8) !ParsedData {
var parsed = try std.json.parseFromSlice(ParsedData, allocator, input, .{});
errdefer parsed.deinit();
// 处理逻辑...
// 调用者必须记得 deinit
return parsed;
}
// 调用方
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
const result = try parseJson(allocator, json_data);
defer result.deinit(); // 别忘了!
对应的 Rust 实现:
// Rust: RAII 自动管理
fn parse_json(input: &[u8]) -> Result<ParsedData, ParseError> {
let parsed: ParsedData = serde_json::from_slice(input)?;
// 返回值的所有权转移给调用方
// 当 ParsedData 被 drop 时,内部资源自动释放
Ok(parsed)
}
// 调用方
let result = parse_json(&json_data)?;
// result 离开作用域时自动释放,无需手动管理
在简单的函数中,两者的差异看起来不大。但在 Bun 这种拥有数百万行代码、数百个模块互相调用的复杂系统中,Rust 的 RAII 语义意味着 编译器帮你追踪每一个资源的生命周期,而不是依赖开发者的记忆力。
4.2 FFI 边界的处理
Bun 作为 JS 运行时,大量通过 FFI 与 C/C++ 库交互。这是迁移中最棘手的部分。
Zig 的 FFI:
// Zig 与 C 互操作是原生支持的
extern "c" fn JSObjectGetProperty(ctx: *JSContext, obj: *JSObject, name: *JSStringRef, exception: *?*JSValue) *JSValue;
pub fn getProperty(ctx: *JSContext, obj: *JSObject, name: []const u8) !*JSValue {
var exception: ?*JSValue = null;
const js_name = JSStringCreateWithUTF8CString(name.ptr);
defer JSStringRelease(js_name);
const result = JSObjectGetProperty(ctx, obj, js_name, &exception);
if (exception) |exc| {
return error.JSError;
}
return result;
}
Rust 的 FFI:
// Rust 需要通过 unsafe 块进行 FFI
extern "C" {
fn JSObjectGetProperty(
ctx: *mut JSContext,
obj: *mut JSObject,
name: *mut JSStringRef,
exception: *mut *mut JSValue,
) -> *mut JSValue;
}
pub fn get_property(ctx: *mut JSContext, obj: *mut JSObject, name: &[u8]) -> Result<*mut JSValue, JSError> {
let mut exception: *mut JSValue = std::ptr::null_mut();
let js_name = unsafe { JSStringCreateWithUTF8CString(name.as_ptr()) };
defer unsafe { JSStringRelease(js_name) };
let result = unsafe { JSObjectGetProperty(ctx, obj, js_name, &mut exception) };
if !exception.is_null() {
return Err(JSError::from_value(exception));
}
Ok(result)
}
注意这里的几个关键差异:
- 可变性标注:Rust 要求显式标注
*mutvs*const,而 Zig 通过const限定参数 - unsafe 边界:Rust 将所有 FFI 调用标记为 unsafe,编译器强制你意识到这是不安全操作
- 空指针检查:Rust 提供了
std::ptr::null_mut()等安全的空指针表达
Bun 的 PORTING.md 明确规定:FFI 边界的 unsafe 只是从 Zig 世界搬到 Rust 世界,语义不变。 这意味着迁移后的 unsafe 数量会很高(后面会讨论这个争议),但至少每个 unsafe 都有明确的来源和注释。
4.3 事件循环和并发模型
Bun 拥有自己的事件循环实现,不依赖 libuv(Node.js 的选择)也不依赖 tokio。这是 Bun 性能优势的来源之一,也是迁移中最需要小心保护的部分。
// Rust 版 Bun 的事件循环核心(简化示意)
pub struct EventLoop {
epoll_fd: RawFd,
timer_heap: BinaryHeap<TimerEntry>,
pending_tasks: VecDeque<PendingTask>,
// 保持与 Zig 版相同的数据结构
}
impl EventLoop {
pub fn run(&mut self) -> RunResult {
loop {
// 1. 检查定时器
self.process_timers()?;
// 2. 等待 I/O 事件(epoll/kqueue/IOCP)
let events = self.poll_io_events(0)?;
// 3. 处理 I/O 回调
for event in &events {
self.handle_io_event(event)?;
}
// 4. 处理微任务
self.drain_microtasks()?;
}
}
}
Bun 团队特别强调了 不引入 async Rust。这意味着整个运行时仍然使用同步模式 + 自己的事件循环,这与 Node.js 的 libuv 模式类似。这个选择是有技术道理的:
- Bun 的事件循环已经经过大量生产验证
- 引入 async runtime(tokio 等)会带来额外的抽象层和性能开销
- 同步代码更容易推理和优化
4.4 测试策略:迁移的信心基础
没有测试的迁移就是赌博。Bun 这次迁移能如此快速推进,一个关键前提是 Bun 已经拥有了相当完整的测试套件。
迁移的测试策略是:
- 翻译后立即跑既有测试:Phase A 的翻译产出,用 Bun 原有的测试套件验证功能正确性
- 全平台覆盖:macOS(x86_64 + ARM64)、Linux(x86_64 + ARM64)、Windows
- canary 灰度:先通过
bun upgrade --canary让愿意尝鲜的用户测试 - benchmark 回归:每次修改后对比性能数据,防止迁移引入性能退化
据官方说明,Rust 版 Bun 已经通过既有测试套件的全平台测试,Linux 平台通过率达到了 99.8%。
五、性能数据:迁移后的真实表现
5.1 官方 Benchmark
Jarred Sumner 在 PR 描述中给出的 benchmark 结论是:"结果从持平到更快"。具体来说:
| 指标 | Zig 版 | Rust 版 | 变化 |
|---|---|---|---|
| 二进制体积 | 基准 | 缩小 3-8MB | ✅ 显著改善 |
| 冷启动时间 | 基准 | 持平或更快 | ✅ |
| HTTP 请求吞吐 | 基准 | 持平或略快 | ✅ |
| npm install 速度 | 基准 | 持平 | ➡️ |
| 构建(bun build) | 基准 | 持平或更快 | ✅ |
| 内存使用 | 基准 | 降低 | ✅ |
二进制体积的缩小是一个特别值得关注的指标。Rust 编译出的二进制通常比 Zig 更大(因为 Rust 标准库和 LLVM 后端可能生成更多代码),但 Bun 通过减少第三方依赖、优化编译配置,实现了反向缩小。这说明 Rust 版的代码组织可能更加精简。
5.2 内存问题的改善
虽然官方没有给出精确的内存对比数据,但结合以下信息可以合理推断:
- Claude Code 主进程从 3 小时膨胀到 14GB+ 的问题有望改善
- 多个长期存在的内存泄漏被修复
- Rust 的 RAII 语义从根本上减少了 alloc/deinit 不匹配的风险
不过需要注意:这些改善目前还停留在 canary 阶段。真正的生产级验证需要足够多的用户在真实工作负载下运行足够长的时间。
5.3 如何在你的项目中测试
如果你已经在使用 Bun,可以用以下方式体验 Rust 版:
# 升级到 canary 版本(Rust 重写版)
bun upgrade --canary
# 运行你的测试套件
bun test
# 检查项目构建
bun build ./src/index.ts --outdir ./dist
# 对比关键指标
# 冷启动
time bun run src/index.ts
# 安装依赖
time bun install
# HTTP 服务性能
bun run --hot src/server.ts
六、争议分析:13000+ unsafe 是不是问题?
6.1 Unsafe 代码数量对比
这是社区讨论最激烈的话题。让我们看看数据:
| 项目 | unsafe 数量 | 代码行数 | unsafe 密度 |
|---|---|---|---|
| uv(Python 包管理器) | ~73 | ~35万 | 0.021% |
| Bun(Rust 版) | ~13000+ | ~68万 | 1.9% |
Bun 的 unsafe 密度是 uv 的 90 倍。这看起来很吓人,但需要结合上下文理解:
为什么 Bun 的 unsafe 这么多?
- FFI 密度极高:Bun 需要直接调用 JavaScriptCore、BoringSSL、zlib、libiconv 等数十个 C/C++ 库。每个 FFI 调用都是 unsafe。
- 运行时底层特性:作为 JS 运行时,Bun 需要直接操作内存布局、处理 GC 交互、管理 JIT 编译的内存区域。
- 从 Zig 直接翻译:Zig 本身就是"安全的 C",大量底层操作在 Zig 中是正常的,翻译到 Rust 自然变成 unsafe。
更合理的对比对象是什么?
不应该拿 uv 和 Bun 比。uv 是一个相对高层的工具,大部分工作不涉及底层系统调用。更合理的对比对象是同样深度对接 C/C++ 生态的项目:
- Servo(浏览器引擎):unsafe 密度也很高
- Firefox 的 Rust 组件:同样有大量 unsafe
- Rust 编译器自身:unsafe 密度远超普通项目
6.2 开发模式的争议:AI 生成 + AI 审查?
另一个争议点是开发流程。传统开源项目的模式是:
人工编写代码 → 人工 Code Review → 人工合并
Bun 这次给人的印象是:
AI 生成代码 → AI 审查 → AI 合并
这种模式在开源社区引发了担忧。核心问题是:谁来为 AI 生成的代码质量负责?
我的看法:这种担忧是合理的,但需要分层看待:
- Bun 团队确实设了人。PORTING.md、workflow 规则、测试套件都是人写的。AI 是在人类定义的约束框架内工作。
- Claude Code 不是完全自主。它是根据
.claude/workflows中的指令执行的,这些指令来自 Bun 团队。 - 最终合并决策是人做的。Jarred Sumner 作为 PR 作者,对最终的合并结果负责。
真正的风险不在"AI 写了代码",而在"人类对 AI 输出的审查不够深入"。当一个 PR 包含 6755 个提交、96 万行代码时,任何人都无法逐行 review。这就是问题的本质——不是 AI 替代了 review,而是大规模迁移本来就不可能逐行 review。
6.3 "6 天重写"的真相
"6 天 96 万行"这个数字很有传播力,但容易产生误导。实际情况是:
- 这不是从零开始设计新架构,而是在已有架构的基础上做跨语言翻译
- Bun 团队在此前已经做了大量的准备工作(PORTING.md、workflow 规则、测试套件)
- Phase A(翻译)的速度确实惊人,但 Phase B(工程化)的工作还在持续
- 进入稳定版前还需要大量的 canary 测试和修复
更准确的描述应该是:在已有完善工程基础和严格规则的前提下,AI 辅助的跨语言迁移可以在极短时间内完成 Phase A 的高质量翻译。Phase B 的工程化仍然需要持续的人类投入。
七、行业影响:AI 驱动的软件开发新时代
7.1 开发范式的三层结构
Bun 事件标志着软件开发正在进入一个三层范式:
┌─────────────────────────────────────┐
│ 第一层:AI 负责吞吐 │
│ 生成草稿、批量迁移、自动验证 │
├─────────────────────────────────────┤
│ 第二层:工具负责约束 │
│ 编译器、类型系统、lint、CI │
├─────────────────────────────────────┤
│ 第三层:人负责判断 │
│ 架构决策、风险判断、发布节奏 │
└─────────────────────────────────────┘
以前开发者主要产出代码。以后在很多大型工程中,开发者的重要产出会变成 约束——迁移手册、类型映射规则、测试策略、review 标准、CI 检查、不允许改变的架构边界。
"程序员设计一个能让 AI 安全工作、能让错误尽早暴露的工程系统"——这可能是未来开发者更准确的描述。
7.2 哪些迁移会变得可行?
过去因为人力成本太高而长期搁置的技术债,可能会被重新评估:
- C/C++ 到 Rust 迁移:大型 C++ 代码库的安全加固
- 遗留脚本工具链重构:将 Python/Perl 脚本中的热点迁移到 Rust
- 内部 DSL 到标准语言:将公司内部 DSL 迁移到更标准、更易招人的语言
- 多年无人敢碰的遗留系统重写:AI 先生成 70% 的可审查草稿,人类再精细打磨
7.3 对编程语言生态的影响
Bun 迁移到 Rust 后,主流 JavaScript 运行时的实现语言分布变成了:
| 运行时 | 实现语言 | 备注 |
|---|---|---|
| Node.js | C++ | libuv + V8 |
| Deno | Rust | 从 Go 迁移而来 |
| Bun | Rust | 从 Zig 迁移而来 |
| JerryScript | C | 嵌入式场景 |
JavaScript 世界的运行时正在向 Rust 集中。这不意味着 C++ 或 Zig 会被淘汰——Node.js 的 C++ 基础极其稳固,Zig 在嵌入式和 WebAssembly 领域有其独特优势。但它确实反映了 Rust 在系统编程领域的吸引力。
7.4 对开源社区的启示
Bun 事件提出了几个值得每个开源维护者思考的问题:
- AI 贡献如何 review? 当一个 PR 包含数千个提交时,传统 review 流程失效了。
- AI 生成代码的许可证是什么? Claude 生成的代码归属如何界定?
- 开源项目是否应该接受 AI 大规模贡献? 社区需要新的贡献规范。
- 如何验证 AI 迁移的语义正确性? 测试通过不等于语义一致。
八、实战指南:如何在自己的项目中借鉴这种方法
8.1 前提条件
不是所有项目都适合 AI 驱动的跨语言迁移。你需要:
- 完整的测试套件:这是最关键的前提。没有测试,迁移就是盲人摸象。
- 清晰的架构文档:AI 需要理解系统边界才能正确翻译。
- 明确的约束规则:什么能改、什么不能改、类型如何映射。
- 可控的迁移范围:先迁移核心模块,验证后再扩展。
8.2 迁移检查清单
□ 评估迁移收益(内存安全、性能、生态、招聘)
□ 整理现有测试覆盖率报告
□ 编写架构文档和模块依赖图
□ 制定类型映射规则和约束清单
□ 设计两阶段迁移策略(翻译 + 工程化)
□ 设置 canary 灰度发布流程
□ 建立 benchmark 基线
□ 制定 rollback 方案
8.3 从小规模开始
如果你想在项目中尝试 AI 辅助的代码迁移,建议从以下场景开始:
# 1. 选一个小模块(< 5000 行)
# 2. 用 Claude Code 或类似工具辅助翻译
# 3. 保持原有接口不变,只改实现
# 4. 用既有测试验证功能正确性
# 5. 用 benchmark 验证性能无退化
# 6. Code Review 重点关注 unsafe 边界和语义正确性
九、总结:一个信号,而非终局
Bun 从 Zig 到 Rust 的重写,是 2026 年最值得关注的技术事件之一。但它的意义不在于"Bun 选了 Rust"或"AI 6 天写了 96 万行代码"。
它真正的意义在于展示了一种 可能的未来软件开发模式:
- AI 在清晰约束下的高吞吐代码生成能力已经成熟
- 编译器级别的安全约束(Rust 的类型系统和借用检查器)可以弥补 AI 生成代码的风险
- 人类开发者的角色正在从"写代码"转向"定义约束和做判断"
- 大规模技术迁移的尝试成本正在显著降低
但风险同样真实存在:
- 13000+ unsafe 代码需要长期的安全审计
- 大规模 AI 生成代码的可维护性有待时间验证
- canary 阶段可能暴露目前未发现的问题
- 开源社区的贡献规范和责任界定需要重新思考
Bun 的这次重写不是终局,而是一个信号。 它告诉我们,软件开发的方式正在发生根本性的变化。AI 不会替代程序员,但会让程序员的工作方式、关注焦点和价值产出发生深刻转变。
未来的程序员,可能更多时候不是在写代码,而是在设计一个系统——这个系统能让 AI 安全地工作,能让错误尽早暴露,能让人类判断留在最关键的地方。
这既是挑战,也是机遇。
参考资源:
- GitHub PR:oven-sh/bun#30412 - Rewrite Bun in Rust
- Bun 官方博客和发布说明
- Hacker News 讨论:Bun implementation is being ported from Zig to Rust
- PORTING.md 和 .claude/workflows(Bun 仓库)
- 社区技术分析和 benchmark 数据