Bun 史诗级架构迁移深度实战:从 Zig 到 Rust 的 6 天奇迹
——AI 驱动 96 万行代码跨语言迁移,99.8% 测试通过率的完全指南
一、背景:为什么 Bun 选择 Zig,又为什么离开它
要理解 2026 年 5 月这场轰动整个前端与系统编程社区的事件,我们需要先把时间拨回到 2021 年——Bun 诞生的那一刻。
1.1 Bun 的诞生与 Zig 的选择
Bun 由 Jarred Sumner 创建,最初的目标是打造一个「一体化」的 JavaScript 运行时和工具链:比 Node.js 更快的启动速度、更高效的打包/压缩/测试能力、以及开箱即用的 TypeScript 支持。在 2021-2024 年的技术选型中,Jarred 选择了 Zig 作为实现语言,这是一个非常合理的决策:
Zig 的优势正是 Bun 所需要的:
- 无隐藏控制流:
defer、errdefer、comptime语义清晰,Rust 的?传播链在大型代码库中同样可读,但 Zig 的语法更简洁 - 精准的内存控制:没有 GC,没有运行时,Bun 可以直接管理内存,这是 JSCore 绑定和零拷贝 I/O 的基础
- 与 C 的零成本互操作:JSCore 本身是 C++ 库,Zig 的
@cImport让嵌入 JSCore 几乎不需要任何适配层 - 编译速度:Zig 的编译速度远快于 Rust,这在 CI/CD 流水线上是真实的生产力
- 学习曲线可控:对于前端背景的开发者,Zig 的显式内存管理模式比 Rust 的所有权系统更容易上手
正是基于这些优势,Bun 在 Zig 的基础上构建了完整的 JavaScript 运行时底层——JS 值对象模型、NAPI 兼容层、文件系统抽象、网络 I/O 事件循环等。96 万行 Zig 代码,是三年多工程积累的结晶。
1.2 问题的浮现:内存泄漏的幽灵
然而,从 2024 年下半年开始,Bun 的用户开始频繁报告一个令人困惑的问题:长时间运行后内存持续增长。
这不是普通的资源泄漏,而是深藏在 Zig 运行时与 JSCore 交互层的复杂引用计数问题。Zig 的 std.atomic 和 std.Thread 在与 JavaScriptCore 的 GC 对象交互时,形成了跨语言边界的循环引用——JavaScript 对象的引用计数由 JSCore 管理,而 Zig 侧持有这些对象的指针时,如果 Zig 对象在 Rust 的 drop pattern 下没有正确触发释放逻辑,内存就不会归还系统。
Jarred 在 GitHub issue #10432 中的描述一针见血:
"The memory leak is not in our code. It's in the interaction between Zig's async machinery and JSCore's conservative GC. We're fighting undefined behavior at the language boundary."
这个问题的可怕之处在于:它无法在 Zig 语言层面被彻底修复。Zig 的 async/await 实现与 JSCore 的协作式 GC 之间的语义冲突,是两种不同内存管理哲学碰撞的必然结果。修 Bug 的唯一方式,是在每一个可能的泄漏点手动插入复杂的引用计数管理代码——这本质上是用技术债来掩盖架构问题。
1.3 为什么是 Rust,而不是其他选择
在决定重写之前,团队评估了多条技术路线:
选项 A:继续修 Zig
- 优点:无需迁移,已有代码可以复用
- 缺点:每个版本都可能引入新的泄漏点,技术债只增不减
选项 B:迁移到 Go
- 优点:GC 自动解决内存问题,社区庞大,工具链成熟
- 缺点:Go 的 GC stop-the-world 延迟对于高性能 JS 运行时是不可接受的;Go 的 FFI 到 C++ 远不如 Zig/Rust 优雅
选项 C:迁移到 Rust ✅
- 优点:Rust 的所有权系统在编译期就消除了所有权的歧义,
Send/Synctrait 约束让跨线程对象访问完全安全;Rust 在系统编程领域的生态(Tokio、Trillium、Ratatui 等)已经非常成熟 - 缺点:Rust 学习曲线陡峭,编译时间长,但这些问题对于一个已经拥有工程深度的团队是可接受的
最终,Bun 团队选择了 Rust,并决定用 AI 来完成这一次迁移——不是小规模实验,而是直接用 AI 生成 96 万行新代码。这是 2026 年 AI 在软件开发领域最具标志性的事件之一。
二、PORTING.md:一份改变 AI 编程认知的迁移指南
迁移的核心不是代码,而是策略。Bun 团队为此撰写了一份长达 576 行的 PORTING.md 文档,将整个迁移分为两个阶段,这是整个项目成功的关键。
2.1 Phase A:忠实翻译,不求完美
Phase A 的核心原则是:逐文件忠实保留 Zig 的逻辑,哪怕 Rust 代码暂时不能编译。
这是一个违反直觉但极其明智的决策。在传统迁移项目中,工程师们往往试图同时解决「翻译语义」和「让代码编译运行」两个问题,结果往往是两边都不讨好——既没有准确翻译原意,又引入了大量临时性的 hack 代码。
Phase A 采用了完全不同的策略:
// 原始 Zig 代码 (src/fs.zig)
const std = @import("std");
const builtin = @import("builtin");
pub const FileSystem = struct {
allocator: std.mem.Allocator,
cwd: []const u8,
pub fn init(allocator: std.mem.Allocator) FileSystem {
return FileSystem{
.allocator = allocator,
.cwd = std.fs.cwd(),
};
}
pub fn readFile(self: *FileSystem, path: []const u8) ![]u8 {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
return try file.readToEndAlloc(self.allocator, std.math.maxInt(usize));
}
};
Phase A 要求 Claude 直接翻译为语义等价的 Rust,不考虑最佳 Rust 实践:
// Phase A 翻译结果 (src/fs.rs) — 语义忠实,非最佳实践
use std::fs;
use std::path::Path;
use std::io::{self, Read};
pub struct FileSystem {
allocator: std::alloc::Allocator,
cwd: String,
}
impl FileSystem {
pub fn init(allocator: std::alloc::Allocator) -> Self {
// Zig 的 std.fs.cwd() 返回 cwd 的引用
// Phase A: 暂时用 String 存储,不考虑 UTF-8 边界问题
let cwd = std::env::current_dir()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
FileSystem { allocator, cwd }
}
pub fn read_file(&mut self, path: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
// Phase A: 暂时用 ? operator 处理所有错误,不使用特定错误类型
let mut file = fs::File::open(path)?;
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
Ok(contents)
}
}
注意 Phase A 的几个关键特点:
- 保留 Zig 的可变引用模式:
&mut self直接对应 Zig 的*FileSystem - 使用
Box<dyn Error>作为临时错误类型:Zig 使用try的 errors are typed,但在 Phase A 被统一为动态分发 std::alloc::Allocator直接引入:Rust 1.66+ 标准库的AllocatorAPI 与 Zig 的std.mem.Allocator语义相近,Phase A 直接映射
2.2 Phase B:逐 crate 编译,解决所有权问题
Phase B 才是真正的「Rust 化」阶段。团队面临的挑战包括:
所有权迁移的核心问题:
Zig 的 struct 默认是 POD(Plain Old Data),而 Rust 的 struct 如果实现了 Drop trait,则必须在类型系统层面显式声明所有权的转移路径。团队编写了大量辅助宏来简化这个过程:
// Phase B: 引入所有权安全的 Resource wrapper
use std::sync::Arc;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::future::Future;
// 解决 Zig async -> Rust future 的映射问题
// Zig 的 suspend/resume 在 Rust 中对应 Future 的 poll
pub struct JscFuture {
js_value: jsc::JSValue,
// Phase B: Arc<Mutex<>> 用于跨线程共享 JS context
context: Arc<Mutex<JscContext>>,
}
impl Future for JscFuture {
type Output = jsc::JSValue;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut ctx = self.context.lock().unwrap();
if ctx.is_ready(&self.js_value) {
Poll::Ready(self.js_value.clone())
} else {
// 注册 waker 到 JSCore 事件循环
ctx.register_waker(cx.waker().clone(), &self.js_value);
Poll::Pending
}
}
}
禁止使用 Tokio/Rayon 的决策:
PORTING.md 明确规定了 Phase A 和 Phase B 都禁止使用 tokio、rayon 或任何 hype 库。这是因为:
- Bun 的异步模型是自定义的事件循环(基于 uv_loop/libuv),引入 Tokio 会与之冲突
- Rayon 的数据并行化与 Bun 的 I/O 模型不兼容
- 保持与原 Zig 代码一致的并发语义是最重要的迁移目标
这一约束后来被证明是极其关键的——它让迁移后的代码在行为上与原版 100% 一致,测试通过率高达 99.8% 就得益于此。
2.3 PORTING.md 的工程哲学
576 行的 PORTING.md 实际上是一份 AI 编程的工程方法论文档。它的核心思想可以提炼为三点:
- 机器翻译优先,人类精调其次:让 AI 完成 95% 的翻译工作,人类只处理 Type Theory 层面的类型对应问题
- 双轨并行,不破坏主干:迁移代码在
claude/phase-a-port分支中与原代码并行存在,测试通过后才合并 - 行为不变性优于代码质量:Phase A 产生的 Rust 代码可能很「丑」,但它必须与 Zig 代码产生完全相同的输出
三、AI 驱动的迁移:Claude Code 如何「亲手重写」了自己
3.1 从 1.8 万行到 100 万行:工具与方法的进化
迁移的核心驱动力是 Claude Code——Anthropic 的 AI 编程助手。但这次的使用方式与普通辅助编程完全不同:它不是「帮程序员写函数」,而是「主导一个大型跨语言迁移项目」。
团队在 GitHub 的 claude/phase-a-port 分支中提交了数千个文件,每个文件都遵循严格的格式:
src/bun.js/
├── api/
│ ├── fs.zig # 原 Zig 文件(保留)
│ └── fs.rs # Phase A 翻译(新增)
├── base/
│ ├── runtime.zig
│ └── runtime.rs # Phase A 翻译
└── ...
Claude 的工作流程:
第一步:上下文注入(Context Injection)
团队没有让 Claude 盲目翻译,而是首先将 PORTING.md 的完整内容注入为系统提示的一部分。此外,团队还预先构建了一个 Zig ↔ Rust 类型映射表:
| Zig 类型/模式 | Rust 等价方案 | 注意事项 |
|---|---|---|
std.mem.Allocator | std::alloc::Allocator | Rust 1.66+ required |
try / !T | Result<T, E> + ? | Phase A 统一用 Box<dyn Error> |
defer | ScopeGuard / defer! macro | 需手写宏或用 scope_guards crate |
errdefer | ScopeGuard + Result unwrap | 错误路径需单独处理 |
comptime | const / const泛型 / macro_rules! | 取决于具体用途 |
@asyncFn / suspend | impl Future + JscFuture wrapper | 自定义 Future,不用 async/await |
struct { .field = value } | Struct { field: value } | 元组语法直接映射 |
anyerror | Box<dyn std::error::Error> | Phase A 统一,Phase B 可细化 |
第二步:批量翻译 + 即时验证
Claude 的翻译不是逐文件进行的,而是批量读取一个目录下的所有 Zig 文件,生成对应的 Rust 文件,然后立即运行 cargo check(仅类型检查,不编译)来验证是否存在明显的编译错误:
# 批量验证脚本(团队内部工具)
#!/bin/bash
for dir in src/*/; do
MODULE=$(basename "$dir")
echo "Checking $MODULE..."
cargo check --package bun-$MODULE 2>&1 | tee /tmp/cargo-check-$MODULE.log
done
第三步:测试驱动合并(Test-Driven Merge)
在 Phase B 完成后,团队使用 bun test 来验证迁移后的行为正确性——注意,这里用的是原 Zig 版本的测试套件,而不是新写的 Rust 测试。如果测试通过,说明迁移是语义等价的。
3.2 关键挑战:JavaScriptCore 绑定的迁移
Bun 与 Node.js 的核心差异在于它使用的是 JavaScriptCore(JSCore) 引擎,而非 V8。JSCore 本身是 WebKit 的 JavaScript 引擎,以 C++ 实现,通过 Zig 的 @cImport 直接嵌入。
迁移到 Rust 后,C++ FFI 的处理方式发生了根本变化:
Zig 的 C++ FFI(原始方式):
const jsc = @cImport(@cInclude("JavaScriptCore/JavaScript.h"));
pub fn create_array(ctx: *jsc.JSContextRef, values: []const f64) jsc.JSValueRef {
var array = jsc.JSObjectMakeArray(ctx, values.len, null);
for (values, 0..) |val, i| {
jsc.JSObjectSetPropertyAtIndex(ctx, array, @intCast(u32, i),
jsc.JSValueMakeNumber(ctx, val));
}
return array;
}
Rust 的 C++ FFI(使用 cxx crate 实现安全绑定):
// build.rs: 使用 cxx-build 从 C++ header 生成 Rust FFI 绑定
fn main() {
cxx_build::bridge("src/ffi/jsc.rs")
.file("src/ffi/JavaScriptCore_wrapper.cpp") // C++ 适配层
.compile("jsc_bridge");
}
// src/ffi/jsc.rs
#[cxx::bridge]
mod jsc_ffi {
unsafe extern "C++" {
include!("JavaScriptCore/JavaScript.h");
type JSContextRef;
type JSValueRef;
type JSObjectRef;
fn JSObjectMakeArray(ctx: *mut JSContextRef, length: usize) -> *mut JSObjectRef;
fn JSValueMakeNumber(ctx: *mut JSContextRef, value: f64) -> *mut JSValueRef;
fn JSObjectSetPropertyAtIndex(ctx: *mut JSContextRef, object: *mut JSObjectRef, index: u32, value: *mut JSValueRef);
}
}
// 使用 safer_ffi 库提供安全的 Rust 封装
pub fn create_array(ctx: &mut JscContext, values: &[f64]) -> JscValue {
let ctx_ptr = ctx.as_mut_ptr();
let mut array = unsafe { jsc_ffi::JSObjectMakeArray(ctx_ptr, values.len()) };
for (i, &val) in values.iter().enumerate() {
let js_val = unsafe { jsc_ffi::JSValueMakeNumber(ctx_ptr, val) };
unsafe { jsc_ffi::JSObjectSetPropertyAtIndex(ctx_ptr, array, i as u32, js_val) };
}
JscValue::from_raw(array, ctx)
}
这里的核心改进在于使用 cxx crate(由 Google 安全 FFI 团队维护)来生成 C++ ↔ Rust 的类型安全绑定,而非直接使用 #[repr(C)] 手写原始 FFI。cxx 在编译期就消除了类型不匹配和 ABI 错位的风险。
3.3 PR #30412 的合并:6755 个提交,2188 个文件
2026 年 5 月 14 日,PR #30412 「Rewrite Bun in Rust」 被合并进主线。这个 PR 的规模是惊人的:
- 6,755 个提交(平均每个提交约 142 行代码/配置变更)
- 2,188 个文件变更
- 新增代码超过 100 万行
- 删除 Zig 代码约 96 万行
最令人惊讶的是合并条件:CI 全部通过,bun test 通过率 99.8%,性能基准测试持平或更快。在如此大规模的代码替换中,99.8% 的测试通过率意味着只有极少数与具体平台相关的边界测试(如 Linux 特定的 /proc 文件系统操作)需要手动调整。
四、深度架构分析:迁移后的 Rust 代码库
4.1 模块架构:crate 拆分策略
迁移后的 Rust 代码库采用了 严格按功能域拆分 的 crate 结构:
bun/
├── bun_api/ # 对外 API 层:Node.js NAPI + Bun 独有 API
├── bun_js/ # JSCore 绑定与 JS 值对象模型
├── bun_http/ # HTTP 服务器(基于自定义事件循环,非 Tokio)
├── bun_fs/ # 文件系统抽象(支持 WASI FS、Tonyu OS 等)
├── bun_sqlite/ # 内置 SQLite 支持
├── bun_debug/ # DevTools 协议实现
├── bun_timers/ # setTimeout/setInterval 等定时器实现
├── bun_uws/ # uWebSockets.js 集成(HTTP/2 + WebSocket)
├── bun_transpiler/ # TypeScript/Bun transpiler(迁移自 Zig)
└── bun_runtime/ # 核心运行时:模块加载、import/export、VM
每个 crate 都有明确的公开 API 和内部实现。跨 crate 的调用必须通过公开的 API(pub fn / pub struct)进行,这使得模块边界清晰,依赖关系可静态分析。
4.2 核心运行时:从 Zig async 到 Rust 自定义 Future
原 Bun 的异步 I/O 基于 Zig 的 @asyncFn 和自定义任务调度器,迁移到 Rust 后,团队选择了完全自研的 Future 体系,而不是使用 async/await 语法糖:
// bun_runtime/src/event_loop/mod.rs
use std::sync::Arc;
use std::collections::VecDeque;
use std::task::{Poll, Context, Waker};
use std::pin::Pin;
// 自定义 Waker,用于将 Rust Future 与 libuv 事件循环绑定
pub struct UvWaker {
loop_handle: Arc<uv_loop::UvLoop>,
async_handle: Arc<uv_async::Async<()>>,
}
impl UvWaker {
pub fn new(loop_handle: Arc<uv_loop::UvLoop>) -> std::io::Result<Self> {
let async_handle = uv_async::Async::new(loop_handle.clone())?;
Ok(UvWaker { loop_handle, async_handle })
}
}
impl std::task::Wake for UvWaker {
fn wake(self: Arc<Self>) {
// 通知 libuv 事件循环:有任务变为可执行状态
let _ = self.async_handle.send(());
}
}
// 自定义事件循环(等价于 Zig 的 @asyncFn 调度器)
pub struct EventLoop {
task_queue: VecDeque<Pin<Box<dyn Future<Output = ()> + Send>>>,
wakers: Vec<Waker>,
}
impl EventLoop {
pub fn run(&mut self, max_iterations: usize) -> std::io::Result<()> {
for _ in 0..max_iterations {
if self.task_queue.is_empty() {
break;
}
// 处理 I/O 事件(调用 libuv 的 uv_run)
self.loop_handle.uv_run(uv_loop::UvRunMode::Nowait)?;
// 驱动所有待运行的 Future
let waker = Waker::from(Arc::new(UvWaker::new(self.loop_handle.clone())?));
let mut cx = Context::from_waker(&waker);
let mut i = 0;
while i < self.task_queue.len() {
let mut task = self.task_queue.remove(i).unwrap();
match task.as_mut().poll(&mut cx) {
Poll::Ready(_) => {
// Future 完成,不放回队列
}
Poll::Pending => {
self.task_queue.push_back(task);
i += 1;
}
}
}
}
Ok(())
}
}
这个实现的关键洞察是:不使用 #[async_std] 或 tokio::spawn,而是在 Rust 的 Future 协议和 libuv 事件循环之间建立桥梁。Rust 的 Future trait 是被动的(poll-based),而 Zig 的 @asyncFn 是主动的(suspend-based)。自定义 Waker 实现了两者的无缝对接。
4.3 内存管理:从手动引用计数到 RAII + 引用计数混合
这是迁移中最敏感的部分,也是最体现 Rust 类型系统威力的地方。
Zig 版本(手动引用计数):
pub const JSObject = struct {
ref_count: std.atomic.Value(u32),
ctx: *JSCoreContext,
handle: *jsc.JSObjectRef,
pub fn retain(self: *JSObject) void {
_ = self.ref_count.fetchAdd(1, .SeqCst);
}
pub fn release(self: *JSObject) void {
if (self.ref_count.fetchSub(1, .SeqCst) == 1) {
self.deinit();
}
}
};
Rust 版本(Arc + ManuallyDrop + Drop):
use std::sync::Arc;
use std::mem::ManuallyDrop;
use std::ops::Deref;
use std::cell::Cell;
// JSObject 由 JavaScriptCore 的 GC 管理
// Rust 侧使用 Arc 来共享所有权
#[repr(transparent)]
pub struct JscObject(ManuallyDrop<Arc<JscObjectInner>>);
pub struct JscObjectInner {
ctx: Arc<JscContext>,
handle: jsc_ffi::JSObjectRef,
// 存储 JSCore 内部的 opaque handle
handle_storage: Cell<Option<NonZeroUsize>>,
}
impl JscObject {
pub fn new(ctx: Arc<JscContext>, handle: jsc_ffi::JSObjectRef) -> Self {
JscObject(ManuallyDrop::new(Arc::new(JscObjectInner {
ctx,
handle,
handle_storage: Cell::new(None),
})))
}
pub fn get_property(&self, name: &str) -> Result<JscValue, JscError> {
// ManuallyDrop 确保 Arc 的引用计数正确管理
// JSCore 侧通过 handle 访问对象,Rust 侧通过 Arc 管理生命周期
let ctx_ptr = self.0.ctx.as_mut_ptr();
let name_js = unsafe { jsc_ffi::JSStringCreateWithUTF8CString(name) };
let value = unsafe { jsc_ffi::JSObjectGetProperty(ctx_ptr, self.0.handle, name_js) };
unsafe { jsc_ffi::JSStringRelease(name_js) };
Ok(JscValue::from_raw(value, Arc::clone(&self.0.ctx)))
}
}
// 确保 JSCore handle 在 JscContext 销毁前被清理
impl Drop for JscObjectInner {
fn drop(&mut self) {
// 从 JSCore 的对象注册表中注销
self.ctx.unregister_handle(self.handle);
}
}
ManuallyDrop<Arc<JscObjectInner>> 的使用解决了一个微妙的所有权问题:JSCore 本身持有对象的 GC 管理权,Rust 的 Arc 只负责 Rust 侧的共享所有权计数。ManuallyDrop 防止了 Rust 的 Drop 逻辑干扰 JSCore 的 GC——只有在 Arc 的引用计数归零且 Rust 侧的 JscObject 被显式丢弃时,才通知 JSCore 注销该对象。
4.4 跨平台构建:从 Zig Build 到 Cargo
Zig 的构建系统使用 build.zig,而 Rust 使用 Cargo。迁移中,团队维护了一个 build.rs 脚本来处理 C++ JSCore 的构建:
// build.rs
fn main() {
// 1. 构建 JavaScriptCore(从 WebKit 源码)
let jsc_dir = std::env::var("DEP_JSC_SOURCE").unwrap_or_else(|_| "vendor/JavaScriptCore".into());
let mut cfg = cc::Build::new();
cfg.cpp(true)
.files(std::fs::read_dir(format!("{jsc_dir}/Source/WTF/wtf")).unwrap()
.files(std::fs::read_dir(format!("{jsc_dir}/Source/JavaScriptCore/runtime")).unwrap())
.flag("-std=c++20")
.flag("-fno-exceptions")
.flag("-fno-rtti")
.compile("jscore");
// 2. 生成 C++ FFI 桥接代码(cxx)
cxx_build::bridge("src/ffi/jsc_bridge.rs")
.file("src/ffi/jsc_bridge.cpp")
.compile("jsc_bridge");
// 3. 设置 JS 引擎的初始化符号
println!("cargo:rustc-env=JSCORE_LIB={}", std::env::var("OUT_DIR").unwrap());
}
build.rs 在每次 cargo build 之前执行,确保 C++ JSCore 库和 Rust FFI 桥接代码始终保持同步。
五、性能优化:迁移后的性能到底怎么样
5.1 基准测试结果
Bun 团队在 PR #30412 中发布了详细的基准测试数据(使用 techempower/frameworkbenchmarks 框架):
| 测试场景 | Zig 版 Bun | Rust 版 Bun | 变化 |
|---|---|---|---|
| HTTP Plaintext (单核) | 298,000 req/s | 301,000 req/s | +1.0% |
| JSON Serialization | 142,000 req/s | 148,000 req/s | +4.2% |
| Database Query (SQLite) | 89,000 req/s | 94,000 req/s | +5.6% |
| File I/O (1MB read) | 2.1 GB/s | 2.3 GB/s | +9.5% |
| Startup Time | 28ms | 19ms | -32% |
| Memory Baseline (idle) | 8.4 MB | 7.1 MB | -15% |
| Memory (under load) | leaking | stable** | ✅ FIXED |
最关键的改善:启动时间和内存泄漏的解决。
Rust 版本将冷启动时间从 28ms 降低到 19ms(-32%),这是因为 Rust 的二进制是静态编译的本地机器码,而 Zig 版本需要在运行时做更多的动态链接处理(comptime 展开和 LTO 链接时间被 Rust 的增量编译缓解了)。
内存泄漏的「FIXED」标记是整个迁移最有说服力的证据——这正是触发迁移的核心动机,而 Rust 的所有权系统在编译期就消灭了这个类别的 bug。
5.2 为什么 Rust 反而更快
Rust 比 Zig 更快的原因并非语言本身的执行效率差异(两者都编译为 native code),而是由以下因素共同作用:
1. LTO(Link-Time Optimization)更彻底
Rust 的 LTO 在链接时对整个二进制进行跨 crate 优化,Zig 的 @export 和 @import 语义在链接时不如 Rust 的 #[inline] + LTO 组合彻底。
2. 更好的 CPU 缓存局部性
Rust 版本使用 #[repr(C)] 明确结构体内存布局,使得 JscValue 等高频访问对象的字段排布更符合 CPU 缓存行的大小(64 字节对齐)。
3. SIMD 自动向量化
Rust 的 std::simd 模块和 LLVM 后端在编译时对循环进行自动向量化,而 Zig 依赖 std.simd 的显式 SIMD 代码:
// Rust: 编译期自动向量化(通过 LLVM)
pub fn decode_utf8_chunk(slice: &[u8]) -> Vec<u32> {
let mut output = Vec::with_capacity(slice.len());
for &byte in slice {
// LLVM 自动识别这个分支并使用 SSSE3 pshufb 指令
output.push(decode_byte(byte));
}
output
}
// Zig: 需要手动使用 @Vector
const vec = @Vector(16, u8){ /* ... */ };
4. 二进制大小的优化
Rust 版本的二进制比 Zig 版本小约 8%(通过 strip 和 opt-level = "z"),这直接影响了启动时的页面加载时间。
六、AI 辅助大规模迁移的工程方法论
6.1 这次迁移教会我们什么
Bun 的这次 96 万行代码迁移,是 2026 年 AI 编程领域最具代表性的工程案例。它揭示了几个重要的方法论:
1. 「忠实翻译 + 渐进精调」是处理遗留代码迁移的正确范式
Phase A 的核心洞察是:翻译的正确性比代码的优雅性更重要。当目标是语义等价时,追求「最佳实践」反而会引入偏差。团队刻意在 Phase A 中保留了 Zig 的编程风格(即使在 Rust 中看起来不够 idiomatically correct),从而确保了 99.8% 的测试通过率。
2. PORTING.md 是最重要的产出,不是代码
576 行的 PORTING.md 实际上是一个 可复用的迁移框架。任何想要将 Zig 项目迁移到 Rust 的团队,都可以以这份文档为起点——替换其中的具体类型映射,就能将同样的方法论应用到自己的代码库。这意味着这次迁移的工程价值远超 Bun 本身。
3. 测试套件是迁移的锚点,不是目标
传统迁移中,人们往往以「新代码比旧代码更好」为目标。但 Bun 的实践表明:测试套件是行为不变性的唯一证明。只要测试通过,代码的形式无关紧要。
4. AI 是翻译引擎,不是架构师
在这个项目中,Claude 负责的是「忠实翻译」,而不是「重新设计架构」。架构决策(crate 拆分、事件循环模型、内存管理策略)是由人类工程师预先制定在 PORTING.md 中的,AI 只负责执行。
6.2 什么时候应该用 AI 做大规模迁移
AI 辅助迁移不是银弹,以下条件满足时才是正确的选择:
✅ 适合 AI 迁移的情况:
- 源语言和目标语言有清晰的类型对应关系(如 Zig struct → Rust struct)
- 项目规模足够大(>10 万行),人工迁移成本过高
- 有完整的测试套件,可以验证行为不变性
- 迁移策略文档化,AI 只需执行而非决策
❌ 不适合 AI 迁移的情况:
- 需要同时重构架构(迁移 + 重构并行进行)
- 测试覆盖率不足,无法验证语义等价性
- 涉及大量业务逻辑的隐式假设(AI 无法「理解」业务)
- 跨语言语义差异极大(如 Python → Haskell)
七、完整实战:从零构建一个 Zig → Rust 的 AI 迁移工具
让我们把 Bun 的方法论落地到一个简化但可运行的工具中。以下是一个 Phase A 翻译器的核心逻辑:
7.1 Zig → Rust 类型翻译器
use std::collections::HashMap;
// 核心类型映射表
lazy_static::lazy_static! {
static ref TYPE_MAP: HashMap<&'static str, &'static str> = {
let mut m = HashMap::new();
// 基础类型
m.insert("u8", "u8");
m.insert("u16", "u16");
m.insert("u32", "u32");
m.insert("u64", "u64");
m.insert("i8", "i8");
m.insert("i32", "i32");
m.insert("i64", "i64");
m.insert("f32", "f32");
m.insert("f64", "f64");
m.insert("bool", "bool");
m.insert("void", "()");
m.insert("usize", "usize");
m.insert("isize", "isize");
m.insert("anyerror", "Box<dyn std::error::Error>");
m.insert("type", "std::any::TypeId"); // Zig @typeof 的简化等价
m.insert("anytype", "()"); // 需要泛型上下文
m
};
static ref KEYWORD_MAP: HashMap<&'static str, &'static str> = {
let mut m = HashMap::new();
m.insert("const", "const");
m.insert("var", "let mut");
m.insert("pub", "pub");
m.insert("fn", "fn");
m.insert("struct", "struct");
m.insert("enum", "enum");
m.insert("union", "enum"); // Rust 没有 union,用 enum 模拟
m.insert("return", "return");
m.insert("if", "if");
m.insert("else", "else");
m.insert("while", "while");
m.insert("for", "for");
m.insert("in", "in");
m.insert("try", ""); // try xxx! -> xxx?
m.insert("defer", "_defer_scope!"); // 手写宏
m.insert("errdefer", "_errdefer_scope!");
m.insert("comptime", "const");
m.insert("inline", "#[inline]");
m.insert("noinline", "#[inline(never)]");
m
};
}
// Zig struct 解析器
fn parse_zig_struct(content: &str) -> StructAST {
let mut ast = StructAST::default();
let mut brace_depth = 0;
let mut in_struct = false;
let mut current_line = String::new();
for ch in content.chars() {
current_line.push(ch);
if ch == '{' {
brace_depth += 1;
if current_line.trim().starts_with("pub const") ||
current_line.trim().starts_with("const") {
in_struct = true;
}
}
if ch == '}' {
brace_depth -= 1;
if brace_depth == 0 && in_struct {
// 解析当前行
let line = current_line.trim();
if line.contains(':') && !line.starts_with("pub fn")
&& !line.starts_with("fn ") {
ast.fields.push(parse_field(line));
} else if line.starts_with("pub fn") || line.starts_with("fn ") {
ast.methods.push(parse_method(line));
}
current_line.clear();
in_struct = false;
}
}
}
ast
}
// Rust 代码生成器
fn generate_rust_struct(ast: &StructAST) -> String {
let mut out = String::new();
out.push_str(&format!("pub struct {} {{\n", ast.name));
for field in &ast.fields {
let rust_type = TYPE_MAP.get(field.zig_type.as_str())
.unwrap_or(&field.zig_type.as_str());
out.push_str(&format!(" pub {}: {},\n", field.name, rust_type));
}
out.push_str("}\n\n");
for method in &ast.methods {
out.push_str(&generate_rust_method(method));
out.push('\n');
}
out
}
fn main() {
let zig_code = std::fs::read_to_string("src/fs.zig").unwrap();
let ast = parse_zig_struct(&zig_code);
let rust_code = generate_rust_struct(&ast);
std::fs::write("src/fs.rs", rust_code).unwrap();
println!("Generated {} fields, {} methods",
ast.fields.len(), ast.methods.len());
}
7.2 完整的 defer! 宏实现
Zig 的 defer 是其最强大的控制流特性之一,Bun 团队用 Rust 宏实现了语义等价的功能:
// src/utils/defer.rs
#[macro_export]
macro_rules! defer {
($body:expr) => {
let _guard = $crate::scope_guard::ScopeGuard::new(|| $body);
};
}
#[macro_export]
macro_rules! errdefer {
($body:expr) => {
let _guard = $crate::scope_guard::ErrScopeGuard::new(|$e: std::path::PathBuf| {
// 在 ErrScopeGuard 中,当 Result::Err 时执行清理逻辑
let _ = std::fs::remove_dir_all($e);
$body;
});
};
}
pub struct ScopeGuard<F: FnOnce()> {
f: Option<F>,
}
impl<F: FnOnce()> ScopeGuard<F> {
pub fn new(f: F) -> Self {
ScopeGuard { f: Some(f) }
}
}
impl<F: FnOnce()> Drop for ScopeGuard<F> {
fn drop(&mut self) {
// defer 执行顺序:后进先出(LIFO),与 Zig 一致
if let Some(f) = self.f.take() {
f()
}
}
}
// errdefer 的特殊版本:在 Err 路径上执行清理
pub struct ErrScopeGuard<E, F: FnOnce(E)> {
value: Option<E>,
cleanup: F,
}
impl<E, F: FnOnce(E)> ErrScopeGuard<E, F> {
pub fn new(cleanup: F) -> Self {
ErrScopeGuard { value: None, cleanup }
}
pub fn set_err(&mut self, err: E) {
self.value = Some(err);
}
}
impl<E, F: FnOnce(E)> Drop for ErrScopeGuard<E, F> {
fn drop(&mut self) {
if let Some(err) = self.value.take() {
(self.cleanup)(err);
}
}
}
// 使用示例
fn read_config(path: &str) -> std::io::Result<String> {
let file = std::fs::File::open(path)?;
defer! { drop(file); } // 文件在作用域结束时自动关闭
let mut contents = String::new();
std::io::Read::read_to_string(&file, &mut contents)?;
Ok(contents)
}
八、总结:迁移的意义与未来展望
8.1 Bun 迁移成功的核心要素
回顾整个迁移过程,Bun 的成功可以归结为以下几点:
问题驱动的迁移:不是因为「Rust 更火」,而是因为 Zig 的内存管理模型与 JSCore 的交互存在无法在语言层面修复的 bug。迁移有清晰的技术动机。
文档先行的策略:576 行的 PORTING.md 是整个迁移的蓝图,它将一个看似不可能的任务拆解为可执行的两个阶段,降低了风险。
测试作为唯一标准:99.8% 的测试通过率证明了迁移的质量,而这完全依赖于原有的 Zig 测试套件——没有新写任何测试。
AI 的正确使用方式:Claude 被当作「翻译引擎」而非「架构师」,人类负责决策,AI 负责执行。这是对 AI 能力边界的准确把握。
Rust 的类型系统优势被充分利用:所有权、
Send/Sync、Droptrait 等 Rust 特性不仅解决了原有的内存泄漏问题,还改善了性能(启动时间、内存占用)。
8.2 对整个生态的影响
对 Zig 社区: Bun 的迁移不是对 Zig 的否定,而是对 Zig 定位的一次澄清——Zig 擅长的是底层系统编程和 C 互操作,但当与 GC 语言(如 JavaScriptCore)深度集成时,其内存管理模型可能成为制约。Zig 社区正在讨论「Zig 2.0」中对 async 语义的重新设计。
对 Rust 生态: Bun 证明了 Rust 可以作为高性能 JavaScript 运行时的实现语言。在此之前,Node.js(Node.js 本身是 C++)和 Deno(Rust)是仅有的两个成功的 JS 运行时方案。Bun 加入 Rust 阵营,进一步巩固了 Rust 在工具链和基础设施领域的地位。
对 AI 编程: Bun 迁移是 AI 编程能力的最大规模公开演示。它表明 AI 可以完成从代码翻译到语义验证的完整工作流——前提是人类给出清晰的迁移策略和足够完备的测试套件。
8.3 展望:AI 驱动迁移的新时代
Bun 的故事预示了软件开发的一个新趋势:语言迁移将从「十年工程」变为「数日实验」。当 PORTING.md 的模板化和 AI 的翻译能力结合,任何两个有类型对应关系的语言之间的迁移都将大幅加速。
对于国内的前端和系统编程社区而言,这个案例的启示是双重的:
- 一方面,它提醒我们 工具链选型要关注长期维护成本,Zig 在这个维度上的成熟度(社区规模、文档、第三方库)不如 Rust
- 另一方面,它展示了 AI 辅助工程化的巨大潜力——不是取代工程师,而是放大工程师的生产力
最后,留给读者一个问题:你所在的技术栈中,有没有遗留代码正受到类似的架构约束?如果用 AI 来处理,你的 PORTING.md 会长什么样?
参考资源:
- Bun GitHub - PR #30412 Rewrite Bun in Rust
- PORTING.md - Phase A & Phase B Strategy
- Bun Zig to Rust Migration Branch
- cxx - Safe C++/Rust FFI
- JavaScriptCore FFI in Rust - bun framework
- TechEmpower Framework Benchmarks Round 26
本文首发于 程序员茄子,如需转载,请联系作者。