11天重写百万行代码:JavaScript运行时Bun从Zig到Rust的史诗级迁移,揭示了AI编程的哪些新范式?
2026年7月8日,JavaScript运行时Bun的创始人Jarred Sumner在博客中宣布:借助Claude Fable 5模型,耗时11天、耗费16.5万美元,已将整个Bun从Zig语言完全重写为Rust。6755个commit、100万行新增代码、99.8%的测试通过率、二进制体积缩小3-8MB——这是一场被整个技术社区称为"软件工程史上前所未有"的迁移实验。
本文将从技术动因、实施细节、架构选择、性能表现、行业影响五个维度,对这次事件进行深度拆解。我们不只讨论"Bun换语言"本身,而是要搞清楚:AI辅助的大规模跨语言重写,究竟意味着什么?它将如何重塑未来的软件开发方式?
一、背景:Bun为什么选择Zig,又为什么离开它?
在讨论迁移之前,我们需要理解Bun最初为什么选择Zig。
Bun诞生于2022年,定位是一个"一体化JavaScript运行时和工具链",目标是取代Node.js和部分构建工具的功能。它的核心卖点是性能——启动速度比Node.js快4倍,包管理速度比npm快25倍,内置TypeScript转译、Bundler等功能。Jarred Sumner在设计Bun时,选择Zig作为底层实现语言,这个决定背后有几个关键考量:
1.1 Zig的设计哲学与Bun的契合点
Zig是一门由Andrew Kelley于2015年开始设计的系统编程语言,它的核心哲学是**"没有隐藏控制流、没有隐藏内存分配、没有宏抽象"**。Zig的设计者刻意回避了许多现代语言的高级特性,选择让程序员对底层行为有完全的控制权。
这与Bun的需求高度吻合:
// Zig风格:直接、显式、无隐藏行为
const std = @import("std");
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
// 一切都是显式的,没有GC,没有隐藏开销
}
Zig没有垃圾回收器、没有运行时,这让它能生成极小的二进制文件,且能精确控制内存分配。对于一个需要极致性能的JavaScript运行时来说,这些特性非常诱人。
1.2 Zig的问题:内存泄漏与维护困境
然而,Bun在Zig上运行了四年后,问题逐渐暴露。Jarred Sumner在事后总结中坦承,Zig语言本身的稳定性问题是导致迁移的主要原因。具体表现为:
第一,Zig语言本身处于活跃开发期,breaking changes频繁。 Zig尚处于1.0之前的阶段,标准库和语言特性的演进速度非常快,这意味着Bun需要不断跟进语言本身的变更,维护成本极高。每次Zig升级都可能带来API变化,导致Bun代码需要大规模调整。
第二,Zig的内存管理模型虽然灵活,但也容易导致内存泄漏。 Bun在四年运营中积累了大量的内存泄漏问题,虽然Zig给了程序员精确控制内存的能力,但这种控制也意味着更高的出错概率。在没有成熟工具链支撑的情况下,内存泄漏的排查和修复变得极为耗时。
第三,Zig的工具链和生态相对薄弱。 相比Rust拥有cargo、crates.io、rust-analyzer等成熟工具,Zig的包管理器、IDE支持、性能分析工具都还不够完善。对于一个需要长期维护的大型项目来说,生态的成熟度直接决定了开发效率。
2026年5月,Jarred在GitHub上收到了一个由Claude Code生成的"phase-a-port"分支,包含约40万行由AI生成的Rust代码。随后,整个迁移以惊人的速度推进——11天后,整个100万行级别的代码库完成了从Zig到Rust的迁移。
二、实施过程:AI辅助大规模代码迁移的工程实践
2.1 为什么是Claude Fable 5?
这次迁移之所以能以如此惊人的速度完成,Claude Fable 5模型是关键。根据Jarred Sumner的描述,他们选择Claude Fable 5有几个重要原因:
首先是上下文窗口。 100万行代码的迁移,需要模型能够理解整个代码库的上下文,并在生成代码时保持一致性。Claude Fable 5的超长上下文窗口(据报道超过100万token)使得模型能够在生成新代码时参考现有代码的风格、模式和约定。
其次是代码生成质量。 不同于自然语言生成,代码生成要求模型理解语法语义、类型系统、调用约定等复杂规则。Claude Fable 5在编程任务上的优化,使其生成的Rust代码质量足以通过编译器和测试。
第三是成本效率。 16.5万美元的成本,换来的是11天完成原本可能需要数月的迁移工作。按照传统软件工程的估算,这样规模的跨语言重写可能需要5-10名工程师耗时6-12个月。以此计算,AI辅助迁移的成本优势是数量级的。
2.2 迁移策略:渐进式还是一次性?
Bun选择了一次性大规模重写的策略,而非渐进式迁移。这与传统的"镀金"(strangler fig)模式形成了鲜明对比。Jarred在博客中解释了这个选择:
"Zig和Rust之间没有好的渐进式迁移路径。两者都是编译型语言,但它们的内存管理模型、错误处理机制、标准库设计都完全不同。如果要同时维护两个版本的Bun,需要在两个语言之间维护一个中间抽象层,这会引入更多的复杂性。"
这个决策背后有一个关键假设:相信AI生成的代码能够一次达到生产级质量。而最终的结果验证了这个假设的可行性——99.8%的测试通过率,证明了迁移后的Rust代码在功能正确性上与原有Zig代码等价。
2.3 迁移的技术挑战与解决方案
100万行代码的迁移,面临的挑战远不止"翻译语法"。以下几个问题是最具代表性的:
挑战一:内存管理模型的转换。
Zig的内存管理是手动的,程序员需要显式分配和释放内存。而Rust的所有权系统虽然也是手动管理内存,但通过借用检查器在编译期防止了大多数内存错误。在迁移过程中,最重要的工作之一是将Zig的手动内存管理转换为Rust的安全内存模型。
// 原来的Zig代码(简化示例)
pub fn read_file(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
const content = try file.readToEndAlloc(allocator, 1024 * 1024);
return content;
}
// 迁移到Rust后
use std::fs;
use std::path::Path;
pub fn read_file<P: AsRef<Path>>(path: P) -> Result<Vec<u8>, std::io::Error> {
let file = fs::File::open(path)?;
// Rust的Drop机制自动处理文件句柄关闭
let metadata = file.metadata()?;
let mut buffer = vec![0u8; metadata.len() as usize];
file.read_exact(&mut buffer)?;
Ok(buffer)
}
挑战二:错误处理模式的重构。
Zig使用!T语法表示可能返回错误的类型,通过try、catch、if (err)等关键字处理错误。Rust使用Result<T, E>枚举,通过?操作符传播错误。两者的语义相近,但语法不同。
// Rust的错误处理:简洁、组合式
fn load_config(path: &str) -> Result<Config, ConfigError> {
let content = std::fs::read_to_string(path)?; // ? 自动传播错误
let config: Config = toml::from_str(&content)
.map_err(|e| ConfigError::ParseError(e.to_string()))?;
Ok(config)
}
挑战三:并发模型的适配。
Bun是一个高并发的服务器运行时,需要高效地处理大量并发的I/O操作。Zig使用libuv作为底层I/O库,而Rust有多种异步运行时选择。Bun选择了不使用async Rust——这是一个非常重要的架构决策,我们后面会详细分析。
三、架构决策:为什么Bun坚持"不用async Rust"?
在Bun的Rust重写中,最引人注目的架构决策之一是:Bun仍然没有使用async/await Rust,而是采用了多线程同步I/O模型。
3.1 为什么不用async Rust?
Rust的async/await语法是现代Rust生态中处理并发的主流方案。Tokio、async-std、smol等异步运行时为Rust提供了强大的异步I/O能力。然而,Jarred明确表示Bun不会采用async Rust,原因如下:
第一,async Rust的复杂性。 async Rust的语法虽然看起来简洁,但背后涉及复杂的Future调度、executor实现、waker机制等概念。对于需要精确控制性能的底层系统软件来说,async抽象带来的性能开销和不确定性是难以接受的。
第二,与JavaScript运行时的模型匹配。 Bun的核心是一个JavaScript引擎(基于JavaScriptCore),它有自己的事件循环和任务调度机制。如果在Rust层再引入一层async runtime,会导致两套调度系统共存,增加复杂性和性能开销。
第三,Zig版本的Bun也没有使用async模型。 Zig本身没有async语法,Bun在Zig时代就是用多线程同步I/O实现的。保持这个架构选择,使得迁移后的Rust版本与原有设计保持一致。
3.2 Bun的并发模型:work-stealing线程池
Bun Rust版本的核心并发架构是一个work-stealing线程池:
use std::thread;
use crossbeam_deque::{Stealer, Worker};
use dashmap::DashMap;
// 全局线程池
static THREAD_POOL: once_cell::sync::Lazy<RayonPool> =
once_cell::sync::Lazy::new(|| RayonPool::new(num_cpus::get()));
// 任务队列:每个线程一个worker,通过work-stealing实现负载均衡
struct WorkerQueue {
worker: Worker<Job>,
stealer: Stealer<Job>,
}
// 调度策略:当某个线程的本地队列空了时,从其他线程的队列"偷"任务
fn steal_work(stealer: &Stealer<Job>) -> Option<Job> {
loop {
match stealer.steal() {
Steal::Success(job) => return Some(job),
Steal::Retry => continue,
Steal::Empty => return None,
}
}
}
这种模型的优势在于:
- 零async开销:不需要Future调度器,没有状态机展开的开销
- 与JavaScript事件循环天然集成:JavaScript的异步操作通过libuv->Bun Rust层->线程池的方式处理
- 易于调试:同步代码的堆栈追踪比async代码清晰得多
3.3 Bun仍然不用第三方HTTP服务器
Bun的另一个重要架构特点是:它没有使用任何第三方HTTP服务器库(如hyper、actix-web等),而是自己实现了HTTP处理逻辑。这与Deno的策略不同——Deno在迁移到Rust后,选择了使用hyper作为HTTP层。
Bun坚持自研HTTP栈的原因是对性能的极致追求。HTTP处理是Bun最核心的路径之一,任何额外的抽象层都可能影响QPS和延迟。通过直接操作socket和处理HTTP协议,Bun能够实现更精细的性能优化。
四、性能对比:重写后的Bun真的更快了吗?
4.1 各项基准测试结果
根据Jarred Sumner公布的基准测试数据,Bun Rust版本在大多数场景下达到或超越了原有Zig版本的性能:
| 测试场景 | Zig版本 | Rust版本 | 变化 |
|---|---|---|---|
| HTTP GET (hello world) | 100% | 103-108% | 略快 |
| HTTP POST (JSON) | 100% | 98-105% | 持平 |
| 文件I/O | 100% | 107-115% | 明显提升 |
| 模块加载 | 100% | 112-120% | 明显提升 |
| npm install (large) | 100% | 95-102% | 持平 |
| 二进制文件大小 | 100% | 75-88% | 缩小3-8MB |
关键数据解读:
HTTP性能小幅提升,特别是文件I/O和模块加载场景的提升显著。这主要得益于Rust的优化编译器和对内存布局的精细控制。Rust的std::fs和std::io在底层I/O操作上做了大量优化。
二进制体积缩小3-8MB是一个重大利好。对于一个需要分发的运行时工具来说,更小的体积意味着更快的下载速度和更低的存储成本。这3-8MB的缩减主要来自于Rust编译器更好的链接优化和去除未使用代码的能力。
4.2 内存泄漏修复
Rust版本修复了大量Zig版本中积累的内存泄漏问题。Rust的所有权系统和生命周期检查器在编译期就防止了大量潜在的内存错误类型,包括:
- 使用后释放(use-after-free)
- 双重释放(double-free)
- 数据竞争(data race)
- 悬垂指针(dangling pointer)
这些错误在Zig中可能导致内存泄漏或未定义行为,在Rust中则由编译器在编译期直接报错。
// Rust编译器强制执行的内存安全规则示例
fn process_data(data: String) -> String {
// data的所有权被转移到函数内部
// 函数返回后,data被自动drop
// 不可能出现"data在外部仍被引用但已被释放"的情况
let result = format!("Processed: {}", data);
result // result的所有权被返回给调用者
}
// 编译期错误示例:悬垂引用
fn dangle() -> &String { // 编译错误!
let s = String::from("hello");
&s // s在函数结束时被drop,返回的引用无效
}
4.3 cgo开销降低的连锁反应
Go 1.26版本中cgo开销降低了约30%,而Bun恰好重度依赖native addon(通过NAPI)。虽然Bun这次是Zig->Rust迁移而非Go相关,但Rust同样需要处理与C库的互操作。Rust的bindgen和cxx等工具在生成FFI绑定方面已经非常成熟,这让Bun的native addon迁移相对顺畅。
五、行业影响:AI辅助编程的新纪元
5.1 从"AI写小脚本"到"AI重构大型系统"
Bun的Rust重写最重要的意义,不是"Bun换语言",而是它展示了AI辅助编程的边界正在以前所未有的速度扩展。
此前,业界对AI编程工具的认知停留在"写小脚本"、"生成代码片段"、"辅助文档撰写"等相对简单的任务上。即使是GPT-4、Claude Opus等顶级模型,大多数开发者也认为它们在处理大型、复杂、跨模块的软件工程任务时力不从心。
Bun的案例打破了这一认知:100万行代码、11天完成、99.8%测试通过——这已经不是"辅助",而是"主导"。AI不是作为助手在人类工程师旁边打下手,而是作为迁移工作的核心执行者,人类工程师的角色转变为"质量验收者"和"架构决策者"。
5.2 迁移范式的根本转变
传统的大规模代码迁移(如Deno从Go到Rust)通常遵循以下流程:
- 人工制定迁移计划
- 分模块逐个迁移
- 每次迁移后充分测试
- 逐步替换生产环境
这个过程通常需要数月甚至数年,过程中需要大量的人工判断和决策。
Bun的迁移流程则完全不同:
第一步:AI生成初始Rust代码(基于现有Zig代码)
↓
第二步:编译器验证(rustc编译)
↓
第三步:测试套件验证(复用原有测试)
↓
第四步:性能基准测试(对比原有实现)
↓
第五步:人工review关键模块
↓
第六步:合并发布
AI在第一步完成了最繁重的工作(代码生成),这让整个流程从"月"压缩到"天"。
5.3 AI生成代码的质量边界
当然,我们也要客观看到这次迁移的一些限制:
首先,Bun的代码库虽然规模大,但结构相对清晰。 Bun的架构设计本身就是为了性能优化的,C-like的代码风格使得从Zig到Rust的翻译相对机械。这不是"AI理解复杂业务逻辑"类型的任务,而是"语法和模式转换"类型的任务。
其次,99.8%的测试通过率意味着仍有0.2%的测试失败。 这些失败的测试涉及边缘情况和平台特定行为,需要人工介入修复。
第三,后续的优化工作还未完成。 Jarred明确表示,Rust版本的canary发布只是第一步,更深度的性能优化、二进制大小缩减、内存使用优化等工作还在进行中。
5.4 这对开发者意味着什么?
对于普通开发者来说,Bun的案例揭示了几个重要趋势:
趋势一:语言选择的长期风险降低了。 过去选择一门编程语言意味着长期的承诺——招聘、社区、库生态、维护成本等。如果未来AI能够快速完成跨语言迁移,那么语言选择的锁定效应将大大减弱。Rust的性能+Go的简洁+Python的AI生态,不再是"三选一",而是可以根据项目需求灵活切换。
趋势二:遗留代码的激活成本将大幅下降。 很多团队有大量使用老旧语言编写的代码,因为迁移成本太高而无法现代化。AI辅助迁移能力的成熟,意味着这些"遗产"可以被快速转换到现代语言栈。
趋势三:学习曲线的优先级调整。 当AI可以生成高质量的Rust代码时,开发者是否还需要完整掌握Rust的所有权系统?答案是"仍然需要",因为review AI生成的代码、修复错误、理解性能瓶颈,都需要对目标语言有深刻理解。但学习的路径可能会改变——从"从零学Rust"到"学Rust来理解AI写的代码"。
六、技术细节:100万行代码迁移的工程挑战
6.1 Zig到Rust的语法映射规则
虽然Zig和Rust都属于系统编程语言,但它们的语法差异仍然显著。以下是迁移过程中最常见的一些模式转换:
变量声明和类型系统:
// Zig
var counter: u32 = 0;
const name: []const u8 = "bun";
const maybe_null: ?*const u8 = null;
// Rust
let mut counter: u32 = 0;
let name: &str = "bun";
let maybe_null: Option<&'static u8> = None;
结构体和方法:
// Zig
const Response = struct {
status: u16,
body: []u8,
pub fn new(status: u16, body: []u8) @This() {
return .{ .status = status, .body = body };
}
};
// Rust
struct Response {
status: u16,
body: Vec<u8>,
}
impl Response {
fn new(status: u16, body: Vec<u8>) -> Self {
Self { status, body }
}
}
错误处理:
// Zig: try/catch
const file = try std.fs.cwd().openFile(path, .{});
const content = try file.readToEndAlloc(allocator, 1024 * 1024);
// Rust: ? 操作符
let file = std::fs::File::open(path)?;
let content = std::fs::read_to_string(path)?;
6.2 测试套件的复用策略
Bun迁移过程中最聪明的决策之一是完全复用原有的测试套件。这不仅节省了编写新测试的时间,更重要的是保证了功能等价性——如果测试用例在Zig版本和Rust版本上表现完全一致,就能有力地证明迁移的功能正确性。
# Bun的测试运行方式
bun test # 运行JavaScript/TypeScript测试
bun test/unit/*.test.ts # 运行单元测试
# 迁移后,测试命令完全不变
bun test # Rust版本的Bun运行同样的测试
这种策略的关键是:测试用例本身不关心底层实现是Zig还是Rust——它们只关心JavaScript API的行为是否正确。这给了Rust重写一个强有力的功能验证机制。
6.3 CI/CD流水线的适配
Bun的CI/CD流水线在迁移后做了以下调整:
# .github/workflows/test.yml(简化示例)
name: Test
on: [push, pull_request]
jobs:
test-rust:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
# 构建Rust版本的Bun
- name: Build Bun (Rust)
run: |
cargo build --release --features=napi
strip target/release/bun # 减小二进制体积
# 复用原有测试套件
- name: Run Test Suite
run: |
./target/release/bun test
# 性能基准测试
- name: Benchmark
run: |
hyperfine --runs=100 './target/release/bun test/benchmarks/http.ts'
七、反思:Bun迁移对软件开发方法论的启示
7.1 "快速重写"vs"渐进演进"的重新思考
软件工程中有一条经典原则:"不要重写,从头重写是唯一能摧毁一切进度的事情"(Joel Spolsky,2000年)。这条原则在过去二十年里被无数工程师引用,用来反对"推倒重来"式的项目决策。
然而,Bun的案例迫使我们重新思考这条原则的条件假设。Joel提出这条原则的背景是:当时没有AI辅助编程工具,重写的成本完全由人力承担,且重写过程中原有系统的bug修复无法同步到新系统中。
Bun的情况完全不同:
- AI承担了99%的代码转换工作
- 测试套件可以在新系统上直接运行
- 迁移可以在11天内完成,而非18个月
这是否意味着"不要重写"的原则已经过时?我认为不完全是。Bun的成功有几个重要的前提条件:
- 目标语言(Rust)的工具链非常成熟
- 测试覆盖率足够高,能提供充分的信心
- 迁移路径相对机械(语法转换为主,非业务逻辑重构)
- 团队对源语言和目标语言都有深入理解
对于大多数业务系统来说,这些条件并不总是满足的。AI降低了重写的成本,但没有改变"何时重写"的决策框架"——改变的只是"重写能以多快的速度完成"。
7.2 AI辅助的边界在哪里?
100万行代码的迁移成功,是否意味着"任何规模的代码迁移都可以AI化"?答案显然是否定的。Bun的迁移有以下特殊性:
代码质量较高: Bun的代码库虽然庞大,但代码风格统一、注释完善、模块边界清晰。这使得AI能够准确理解代码意图。
测试覆盖率高: 高测试覆盖率是AI辅助重写的安全网。没有足够的测试,即使AI生成了能编译通过的代码,也难以保证功能正确性。
领域相对简单: Bun是一个基础设施工具(JavaScript运行时),其核心逻辑是处理I/O、解析JS、执行代码、调度线程等。这些逻辑的"正确性"有明确的判断标准(运行测试、benchmark),不像业务系统那样涉及复杂的领域规则和人为判断。
对于业务逻辑复杂、缺乏测试、没有明确"正确性"标准的项目,AI辅助迁移的效果可能大打折扣。
7.3 下一个"Bun"会是谁?
Bun的迁移是JavaScript运行时领域的一次标志性事件。Deno从Go到Rust,Bun从Zig到Rust——这两个最大的JavaScript运行时都不约而同地选择了Rust作为底层实现语言。这背后不仅仅是"哪个语言更好"的问题,而是Rust在系统编程领域的综合优势达到了临界点:
- 性能:与C/C++相当
- 内存安全:编译期保证
- 工具链:Cargo生态无可匹敌
- 社区:最受欢迎的语言(Stack Overflow调查连续多年第一)
- 跨平台:一套代码覆盖所有主流平台
下一个可能发生大规模语言迁移的领域是什么?我认为有几个候选:
- Python底层库的Rust重写(如NumPy、Pandas的核心计算路径)
- Node.js原生模块的迁移(利用napi-rs的经验)
- 数据库存储引擎(SQLite、RocksDB的Rust实现版本)
八、总结:Bun不是终点,而是起点
Bun从Zig到Rust的迁移,是2026年软件工程领域最具标志性的事件之一。它的意义不仅在于一个JavaScript运行时的底层语言更换,更在于它展示了AI辅助编程的工程可行性边界——100万行代码、11天、99.8%测试通过,这些数字在此之前是不可想象的。
但我们也要保持清醒:Bun的成功有其特殊性,不代表AI辅助迁移可以解决所有软件工程问题。代码迁移的技术难度只是问题的一半,另一半是架构决策、测试策略、团队能力——这些仍然是需要人类工程师负责的工作。
对于开发者而言,Bun的案例带来的最重要启示是:拥抱变化,但理解不变。AI会改变我们写代码的方式,但不会改变软件工程的核心原则——可读性、可测试性、架构清晰性,这些品质无论在什么时代都是好代码的标志。
最后,如果你对Bun的Rust版本感兴趣,可以访问其GitHub仓库体验:
# 安装Rust版本的Bun(canary channel)
bun upgrade --canary
# 验证版本信息
bun --version
# 应该显示类似:bun 2.x.x (rust)
当你在终端里敲下bun upgrade --canary时,你正在运行一个AI在11天内生成的100万行Rust代码——这个事实本身,就已经足够令人兴奋了。
参考资料:
- Jarred Sumner博客原文(2026年7月8日)
- Bun GitHub仓库:https://github.com/oven-sh/bun
- Rust官方文档:https://doc.rust-lang.org/
- Go 1.26发布说明:https://go.dev/blog/go1.26
- TIOBE 2026年7月编程语言排行榜
本文作者:程序员茄子(技术深度解析,2026年7月)