编程 6天、96万行:一次被内存泄漏逼出来的语言迁移——Bun从Zig到Rust的完整复盘

2026-05-31 11:51:31 +0800 CST views 9

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 的执行模式是这样的:

  1. 逐文件翻译:按文件依赖顺序,逐一处理 Zig 源文件
  2. 命名规范转换snake_case vs camelCase,Zig 的 camelCase 方法名需要映射为 Rust 的 snake_case
  3. 可选参数处理:Zig 的可选参数(默认参数值)需要映射为 Rust 的 builder pattern 或 Option 参数
  4. 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 的工作就是系统性地解决这些借用规则冲突。这需要:

  1. 分析整个数据流,确定哪些引用是 alias 的
  2. 引入 Rc<RefCell<T>>Arc<Mutex<T>> 处理需要共享可变性的场景
  3. 使用 unsafe 代码块包裹那些"确实安全但编译器无法自动证明"的逻辑
  4. 重构那些"共享状态设计"改为"拥有权清晰的设计"

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"。正确的做法是:

  1. 外围包优先:从不涉及核心数据结构的模块开始翻译
  2. 稳定接口先行:先翻译公共 API,保持外部调用方式不变
  3. unsafe 兜底:先用 unsafe 包裹不安全的部分,编译通过后再逐步审计
  4. 测试驱动验证:每个模块翻译后立即运行测试套件验证行为等价性

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 年的视角看,三种语言在基础设施领域的定位正在分化:

维度RustZigGo
内存安全编译期保证手动管理(更精细但更累)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

推荐文章

Vue3如何执行响应式数据绑定?
2024-11-18 12:31:22 +0800 CST
JavaScript设计模式:观察者模式
2024-11-19 05:37:50 +0800 CST
JavaScript数组 splice
2024-11-18 20:46:19 +0800 CST
html一些比较人使用的技巧和代码
2024-11-17 05:05:01 +0800 CST
18个实用的 JavaScript 函数
2024-11-17 18:10:35 +0800 CST
2024年公司官方网站建设费用解析
2024-11-18 20:21:19 +0800 CST
程序员茄子在线接单