Bun 从 Zig 到 Rust 的百万行 AI 重写:一场被 AI 改写的语言选择、内存安全与工程哲学之战
11 天、96 万行代码、16.5 万美元、99.8% 测试通过率——2026 年 7 月,Bun 团队用 Claude 把整个 JavaScript 运行时从 Zig 完全重写为 Rust。这不是一次普通的重构,它撕开了三层被行业刻意回避的伤口:AI 到底能不能扛得住百万行系统级代码的迁移?内存安全是靠语言还是靠工程投入?以及,当一门语言被"AI 明星项目"绑定时,语言社区该高兴还是该逃离?本文以工程师视角,从运行时架构、语言机制、迁移方法论到性能实测,把这件事彻底拆开讲透。
一、背景:这不是"换个语言写 Hello World"
先把事件的骨架摆清楚,因为网上传播的版本已经严重失真。
2026 年 5 月初,Bun 的 GitHub 仓库里出现了一个叫 claude/phase-a-port 的分支。分支里数十万行由 AI 生成的 Rust 代码,和原始 Zig 实现并排存在,同时附带一份长达 576 行的 PORTING.md——一份 Zig-to-Rust 的迁移作战手册。7 月 8 日,Bun 作者 Jarred Sumner 发博客正式宣布:Bun 已从 Zig 完全重写为 Rust,主要工作由 Claude 完成,核心迁移耗时约 11 天(另一说法是 Linux x64 glibc 环境下的核心迁移约 6 天),涉及约 96 万行代码,通过了原有测试套件 99.8% 的验证,花费约 16.5 万美元的 API 与算力成本。
要理解这件事的分量,你得先知道 Bun 是什么。
Bun 不是一个库,不是一个框架,它是一个完整的 JavaScript/TypeScript 运行时,直接对标 Node.js 和 Deno。它内部至少包含这些子系统:
- 一个 JavaScript 引擎的深度集成层(Bun 用的是 JavaScriptCore,即 Safari 的引擎,而不是 V8)
- 一个事件循环 + 异步 I/O 层(自己实现的,不用 libuv)
- 一个包管理器(
bun install,号称比 npm 快数倍) - 一个打包器(bundler,对标 esbuild/webpack)
- 一个测试运行器(
bun test) - 一个转译器(TypeScript/JSX 直接跑,不需要预编译)
- 大量 Node.js API 的兼容层(
fs、http、net、crypto……)
这套东西加起来是接近百万行的系统级代码,涉及大量手写内存管理、指针操作、C++ 互操作(因为 JavaScriptCore 是 C++ 写的)、SIMD 优化、以及和操作系统内核打交道的 syscall 封装。把这样一个东西整体换语言,正常情况下是"重写公司"级别的工程量,通常需要一个团队干一到两年。
而他们说,用 AI,11 天干完了。
这就是为什么这件事值得程序员认真对待——它同时是技术事件(AI 编码能力的边界测试)、语言事件(Zig vs Rust 的正面对撞)和行业事件(AI-native 工程范式的一次极限压测)。
二、核心概念:为什么是 Zig,又为什么要逃离 Zig
2.1 Zig 给了 Bun 什么
Bun 最初选择 Zig 不是拍脑袋。Zig 是一门定位在"更好的 C"的系统级语言,它给了 Bun 几个关键能力:
1)comptime——编译期执行的杀手锏
Zig 最独特的特性是 comptime,它允许你在编译期运行任意 Zig 代码,用来做代码生成、泛型、类型计算。它没有独立的宏系统、没有模板元编程的那套语法,一切都是同一种语言在不同阶段执行。
// Zig:comptime 泛型,编译期就把类型确定下来
fn Vector(comptime T: type, comptime len: usize) type {
return struct {
data: [len]T,
const Self = @This();
pub fn dot(self: Self, other: Self) T {
var sum: T = 0;
// 这个循环会在编译期被完全展开(如果 len 是编译期已知)
inline for (0..len) |i| {
sum += self.data[i] * other.data[i];
}
return sum;
}
};
}
// 使用时,Vector(f32, 4) 就是一个具体类型,零运行时开销
const Vec4 = Vector(f32, 4);
对 Bun 这种到处需要"根据不同数据布局生成高度特化代码"的项目,comptime 非常香。你可以用它在编译期生成 Node.js API 的绑定代码、生成不同平台的 syscall 封装、生成解析器的状态机。
2)与 C/C++ 的无缝互操作
Zig 可以直接 @cImport C 头文件,不需要写一堆 FFI 胶水。Bun 要深度嵌入 JavaScriptCore(一个庞大的 C++ 代码库),这个能力省了海量工作。
// Zig 直接导入 C 头文件,拿到里面所有函数和结构体
const c = @cImport({
@cInclude("JavaScriptCore/JavaScript.h");
});
pub fn evalScript(ctx: c.JSContextRef, code: [*:0]const u8) c.JSValueRef {
const script = c.JSStringCreateWithUTF8CString(code);
defer c.JSStringRelease(script);
return c.JSEvaluateScript(ctx, script, null, null, 0, null);
}
3)手动内存管理 + 显式分配器
Zig 没有 GC,也没有像 Rust 那样强制的所有权系统。它把内存管理完全交给你,但通过"分配器(allocator)作为显式参数"的约定,让内存管理变得可追踪。
// Zig:分配器作为显式参数传入,谁分配谁负责
pub fn buildString(allocator: std.mem.Allocator, parts: []const []const u8) ![]u8 {
var total: usize = 0;
for (parts) |p| total += p.len;
const buf = try allocator.alloc(u8, total);
var offset: usize = 0;
for (parts) |p| {
@memcpy(buf[offset .. offset + p.len], p);
offset += p.len;
}
return buf; // 调用方负责 allocator.free(buf)
}
这套模式的好处是极致的性能和控制力,坏处是——它不阻止你犯错。用完没 free 就是泄漏,free 了还用就是 use-after-free,越界访问在 release 模式下就是未定义行为。
2.2 逃离 Zig 的真实动因
官方博客给出的理由是"内存安全"和"生态成熟度"。但根据 Zig 项目创始人 Andrew Kelley 的公开回应,真实故事要复杂得多,而且更值得程序员深思。
动因一:Anthropic 收购 + Claude 是用 Bun 写的
Bun 被 Anthropic 收购后,出现了一个尴尬的链路:Claude(AI 产品)用 Bun 运行,Bun 用 Zig 写。这条间接绑定关系带来两个问题:一是大量 AI 生成的、质量参差的代码涌入;二是让 Zig 社区担心自己的语言被外界当成"和 AI 深度绑定的语言"。用 Rust 重写,某种程度上是给这条链路"解耦"。
动因二:Zig 生态与团队理念的裂痕
Kelley 直言,Bun 的代码质量在他们的用户代码审查中"把人吓到了"——各种 hack 堆叠、滥用断言、功能开发飞快但几乎不花时间反思和修 bug。Zig 团队反复提醒的工程实践(比如别滥用 comptime、注意编译时间、开 LTO)没被采纳。双方对项目长期规划的分歧越来越大。
动因三:Zig 尚未 1.0,长期押注有风险
这是最"技术正确"的理由。Zig 到 2026 年仍未发布 1.0,语言本身还在演进,标准库和工具链的稳定性对一个商业化运行时来说是长期风险。Rust 已经是成熟、稳定、生态庞大的选择。
这里有个关键的认知点,我必须点破:博客把"避免 bug"归因于语言选择(Zig 不安全 → Rust 安全),这在工程上是一次偷换概念。 避免 bug 最核心的方法是投入足够的工程资源——写测试、做 fuzzing、认真 code review、及时修 bug。同样用 Zig,TigerBeetle(一个金融数据库)就写得极其扎实。语言给你护栏,但护栏挡不住"不刹车的驾驶习惯"。
三、语言机制对撞:Zig 的自由 vs Rust 的契约
这是全文技术含量最高的一节。要理解这次迁移的难度和争议,必须把两门语言的内存模型讲清楚。
3.1 内存安全:编译期证明 vs 运行期自律
Rust 的核心是所有权(ownership)+ 借用检查器(borrow checker)。它在编译期就证明你的内存访问是安全的:每块内存有唯一所有者,要么有多个不可变引用,要么有一个可变引用,二者不可兼得。
// Rust:借用检查器在编译期就拦住 use-after-free
fn main() {
let s = String::from("bun");
let r = &s; // 不可变借用
// let m = &mut s; // 编译错误!已有不可变借用时不能可变借用
println!("{}", r);
// s 离开作用域自动释放,编译器保证 r 不会比 s 活得久
}
而 Zig 靠的是约定 + 运行期检查(debug 模式):
// Zig:debug 模式下有安全检查,release-fast 模式下为了性能会关掉
pub fn getItem(list: []const i32, index: usize) i32 {
// debug 模式:越界会 panic 并打印堆栈
// ReleaseFast 模式:越界是未定义行为(可能读到垃圾数据)
return list[index];
}
差别在哪?Rust 把"证明安全"的成本转移到了编译期和开发者的心智负担上——你得和借用检查器搏斗。Zig 把成本转移到了运行期(debug 检查)和纪律上——写得爽,但错了就是你的锅。
3.2 迁移中最痛的地方:unsafe 的爆炸
这是整个事件里最被忽视、但最该被程序员盯住的技术细节。
有开发者对比发现:作为参照的 uv 项目(Python 包管理器,Rust 写的)只有 73 处 unsafe 调用,而 AI 迁移后的 Bun Rust 版本,unsafe 调用超过了 13000 处。
这个数字说明了什么?
它说明这次迁移在很大程度上是**"语法翻译"而非"语义重写"**。Zig 的手动内存管理和裸指针操作,被 AI 大量地、机械地翻译成了 Rust 的 unsafe 块。
// AI 迁移常见产物:为了让 Zig 风格的指针操作能编译通过,
// 大量使用 unsafe 绕过借用检查器
pub unsafe fn copy_raw(dst: *mut u8, src: *const u8, len: usize) {
// 这里其实就是把 Zig 的 @memcpy 直译过来
std::ptr::copy_nonoverlapping(src, dst, len);
}
// 更典型的:把裸指针在结构体里传来传去,绕过所有权
pub struct RawBuffer {
ptr: *mut u8,
len: usize,
cap: usize,
}
unsafe impl Send for RawBuffer {} // 手动断言线程安全——这是危险信号
关键点来了:Rust 的内存安全保证只在 safe 代码里成立。一旦进入 unsafe,借用检查器就闭嘴了,安全责任重新回到程序员(或 AI)身上。13000 处 unsafe 意味着,Bun 的"内存安全"承诺,有相当大一块其实是没有编译器兜底的。
换句话说:你从"一门不检查你的语言(Zig)"迁移到"一门检查你的语言(Rust)",但如果 63% 的危险操作都塞进 unsafe,那你只是把 // 注意这里危险 的注释换成了 unsafe {} 关键字而已。真正的内存安全没有凭空产生,它需要有人把这些 unsafe 块一个个重写成 safe 的、符合所有权模型的代码——而这,恰恰是 AI 最难自动完成的部分。
3.3 comptime 到哪去了
Zig 的 comptime 是没有 Rust 直接对应物的。Rust 里最接近的是:
- 泛型 + trait:处理类型多态
- 宏(macro_rules! 和过程宏):处理代码生成
- const 泛型和 const fn:处理编译期计算
// Rust:用 const 泛型模拟 Zig 的 Vector(T, len)
struct Vector<const N: usize> {
data: [f32; N],
}
impl<const N: usize> Vector<N> {
fn dot(&self, other: &Self) -> f32 {
let mut sum = 0.0;
for i in 0..N {
sum += self.data[i] * other.data[i];
}
sum
}
}
但 Zig 里那种"编译期跑一段任意逻辑来生成一整个类型/一堆绑定代码"的模式,在 Rust 里往往要拆成过程宏(写起来复杂得多)或者干脆改成运行期方案。AI 在做这类转换时,倾向于选"能编译通过"的路径,而不是"最地道、最高效"的路径——这也是为什么 Kelley 质疑:博客里列举的一堆"减少二进制大小"的工程工作,其实和重写本身没关系,是把本该在 Zig 里做的事挪到了 Rust 里补做。
四、架构分析:AI 是怎么啃下百万行的
抛开争议,从纯工程角度看,用 AI 迁移百万行代码这件事本身的方法论是有价值的。我们根据公开信息(PORTING.md、phase 分支命名)复盘它的作战结构。
4.1 分阶段(Phase)策略
从分支名 claude/phase-a-port 能看出,迁移是分 phase 进行的,不是一把梭。一个合理的百万行迁移会这样切:
- Phase A:基础设施层——内存分配、字符串、集合、错误处理这些底层原语先搬。因为上层全依赖它们。
- Phase B:I/O 与事件循环——异步运行时、syscall 封装。
- Phase C:子系统——包管理器、打包器、转译器逐个搬。
- Phase D:API 兼容层——Node.js API 一个个对齐。
- Phase E:收尾——性能调优、二进制瘦身、修最后 0.2% 的测试。
4.2 "并排存在 + 测试套件当裁判"
关键的工程智慧在于:新旧代码并排存在,用原有测试套件当回归裁判。
这套流程用伪代码表达就是:
# 迁移循环的本质
for module in $(topological_sort modules); do
# 1. 让 AI 读 Zig 实现 + PORTING.md 规范
claude port --source "src/${module}.zig" \
--guide PORTING.md \
--target "src-rs/${module}.rs"
# 2. 编译
cargo build || claude fix-compile-errors
# 3. 跑该模块相关的测试
bun test "test/${module}" || claude fix-test-failures
# 4. 通过则合并,进入下一个模块
done
PORTING.md 那 576 行的作用,就是给 AI 一套稳定的"翻译词典"和"约定俗成":Zig 的 allocator 参数怎么映射到 Rust、错误处理 !T 怎么映射到 Result<T, E>、defer 怎么映射到 Drop 或作用域、comptime 怎么映射到泛型/宏。有了这份规范,AI 每次翻译才能保持一致性,不会这个文件用一种风格、那个文件用另一种风格。
4.3 为什么测试套件是双刃剑
Bun 官方说:靠现有测试套件来给这一百万行未审查的 Rust 代码把关。
Kelley 的反驳一针见血,我把它翻译成工程语言:测试套件只能证明"你测到的路径行为正确",不能证明"没测到的路径没有内存问题"。
内存安全 bug 的可怕之处恰恰在于它们经常出现在:
- 罕见的错误处理分支
- 并发竞态(测试很难稳定复现)
- 极端输入边界
- 资源耗尽时的清理路径
这些地方,测试覆盖率天然低。而 13000 处 unsafe 里的任何一个,只要有一个指针生命周期算错,就是一颗定时炸弹。99.8% 测试通过率听起来很高,但对一个运行时来说,剩下 0.2% 和"没测到的部分"里藏着的可能就是生产环境的段错误。
这就引出一个更深的问题:Zig 版本"号称有很多烦人的 bug",那些 bug 是语言的错,还是"功能开发飞快但几乎不修 bug"的工程习惯的错?如果是后者,换成 Rust,一百万行 AI 生成的代码难道就自动没 bug 了?逻辑上说不通。
五、代码实战:手把手看四类典型迁移
我们不能只谈概念。下面用四个 Bun 里真实会遇到的模式,具体展示 Zig → Rust 的迁移长什么样,以及坑在哪。
5.1 错误处理:从 !T 到 Result<T, E>
Zig 的错误是值,用 !T(错误联合类型)表达,try 传播:
// Zig
const ReadError = error{ FileNotFound, PermissionDenied, OutOfMemory };
fn readConfig(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
error.FileNotFound => return error.FileNotFound,
else => return err,
};
defer file.close();
return try file.readToEndAlloc(allocator, 1024 * 1024);
}
迁移到 Rust,try 变 ?,!T 变 Result<T, E>,defer file.close() 靠 File 的 Drop 自动完成:
// Rust
use std::fs;
use std::io;
#[derive(Debug)]
enum ReadError {
FileNotFound,
PermissionDenied,
Io(io::Error),
}
impl From<io::Error> for ReadError {
fn from(e: io::Error) -> Self {
match e.kind() {
io::ErrorKind::NotFound => ReadError::FileNotFound,
io::ErrorKind::PermissionDenied => ReadError::PermissionDenied,
other => ReadError::Io(io::Error::from(other)),
}
}
}
fn read_config(path: &str) -> Result<Vec<u8>, ReadError> {
// File 离开作用域自动 close,不需要手动 defer
let data = fs::read(path)?; // ? 自动通过 From 转换错误类型
Ok(data)
}
这类迁移 AI 做得相当好,因为映射规则清晰、模式固定。这是"语法翻译"能胜任的部分。
5.2 手动内存管理:从显式分配器到所有权
Zig 里到处传 allocator,Rust 里所有权自动管理。这是迁移质量分水岭:
// Zig:调用方拥有返回值,负责释放
fn joinPaths(allocator: std.mem.Allocator, base: []const u8, name: []const u8) ![]u8 {
const result = try allocator.alloc(u8, base.len + 1 + name.len);
@memcpy(result[0..base.len], base);
result[base.len] = '/';
@memcpy(result[base.len + 1 ..], name);
return result;
}
地道的 Rust 写法(safe,用所有权):
// Rust:String 拥有自己的内存,Drop 时自动释放,零 unsafe
fn join_paths(base: &str, name: &str) -> String {
let mut result = String::with_capacity(base.len() + 1 + name.len());
result.push_str(base);
result.push('/');
result.push_str(name);
result
}
但 AI 偷懒时可能产出的写法(用 unsafe 直译):
// 反面教材:机械翻译 Zig 的裸内存操作,引入不必要的 unsafe
fn join_paths_bad(base: &str, name: &str) -> *mut u8 {
let total = base.len() + 1 + name.len();
unsafe {
let layout = std::alloc::Layout::array::<u8>(total).unwrap();
let ptr = std::alloc::alloc(layout);
std::ptr::copy_nonoverlapping(base.as_ptr(), ptr, base.len());
*ptr.add(base.len()) = b'/';
std::ptr::copy_nonoverlapping(
name.as_ptr(),
ptr.add(base.len() + 1),
name.len(),
);
ptr // 调用方拿到裸指针,还得手动 dealloc——把 Zig 的坑原样搬过来了
}
}
第二种写法能编译、能过测试,但它完全浪费了 Rust 的价值。13000 处 unsafe 里,恐怕有大量就是这类"能跑但没意义"的直译。这就是为什么迁移的"数量"完成得很快,但"质量"是另一回事。
5.3 defer 到 Drop:资源清理的范式转换
Zig 的 defer/errdefer 是显式的作用域清理:
// Zig
fn processRequest(allocator: std.mem.Allocator) !void {
const conn = try openConnection();
defer conn.close(); // 函数退出时执行
const buf = try allocator.alloc(u8, 4096);
errdefer allocator.free(buf); // 只在出错时执行
try conn.read(buf);
// 成功路径下 buf 的所有权转交给别人,所以用 errdefer 而非 defer
try handOff(buf);
}
Rust 里,defer 变成 Drop trait 的自动调用,errdefer 这种"仅错误路径清理"的语义要靠 RAII + 所有权转移表达:
// Rust:Drop 自动管理,所有权转移天然处理 errdefer 语义
struct Connection { /* ... */ }
impl Drop for Connection {
fn drop(&mut self) {
// 等价于 defer conn.close()
self.close();
}
}
fn process_request() -> Result<(), Error> {
let conn = open_connection()?; // conn 离开作用域自动 close
let buf = vec![0u8; 4096]; // buf 出错时自动释放(等价 errdefer)
conn.read(&buf)?;
hand_off(buf)?; // 所有权转移,buf 不再由本函数释放
Ok(())
}
这是 Rust 优于 Zig 的地方之一:RAII 让"错误路径的资源清理"变成默认行为,不需要手写 errdefer。地道迁移能真正提升代码健壮性——前提是 AI 愿意重构而不是直译。
5.4 C++ 互操作:从 @cImport 到 bindgen + FFI
这是迁移里最硬的骨头。Bun 深度依赖 JavaScriptCore(C++),Zig 能 @cImport 直接吃头文件,Rust 得靠 bindgen 生成 FFI 绑定,而且 C++ 的名字修饰(name mangling)、虚表、异常,FFI 都处理不好,通常要包一层 C 接口。
// Rust:通过 extern "C" 声明 FFI,配合 bindgen 生成的绑定
#[repr(C)]
pub struct JSContextRef(*mut std::ffi::c_void);
extern "C" {
fn JSEvaluateScript(
ctx: JSContextRef,
script: *const std::ffi::c_char,
this_object: *mut std::ffi::c_void,
source_url: *const std::ffi::c_char,
starting_line_number: i32,
exception: *mut *mut std::ffi::c_void,
) -> *mut std::ffi::c_void;
}
pub fn eval_script(ctx: JSContextRef, code: &std::ffi::CStr) -> Result<*mut std::ffi::c_void, ()> {
let mut exception = std::ptr::null_mut();
// FFI 调用本身就是 unsafe,无可避免
let result = unsafe {
JSEvaluateScript(ctx, code.as_ptr(), std::ptr::null_mut(),
std::ptr::null(), 0, &mut exception)
};
if !exception.is_null() {
return Err(());
}
Ok(result)
}
FFI 边界的 unsafe 是"合理的 unsafe"——因为它本质上就是在和外部 C++ 世界打交道,编译器无法验证对面的行为。这部分的 unsafe 数量是 Zig 和 Rust 都逃不掉的。但把它和"内部逻辑本可以 safe 却被直译成 unsafe"的部分区分开,才能看清 13000 这个数字里,哪些是必要成本、哪些是技术债。
六、性能优化:LTO、二进制体积与编译速度的罗生门
官方博客把 Rust 版的性能提升归功于一系列工程优化。但这里有几个被 Kelley 戳破的技术真相,程序员必须知道。
6.1 LTO 不是 Rust 的专利
博客把性能提升部分归因于 LTO(链接时优化)。事实是:Zig 从一开始就支持 LTO,而且早期是默认开启的,后来因为踩了太多 LLVM 的 bug 才改成默认关闭——而这些 LLVM bug 在 Rust 里同样存在(两者都用 LLVM 后端)。
# Rust 开 LTO 就是 Cargo.toml 里几行配置
[profile.release]
lto = "fat" # 全量 LTO
codegen-units = 1 # 牺牲编译并行度换更好的优化
opt-level = 3
panic = "abort" # 去掉 unwinding 代码,减小体积
strip = true # 剥离符号表
换句话说,如果给 Zig 版本也认真开 LTO,性能差距可能没那么大。把"开了 LTO 后变快"算成"换 Rust 的功劳",是不严谨的。
6.2 二进制瘦身与重写无关
博客列举了一堆减少二进制体积的工程工作来证明 Rust 版更好。但这些工作——去死代码、strip 符号、优化 panic 策略、按需编译特性——大部分和"用哪门语言"无关,是任何语言都该做、本该在 Zig 版里就做的功课。Kelley 的推测很可能是对的:博客拖了很久才发,就是因为团队在补做这些"和重写无关但能让数据好看"的优化。
6.3 编译速度:那个没人提的房间里的大象
这是最致命的一击。Kelley 给了一组硬数据作对比:
- Zig 编译器项目本身约 60 万行代码(和重写前的 Bun 体量相当),从头全量编译只要 16 秒,之后每次增量编译 90 毫秒。
- Bun 重写后(Rust)对应的编译数据是多少?博客只字未提。
熟悉 Rust 的人都知道,Rust 的编译速度是它长期被诟病的痛点。一个百万行、开了 fat LTO + codegen-units=1 的 Rust 项目,全量编译很可能是分钟级甚至更久,增量编译也远慢于 Zig。对一个需要频繁迭代的运行时项目,编译速度直接影响开发体验和 CI 成本。博客回避这个数据,本身就说明了问题。
6.4 客观地说 Rust 的性能优势在哪
平心而论,Rust 也不是没有真实优势:
- 更成熟的优化器调教:Rust 的 LLVM 集成打磨多年,某些场景下代码生成质量更稳定。
- 零成本抽象的迭代器:
iter().map().filter().collect()这套在优化后往往和手写循环一样快,且更不易出错。 - 更好的别名分析:Rust 的所有权模型给了编译器更强的
noalias保证(类似 C 的restrict),理论上能让优化器更激进。
// Rust 的 noalias 优势:编译器知道 a 和 b 不重叠,可以放心向量化
fn add_slices(a: &[f32], b: &[f32], out: &mut [f32]) {
for i in 0..out.len() {
out[i] = a[i] + b[i]; // 借用规则保证 out 不和 a/b 重叠,可自动 SIMD
}
}
所以 Rust 版能更快,是可信的。但"能更快"和"因为换了 Rust 所以快"是两回事,中间隔着大量本可在 Zig 里做的工程投入。
七、AI 迁移方法论的可复用经验
抛开 Bun 这个具体案例的争议,这次事件给"用 AI 做大规模代码迁移"沉淀了一些真正可复用的方法论,这对每个工程团队都有价值。
1)先写"迁移宪法"(PORTING.md)
不要让 AI 自由发挥。先人工定义好所有跨语言映射规则、命名约定、错误处理范式、内存管理策略,形成一份稳定的规范文档,让 AI 每次翻译都遵循同一套规则。一致性比局部最优更重要。
2)拓扑排序,从底层往上搬
按依赖关系排序,先搬没有依赖的底层模块(内存、字符串、集合),再往上。这样每搬一层,下面都是已验证的稳定地基。
3)测试套件是必要条件,不是充分条件
用现有测试当回归裁判是对的,但要清醒:测试通过 ≠ 正确,尤其是内存安全和并发问题。迁移后必须补 fuzzing、内存检测(Miri、ASan)、以及针对 unsafe 块的专项人工审查。
# 迁移后的必要验证,绝不能只靠 bun test
cargo miri test # 检测 unsafe 代码里的未定义行为
cargo test --release # release 模式下再跑一遍
RUSTFLAGS="-Z sanitizer=address" cargo test # ASan 抓内存错误
cargo fuzz run parser # 对解析器等关键路径做模糊测试
4)区分"直译"和"重构",用 unsafe 数量当质量指标
迁移后统计 unsafe 的数量和分布。FFI 边界的 unsafe 是合理的,内部逻辑里泛滥的 unsafe 是技术债。把 unsafe 密度作为迁移质量的量化指标,指导后续的 safe 化重构。
5)成本要算总账
11 天 + 16.5 万美元只是"翻译"的成本。真正的成本还包括:后续把 13000 处 unsafe 逐步 safe 化的工程投入、编译速度变慢带来的长期 CI 成本、以及潜在生产 bug 的排查成本。AI 加速的是最机械的那一段,不是全部。
八、这件事对整个行业意味着什么
站在 2026 年年中回看,Bun 的 Zig→Rust 重写是一个标志性事件,它至少确认了三件事,也留下三个悬念。
三个确认:
AI 已经能扛住百万行级别的机械迁移。 这在两年前是不可想象的。同源语言间的语法翻译、模式转换,AI 的效率和一致性已经超过人类团队。这个能力是真实的,会重塑遗留系统现代化的成本结构。
语言选择从来不只是技术决策。 Zig 创始人的回应把这层窗户纸捅破了:Bun 换语言,掺杂了收购后的品牌解耦、社区关系的破裂、管理理念的冲突。技术选型永远嵌在组织、人和商业的语境里。
内存安全是工程投入的结果,不是语言的赠品。 Rust 给你更好的护栏,但 13000 处 unsafe 证明:如果你不认真用护栏,换语言只是把风险换了个马甲。同样,Zig 写得好(TigerBeetle)一样扎实,写得烂(据称的 Bun)一样是雷。
三个悬念:
那 13000 处 unsafe 会被 safe 化吗? 如果会,需要多久、多少投入?如果不会,Bun 的"内存安全"承诺就打了大折扣。这是检验这次重写成色的核心指标。
编译速度会不会拖垮迭代? 一个回避编译数据的运行时项目,长期开发体验存疑。
AI 重写会不会成为"技术债洗白"的手段? 用 AI 快速重写来掩盖"不修 bug、不做工程投入"的习惯——如果这成为行业模式,那才是真正危险的信号。因为工具再强,也救不了不刹车的驾驶习惯。
九、总结:工具很强,但写代码的人才是最复杂的变量
Andrew Kelley 那句话值得刻在每个工程师的桌上:"代码写完就完事了?不,写代码的人才是最复杂的变量。"
Bun 用 11 天、96 万行、16.5 万美元完成了一次令人瞠目的 AI 迁移,这展示了 AI 编码能力的真实边界——它能在机械翻译上无限接近人类团队的天花板并大幅超越其速度。这是了不起的成就,值得肯定。
但同一件事的另一面同样真实:13000 处 unsafe、被回避的编译速度、被偷换概念的"内存安全归因"、被算到语言头上的工程优化——这些都提醒我们,AI 加速的是"体力活",而工程质量、架构判断、长期维护性这些"脑力活"和"责任活",依然牢牢握在人的手里。
对我们普通程序员的启示很朴素:
- 别迷信语言能自动带来安全,安全是你写测试、做 fuzzing、认真 review 挣来的;
- 别迷信 AI 能替你思考架构,它能替你翻译,但替不了你判断;
- 别被漂亮的数字(99.8%、11 天)带节奏,学会追问它没告诉你的那 0.2% 和那些被回避的指标。
Bun 的这次重写,与其说是"Rust 战胜了 Zig",不如说是"AI 时代,工程纪律比任何时候都更重要"的一次大型公开课。工具的进化会一次次刷新我们对"可能性"的认知,但真正决定软件成败的,永远是那个坐在键盘前——或者,如今坐在 AI 对话框前——做判断、负责任的人。
慢慢喝茶,慢慢看这场大戏继续演下去。而我们能做的,是别在下一次技术狂热里,忘了最基本的工程常识。