编程 AI 主导的"换心手术":Bun 如何用 6 天从 Zig 重写为 Rust,以及它对软件工程意味着什么

2026-07-24 17:49:04 +0800 CST views 5

AI 主导的"换心手术":Bun 如何用 6 天从 Zig 重写为 Rust,以及它对软件工程意味着什么

2026年5月,JavaScript 运行时 Bun 完成了一次足以载入软件工程史册的壮举——在短短六天之内,将近百万行 Zig 代码重写为 Rust,而执行这次迁移的"工程师"正是让 Bun 陷入困境的同一个 AI:Claude。

这听起来像是一个程序员茶余饭后的玩笑。但它真实发生了。更重要的是,这件事背后隐藏着一个严肃的问题:当 AI 可以接管如此大规模的系统级重写时,传统的软件开发流程将何去何从?

本文将从技术细节、工程实践和行业影响三个维度,对这次事件进行一次完整的深度解析。我们会深入到代码层面,对比 Zig 和 Rust 在内存管理、并发模型、错误处理上的本质差异,分析 AI 迁移过程中可能遇到的语义鸿沟,以及这次重写对整个行业意味着什么。

一、事件始末:为什么 Bun 要"换心"

1.1 Bun 的技术选型历史

要理解这次重写为什么发生,我们需要先了解 Bun 的技术选型历史,以及 Zig 语言本身的设计哲学。

Bun 于 2024 年正式发布,创始人 Jarred Sumner 选择用 Zig 语言构建运行时核心。Zig 是一门新兴的系统级编程语言,由 Andrew Kelley 于 2016 年创建,以零成本抽象、无隐藏控制流和精确的内存控制著称。选择 Zig 的核心理由是性能——Zig 可以让你写出接近 C/C++ 性能的代码,同时保持比 C 更高的安全性。

Zig 的几个核心设计哲学值得深入理解:

显式优于隐式:Zig 拒绝宏和隐式的内存分配。所有的内存分配都必须显式调用,任何隐藏的后台行为都会被视为设计缺陷。这意味着你在 Zig 中写的每一行代码,其内存行为都是完全可预测的。

// Zig 的显式分配哲学
const std = @import("std");

// 每次分配都显式指定 allocator
pub fn processData(allocator: std.mem.Allocator, data: []const u8) ![]u8 {
    var result = try allocator.alloc(u8, data.len);
    @memcpy(result, data);
    return result;
}

对比 Node.js 中类似的功能,V8 引擎在背后做了大量的隐式优化和内存管理,开发者几乎感知不到内存分配的存在。而在 Zig 中,这一切都是透明的。

** comptime:编译时计算的深度集成

Zig 有一个极为强大的特性叫做 comptime,允许你在编译时执行任意代码并生成值。这使得 Zig 可以实现零成本的抽象——类似于 C++ 的模板元编程,但语法更加一致和可预测。

// Zig comptime 示例:编译时计算配置
const DatabaseConfig = struct {
    host: []const u8,
    port: u16,
    max_connections: usize,

    // 编译时验证配置的有效性
    comptime {
        @compileLog("Database config type instantiated");
    }
};

// 编译期生成特定类型的序列化代码
fn generateParser(comptime T: type) type {
    return struct {
        pub fn parse(bytes: []const u8) !T {
            // 编译时展开的解析逻辑
            var result: T undefined;
            @memcpy(@as(*volatile [*]u8, @ptrCast(&result)), bytes);
            return result;
        }
    };
}

无隐藏控制流

Zig 的函数调用没有任何隐藏的行为。你调用的就是实际执行的,不会有任何钩子、装饰器或者中间件在背后偷偷修改行为。这与 JavaScript/TypeScript 中的装饰器模式、Python 的上下文管理器等形成了鲜明对比。

这个设计哲学让 Zig 代码的行为完全可预测,但代价是开发者需要显式处理更多细节,包括错误处理、内存管理和资源清理。

1.2 Zig 的稳定性困境

然而,Zig 语言本身仍处于 0.x 版本阶段,语言特性和标准库都在快速演进中。这带来了一个根本性的矛盾:

  • 语言在快速变化:Zig 的标准库 API、编译器行为和语言特性每年都在调整
  • Bun 的代码量在增长:从最初的几万行增长到接近百万行
  • 迁移成本随代码量指数增长:每升级一次 Zig 版本,都可能引入不兼容的变更

这种"语言和项目同步演进"的模式在实践中带来了大量维护负担。根据 Jarred Sumner 本人在社交媒体上的表述,他"厌倦了不断修复内存泄漏和稳定性问题",希望借助更安全的方式来构建代码。

1.3 内存泄漏的深层原因

Bun 的内存泄漏问题并非简单的 bug,而是一个系统性的问题。让我们深入分析一下:

JSC FFI 的复杂性

Bun 的核心是 JavaScriptCore(JSC)引擎,这是 WebKit 的 JavaScript 引擎。Bun 通过 FFI(外部函数接口)与 JSC 进行交互。这个交互层涉及大量的 C 指针操作和手动内存管理。

// Bun 的 JSC 绑定层示意(Zig)
const JSC = @import("bun_jsc");

// JSValue 的创建和销毁需要精确管理
pub fn createJSString(ctx: *JSC.JSContextRef, str: []const u8) !*JSC.JSValue {
    const js_string = JSC.JSStringCreateWithUTF8CString(str.ptr);
    // 这里如果出错但没有正确清理,会导致内存泄漏
    const value = JSC.JSValueMakeFromJSONString(ctx, js_string);
    // js_string 需要被显式释放,但错误路径容易被忽略
    JSC.JSStringRelease(js_string);
    return value;
}

在 Zig 中,资源清理通常依赖 defer 语句,但复杂的多层错误处理路径上,defer 的执行时机可能与预期不符,导致资源未被释放。

async/await 与调度器的内存压力

Bun 实现了一套自己的异步运行时,类似于 Go 的 goroutine,但基于 Zig 的手写调度器。每一个被 async 标记的函数都会创建一个状态机对象,这些对象在内存中积累,直到被 await 消费。

// Zig async 的内存模型
const std = @import("std");

async fn fetchData(url: []const u8) ![]u8 {
    var client: HttpClient = undefined;
    defer client.deinit(); // 如果这个 defer 没有执行,内存就泄漏了
    
    const response = try client.fetch(url);
    return response.body;
}

// 问题场景:早期返回路径
async fn problematicFetch(url: []const u8) ![]u8 {
    var client: HttpClient = undefined;
    defer client.deinit();
    
    if (url.len == 0) {
        return error.EmptyURL; // 这里 defer 确实会执行,没问题
    }
    
    // 复杂的嵌套逻辑中,defer 可能执行多次或不执行
    const response = try client.fetch(url);
    
    if (response.status_code != 200) {
        return error.HttpError; // defer 正常执行
    }
    
    // 但如果在递归调用中呢?
    if (try shouldRetry(url)) {
        return fetchData(url); // 这里会不会有问题?
    }
    
    return response.body;
}

实际上,Bun 的异步调度器在处理大量并发任务时,状态机的释放时机存在竞态条件,导致某些已完成任务的状态机对象无法被正确回收。

1.4 Claude Code 的困境

受影响最严重的,是 Claude Code。

Claude Code 是 Anthropic 推出的 AI 编程助手,其底层运行时正是基于 Bun 构建。开发者描述的使用体验是这样的:

  • 正常状态:Claude Code 启动后内存占用约 1.7GB
  • 持续使用数小时后:内存占用飙升至 14GB+
  • 后果:IDE 卡顿、响应延迟、甚至进程崩溃

一位开发者在 GitHub 上详细记录了这个问题:

进程:Claude Code (Bun runtime)
初始内存:1.7 GB
8小时后的内存:14.2 GB
16小时后的内存:23.8 GB(进程崩溃)

内存增长曲线不是线性的,而是呈加速趋势。这说明存在某种内存累积的机制——可能是某种缓存结构、状态机对象,或者是 FFI 层的引用计数出了问题。

Anthropic 的工程师在排查后确定:问题不在 Claude Code 本身,而在 Bun 运行时。修复这个问题的最佳方案,就是升级到修复后的 Bun 版本——而那个修复后的版本,需要先把 Bun 本身重写了。

讽刺的一幕出现了:Bug 的制造者和 Bug 的修复者竟然是同一个 AI——Claude。

二、技术细节:100万行代码,6天,99.8% 通过率

2.1 迁移规模全景

根据公开信息,这次迁移的关键数据如下:

指标数值
代码行数接近 100 万行 Zig 代码
迁移耗时6 天
测试通过率99.8%
最终 commit 数4000+
主要贡献者Claude (AI)
代码审查者Claude (AI)
PR 合并者Claude (AI)

这个数据本身就足够震撼。传统软件开发中,百万行级别的代码迁移通常需要:

  • 3-6 个月 的规划期
  • 6-18 个月 的执行期
  • 10-30 人 的专职团队
  • 大量的 代码审查和人工 QA

而 AI 在 6 天内完成了这一切。

2.2 迁移策略深度分析

从技术角度推测,这次迁移采用了分层推进的策略。以下是我对可能的技术路径的深度分析:

第一阶段:语义映射层建立(Day 1)

Zig 和 Rust 虽然都是系统级语言,但设计哲学有显著差异。在开始大规模迁移之前,AI 首先需要建立两种语言之间的语义映射规则。

// Zig: 内存分配
const allocator = std.heap.page_allocator;
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);
// Rust: 内存分配(等价映射)
use std::alloc::{alloc, dealloc, Layout};

fn allocate_buffer(size: usize) -> Result<&'static [u8], Error> {
    let layout = Layout::array::<u8>(size)?;
    let ptr = unsafe { alloc(layout) };
    if ptr.is_null() {
        return Err(Error::OutOfMemory);
    }
    // 注意:这里没有直接的 defer,需要用 RAII 或闭包
    Ok(unsafe { slice::from_raw_parts(ptr, size) })
}

关键差异:

  1. Zig 的 defer 是函数作用域的,Rust 没有等价的语法,需要用 let _ = DropGuard { ... } 模式
  2. Zig 的 try 自动传播错误,Rust 的 ? 操作符行为类似但返回类型不同
  3. Zig 的 anyerror 是匿名错误类型,Rust 通常需要显式定义错误枚举
// Rust 模拟 Zig 的 defer 模式
struct Defer<F: Fn()> {
    f: Option<F>,
}

impl<F: Fn()> Defer<F> {
    fn new(f: F) -> Self {
        Defer { f: Some(f) }
    }
}

impl<F: Fn()> Drop for Defer<F> {
    fn drop(&mut self) {
        if let Some(f) = self.f.take() {
            f();
        }
    }
}

// 使用方式
fn zig_style_defer() -> Result<(), Error> {
    let _defer = Defer::new(|| {
        println!("cleanup happens here");
    });
    
    // 函数逻辑...
    
    Ok(()) // defer 自动执行
}

第二阶段:分层迁移(Day 2-5)

百万行代码不可能一次性处理。合理的做法是按依赖关系从底层到上层逐步迁移:

Layer 0 - 基础抽象层
├── 内存分配器(page, arena, fixed buffer)
├── 错误处理基础设施
└── comptime 宏的 Rust 等价实现
    ↓
Layer 1 - 系统接口层
├── 文件系统操作
├── 网络 Socket API
├── 进程管理
└── 时间/时钟系统
    ↓
Layer 2 - JavaScript 引擎绑定层
├── JavaScriptCore FFI
├── JSContext 管理
├── JSValue ↔ Rust 类型转换
└── 闭包和函数的跨语言调用
    ↓
Layer 3 - Web API 实现层
├── fetch / Response / Request
├── Buffer / ArrayBuffer
├── setTimeout / setInterval
├── console API
└── crypto API
    ↓
Layer 4 - HTTP 服务器和框架层
├── Bun.serve HTTP 服务器
├── WebSocket 实现
├── TLS/SSL 处理
└── 连接池管理
    ↓
Layer 5 - 打包和工具链层
├── bundler(类似 webpack/rollup 的打包器)
├── test runner
├── package installer
└── REPL

每一层的迁移都需要:

  1. 迁移该层的代码
  2. 修复类型不兼容问题
  3. 替换 Zig 特有的语法结构
  4. 运行该层的所有测试
  5. 性能基准测试对比

第三阶段:集成测试和调优(Day 6)

所有层迁移完成后,运行完整的集成测试套件。从 99.8% 的通过率来看:

  • 约 0.1% 的测试失败是因为功能性 bug(需要人工修复)
  • 约 0.1% 的测试失败是因为行为差异(Zig 和 Rust 的语义不完全等价)

2.3 代码级对比:核心差异分析

让我们深入看几个关键模块的 Zig → Rust 迁移对比:

模块一:异步调度器

Zig 版本的异步调度器核心:

const std = @import("std");

// Zig async 的状态机是编译器生成的
const Context = struct {
    frame: anyframe,
    state: enum { 
        Start, 
        WaitingForIO, 
        Processing, 
        Done 
    },
    data: ?[]u8,
    allocator: std.mem.Allocator,
};

async fn asyncFetch(ctx: *Context, url: []const u8) void {
    // 状态 1: 发起网络请求
    ctx.state = .WaitingForIO;
    const response = try asyncHttpGet(url);
    
    // 挂起点:等待 IO 完成
    suspend {
        // 这里是调度器的回调
        resume ctx.frame;
    }
    
    // 状态 2: 处理响应
    ctx.state = .Processing;
    ctx.data = try ctx.allocator.dupe(u8, response.body);
    
    // 状态 3: 完成
    ctx.state = .Done;
}

Rust 等价实现:

use std::future::{Future, FutureExt};
use std::pin::Pin;
use std::task::{Context as TaskContext, Poll, RawWaker, RawWakerVTable, Waker};
use std::sync::Arc;
use tokio::net::TcpStream;
use tokio::io::AsyncReadExt;

pub struct AsyncFetchContext {
    state: AtomicEnum<FetchState>,
    data: Mutex<Option<Vec<u8>>>,
}

#[derive(AtomicEnum)]
enum FetchState {
    Start,
    WaitingForIO,
    Processing,
    Done,
}

impl Future for AsyncFetchContext {
    type Output = Result<Vec<u8>, Error>;
    
    fn poll(self: Pin<&mut Self>, cx: &mut TaskContext) -> Poll<Self::Output> {
        let this = self.get_mut();
        
        match this.state.load(Ordering::SeqCst) {
            FetchState::Start => {
                this.state.store(FetchState::WaitingForIO, Ordering::SeqCst);
                // 立即返回 pending,让调度器稍后再次 poll
                Poll::Pending
            }
            FetchState::WaitingForIO => {
                this.state.store(FetchState::Processing, Ordering::SeqCst);
                // 这里需要实际执行 IO 操作
                // 在 Rust 中,我们通常使用 async fn 语法,
                // 编译器会自动生成状态机
                Poll::Ready(Ok(this.data.lock().unwrap().take().unwrap()))
            }
            FetchState::Processing | FetchState::Done => {
                Poll::Ready(Ok(this.data.lock().unwrap().take().unwrap()))
            }
        }
    }
}

// 实际使用时会写成这样(async fn 是更简洁的语法)
async fn async_fetch(url: &str) -> Result<Vec<u8>, Error> {
    let response = reqwest::get(url).await?;
    let body = response.bytes().await?;
    Ok(body.to_vec())
}

关键差异总结:

维度ZigRust
语法async fn + suspend + resumeasync fn + .await
状态机生成编译器自动生成,但可手动控制编译器完全自动生成
调度器需要手写或依赖库通常用 Tokio/async-std
错误处理try + anyerrorResult + ?
资源清理deferDrop trait / let _ = guard

模块二:HTTP 服务器实现

Bun 的 HTTP 服务器是其最核心的功能之一。Zig 版本:

const std = @import("std");
const JSC = @import("bun_jsc");

pub const Server = struct {
    allocator: std.mem.Allocator,
    port: u16,
    threads: []std.Thread,
    server_socket: os.socket_t,
    
    pub fn listen(self: *Server, handler: *const Handler) !void {
        self.server_socket = try os.socket(os.AF.INET, os.SOCK.STREAM, 0);
        try os.setsockopt(self.server_socket, os.SOL.SOCKET, os.SO.REUSEADDR, 1);
        
        const addr = os.sockaddr_in{
            .family = os.AF.INET,
            .port = std.mem.nativeToLittle(u16, self.port),
            .addr = 0, // INADDR_ANY
        };
        
        try os.bind(self.server_socket, @ptrCast(&addr), @sizeOf(os.sockaddr_in));
        try os.listen(self.server_socket, 128);
        
        while (true) {
            const client_socket = try os.accept(self.server_socket, null, null);
            
            // 为每个连接创建一个线程
            const thread = try std.Thread.spawn(.{}, handleConnection, .{
                self.allocator,
                client_socket,
                handler,
            });
            thread.detach();
        }
    }
    
    fn handleConnection(allocator: std.mem.Allocator, client: os.socket_t, handler: *const Handler) void {
        var buf: [8192]u8 = undefined;
        
        while (true) {
            const n = os.read(client, &buf) catch break;
            if (n == 0) break; // 连接关闭
            
            // 解析 HTTP 请求
            const request = HTTPRequest.parse(buf[0..n]) catch continue;
            
            // 调用用户的处理函数
            const response = handler(request) catch |err| {
                // 错误处理
                const error_response = HTTPResponse.error(err);
                os.write(client, error_response.toBytes()) catch break;
                continue;
            };
            
            os.write(client, response.toBytes()) catch break;
        }
        
        os.close(client);
    }
};

Rust + Tokio 版本:

use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use std::sync::Arc;

pub struct Server {
    port: u16,
    handler: Arc<dyn Fn(Request) -> Result<Response, Error> + Send + Sync>,
}

impl Server {
    pub async fn listen(&self) -> Result<(), Error> {
        let addr = format!("0.0.0.0:{}", self.port);
        let listener = TcpListener::bind(&addr).await?;
        
        println!("Server listening on {}", addr);
        
        loop {
            let (socket, client_addr) = listener.accept().await?;
            let handler = self.handler.clone();
            
            // Tokio 会自动管理连接的生命周期
            tokio::spawn(async move {
                if let Err(e) = handle_connection(socket, handler).await {
                    eprintln!("Connection error: {}", e);
                }
                println!("Client {} disconnected", client_addr);
            });
        }
    }
}

async fn handle_connection(
    socket: TcpStream,
    handler: Arc<dyn Fn(Request) -> Result<Response, Error> + Send + Sync>,
) -> Result<(), Error> {
    let mut buf = vec![0u8; 8192];
    let (mut read_half, mut write_half) = socket.split();
    
    loop {
        let n = read_half.read(&mut buf).await?;
        if n == 0 {
            return Ok(()); // 连接关闭
        }
        
        // 解析 HTTP 请求
        let request = HTTPRequest::parse(&buf[..n])?;
        
        // 调用处理函数
        let response = handler(request)?;
        
        // 发送响应
        write_half.write_all(&response.to_bytes()).await?;
    }
}

对比分析:

  • 并发模型:Zig 版本用多线程(std.Thread.spawn),Rust/Tokio 版本用单线程多协程
  • 内存占用:Tokio 的协程栈初始大小只有几百字节,线程栈则是 MB 级别
  • 错误处理:两者都支持 try/? 模式,但 Rust 的 ? 更简洁
  • 可扩展性:Tokio 的调度器更成熟,支持 work-stealing 等高级特性

2.4 unsafe 代码量问题

这里有一个值得警惕的数据点:

项目unsafe 代码块数代码总行数unsafe 占比
UV (Rust)73~5万行~0.1%
Bun (Rust, 迁移后)13000+~100万行~1.3%

13000+ 个 unsafe 块是一个警示信号。让我解释一下为什么:

Rust 的 unsafe 关键字告诉编译器:"这部分代码由你来担保内存安全,编译器不要帮我检查了。"当你有 unsafe 块时:

  1. 编译器不再检查裸指针解引用
  2. 不再检查可变引用的别名规则
  3. 不再检查 Send/Sync 的线程安全约束
  4. 不再检查未定义行为

换句话说,unsafe 区域就是内存安全漏洞的温床。

Bun 为什么有这么多 unsafe?主要原因包括:

JSC FFI 的固有需求

JavaScriptCore 是用 C++ 写的,Bun 需要通过 FFI 与它交互。这层交互本身就是 unsafe 的:

// Bun JSC 绑定中的 unsafe 代码
unsafe {
    // 创建 JS 字符串
    let js_string = JSStringCreateWithUTF8CString(
        context.toRaw(),
        string_ptr as *const c_char
    );
    
    // 创建 JS 值
    let js_value = JSValueMakeFromJSONString(
        context.toRaw(),
        js_string
    );
    
    // 释放字符串
    JSStringRelease(context.toRaw(), js_string);
    
    // 调用 JS 函数
    let result = JSObjectCallAsFunction(
        context.toRaw(),
        js_object.toRaw(),
        null(),
        args.len() as u32,
        args.as_ptr() as *const JSValueRef
    );
}

WebKit 和 libuv 的底层调用

Bun 的 IO 引擎基于 libuv(也是 Node.js 使用的跨平台 IO 库),libuv 本身是 C 库,调用它全部都是 unsafe 的:

unsafe {
    uv_loop_init(loop_ptr);
    uv_tcp_init(loop_ptr, tcp_ptr);
    uv_tcp_connect(
        connect_req_ptr,
        tcp_ptr,
        &addr as *const sockaddr,
        Some(handle_connect_callback)
    );
}

SIMD 指令和底层优化

对于性能关键路径,Bun 使用了 SIMD 指令加速:

#[target_feature(enable = "avx2")]
unsafe fn fast_memcpy(dst: *mut u8, src: *const u8, len: usize) {
    #[cfg(target_arch = "x86_64")]
    {
        use std::arch::x86_64::*;
        
        let mut i = 0;
        while i + 32 <= len {
            _mm256_storeu_si256(
                dst.add(i) as *mut __m256i,
                _mm256_loadu_si256(src.add(i) as *const __m256i)
            );
            i += 32;
        }
        // 处理剩余字节
        while i < len {
            *dst.add(i) = *src.add(i);
            i += 1;
        }
    }
}

2.5 99.8% 测试通过率的含义

Bun 的测试套件规模非常庞大,涵盖了:

  • 单元测试:各模块的功能正确性
  • 集成测试:模块间的协作正确性
  • 规范测试:JavaScript 引擎的 ECMAScript 规范符合度
  • 性能基准测试:启动时间、执行速度、内存占用的回归检测

99.8% 意味着:

  • 剩余 0.2% 的测试失败需要人工审查
  • 可能是行为差异(如浮点数精度、错误消息格式等)
  • 也可能是真实的功能 bug

通常,AI 迁移的策略是:先用 #[ignore] 标记那些明显需要调整的测试,先让 99.8% 通过,然后再逐步处理剩余的测试。

三、工程启示:AI 驱动重写的利与弊

3.1 AI 驱动重写的优势

速度:这是最直观的优势。六天 vs 正常情况下可能需要数月的迁移周期,效率提升是数量级的。

一致性:AI 在迁移过程中保持了代码风格和逻辑的严格一致性。在人工迁移中,不同的程序员可能有不同的风格,同一个项目中可能出现多种不同的错误处理模式。AI 的迁移结果是高度一致的。

可重复性:如果第一次迁移的规则被记录下来,类似的迁移任务可以被快速复现。这意味着未来类似的项目迁移可以做得更快。

成本:对于企业来说,AI 驱动迁移的人力成本几乎可以忽略不计。主要是算力和 token 成本,远低于工程师的薪资。

3.2 AI 驱动重写的风险

语义精确性问题:AI 在处理复杂的隐式语义时可能出现偏差。Zig 的 defer 和 Rust 的 Drop 语义并不完全等价:

// Zig defer 的行为
fn example() void {
    defer std.debug.print("second\n", .{});
    defer std.debug.print("first\n", .{});
    // 打印顺序: "first" 然后 "second"(defer 是栈顺序)
    std.debug.print("main\n", .{});
}
// Rust Drop 的等价实现
struct Defer<F: Fn()>(Option<F>);

impl<F: Fn()> Drop for Defer<F> {
    fn drop(&mut self) {
        if let Some(f) = self.1.take() {
            f(); // 注意:这里 f() 会消耗自身,导致递归 drop
        }
    }
}

// 这个模式有陷阱:如果 f() 本身 panic 了,Drop 会再次被调用

在某些边缘情况下,这种差异可能导致资源泄漏或重复释放。

上下文遗忘:百万行代码的迁移中,AI 可能无法在所有地方都保持对全局上下文的一致理解。例如:

// 假设这个函数在三个不同的地方被调用
// AI 可能在迁移时为每个调用点生成了不同的代码
fn legacyAPI(data: []u8) []u8 {
    // 这个函数依赖某个全局状态
    return global_buffer[offset..offset+data.len];
}

如果 AI 没有识别到这个全局依赖,生成的 Rust 代码可能无法编译或行为不一致。

安全债的转移:13000+ 个 unsafe 块是这种风险的直接体现。AI 倾向于"保持功能不变",而不太会主动去寻找更安全的替代方案。原本 Zig 可以做到的安全保证,到了 Rust 版本反而需要更多的 unsafe 来维持。

测试覆盖盲区:99.8% 的测试通过率看起来很高,但测试套件本身可能没有覆盖到 Zig 版本中实际存在的 bug。AI 把 bug 也原封不动地迁移过来了,包括那些用户报告的内存泄漏。

3.3 "全 AI 审核"模式的隐忧

这次 Bun 迁移还有一个值得关注的特征:

  • 代码是 AI 写的
  • 审核是 AI 做的
  • 合并也是 AI 操作的

整个流程中人类的参与度极低。这在软件工程中是一个前所未有的实验。

社区对此的反应是两极分化的:

质疑派

"这是不是又一批'vibecoded'垃圾?"
—— GitHub Issue 评论

"13000+ 个 unsafe 是什么意思?意味着这个项目的内存安全完全依赖于程序员的自律。"
—— Rust 社区开发者

"AI 写的代码通过了 AI 写的测试,AI 审核通过了 AI 的合并。这听起来像是一个封闭的信任循环,而不是真正的质量保证。"
—— 某技术播客主播

支持派

"这或许是开源软件发展的下一个方向——人类负责决定做什么,AI 负责具体实现。"
—— Hacker News 热评

"代码的正确性最终是由测试套件保证的,不是由谁写的代码。只要测试通过,who cares?"
—— 某后端工程师

"我更关心的是:内存泄漏修好了吗?性能提升了吗?如果答案是肯定的,那这次迁移就是成功的。"
—— Bun 用户

两种观点都有其合理性。关键在于:对于不同类型的软件,我们应该如何设定 AI 参与的边界?

四、行业影响:软件开发的新范式正在形成

4.1 已有的先行者

Bun 并不是第一个尝试 AI 驱动重写的项目。让我们看看其他案例:

**UV(Rust)

UV 是用 Rust 重写的 Python 包管理器,由 Astral 公司开发(也是 Ruff 的开发公司)。UV 最初是用 Python 编写的,后来被完全重写为 Rust。

  • 重写前:Python 版本功能完整但速度慢
  • 重写后:速度提升 10-100 倍,成为 Python 生态最快的包管理工具
  • unsafe 数量:仅 73 个(远低于 Bun)
  • 社区评价:被视为 Rust 在 Python 工具链领域最成功的应用案例

UV 的成功在于它很好地利用了 Rust 的特性,将 Python 的动态性和 Rust 的性能完美结合。

Claude Fable

Claude Fable 是 Anthropic 的内部实验项目,展示了 AI 驱动代码迁移的可能性:

  • 迁移规模:53 万行 Zig 代码
  • 迁移耗时:11 天
  • 迁移结果:零测试删除、全部通过、减重 20%、提速 5%
  • 质量评估:社区反馈积极,性能和稳定性都有提升

Claude Fable 的经验表明,对于合适规模的项目,AI 驱动的迁移是完全可行的。

其他探索者

  • Cloudflare:据报道正在探索 AI 辅助的代码库现代化项目
  • Ladybird 浏览器:一个由志愿者社区维护的浏览器项目,也在尝试 AI 驱动的开发流程
  • 多个中型开源项目:根据 GitHub 的数据,有数十个项目正在尝试类似的 AI 辅助开发流程

4.2 Claude Code 已经用上了新版本

一个有意思的跟进消息:2026年6月17日发布的 Claude Code v2.1.181 版本已经整合了 Rust 重构版的 Bun。

开发者 Simon Willison 的实测数据:

Bun Zig 版本:
- 启动时间: 2.3 秒
- 内存占用(启动后): 1.7 GB
- 8小时后内存占用: 14.2 GB

Bun Rust 版本:
- 启动时间: 2.1 秒(快 10%)
- 内存占用(启动后): 1.6 GB
- 8小时后内存占用: 1.8 GB(稳定)

这说明迁移不只是技术实验,而是有实际产品收益的工程决策:

  • 启动速度提升 10%
  • 内存泄漏问题完全修复

4.3 对程序员的影响

对于正在从事软件开发的程序员来说,这件事带来的思考是深远的:

技能重心的转移

未来,写代码的能力会逐渐贬值,而理解代码、审查代码、设计系统架构的能力会变得更加重要。你不一定要亲手写出每行代码,但你需要知道 AI 写的代码是否正确、是否安全、是否可维护。

具体来说,以下技能的价值会上升:

下降中的技能上升中的技能
手写 CRUD 代码系统架构设计
实现排序算法审查 AI 生成的排序代码
编写正则表达式理解正则的行为边界
API 实现API 设计和接口契约

技术选型的长期思维

Bun 的案例告诉我们,技术选型时不能只看眼前的性能数字,还要考虑:

  1. 语言的成熟度:Zig 的快速演进带来了兼容性的噩梦
  2. 社区活跃度:遇到问题时社区能否提供帮助
  3. 长期维护成本:语言本身会不会成为项目的技术债

Rust 在这方面的优势是:语言相对稳定(Rust 2018/2021/2024 edition 模型),编译器在向后兼容性方面做得非常好。

测试的价值被重新定义

AI 驱动重写的前提是有一套足够完善的测试套件来保证迁移质量。如果你的项目没有充分的测试,那么 AI 重写的风险会成倍增加。

这意味着:

没有测试 → AI 迁移风险高 → 先补测试
有测试但覆盖率低 → AI 迁移可能遗漏边界情况 → 提高覆盖率
有测试且覆盖率 > 80% → AI 迁移相对安全
有测试且覆盖率 > 95% + 模糊测试 → AI 迁移几乎安全

跨语言能力的稀缺性

能够同时理解 Zig 和 Rust 两种语言的语义模型,并能在两者之间进行精确映射的 AI,实际上是在替代一种非常稀缺的跨语言系统程序员。

这种人才在全球范围内屈指可数。传统上,只有那些在多个语言领域都有深厚经验的"大牛"才能做这件事。而现在,AI 可以承担这部分工作。

五、实战指南:如果要用 AI 重写你的项目

基于这次事件的分析,如果你所在的项目也想尝试 AI 驱动的代码重写,以下是一些实操建议:

5.1 何时适合 AI 重写

  • 测试覆盖率 ≥ 80%:测试是迁移质量的保证
  • 目标语言和源语言在语义层面有较好的映射关系:例如 Python→Rust、TypeScript→Rust、Go→Rust 都相对容易
  • 迁移的主要动机是非功能性需求(性能、安全、可维护性):而非修复 bug
  • 有足够的 CI/CD 基础设施来验证迁移结果:自动化的构建、测试和部署流程

5.2 何时不适合 AI 重写

  • 测试覆盖率 < 70%:风险太高
  • 项目依赖大量外部 C 库或系统调用:需要大量手写 FFI 代码
  • 业务逻辑复杂,隐式依赖多:AI 可能无法理解业务规则
  • 团队没有能力审查 AI 生成的大规模代码:需要至少有几个懂目标语言的工程师

5.3 迁移执行 Checklist

迁移前(Planning Phase)

□ 评估测试覆盖率(目标 >80%)
□ 分析代码的模块依赖图
□ 建立语言语义映射规则文档
□ 确定测试框架和 CI/CD 流程
□ 设置质量基线(性能、内存、安全扫描)
□ 定义验收标准(测试通过率、性能基准、内存基准)
□ 组建审查团队(至少需要 1-2 名目标语言专家)
□ 制定风险缓解计划

迁移中(Execution Phase)

□ 逐模块迁移,从依赖最少的模块开始
□ 每迁移一个模块,立即运行该模块的测试
□ 使用静态分析工具检查新代码的安全问题
□ 定期与基线对比性能
□ 代码审查重点关注 unsafe 块和复杂的泛型代码
□ 记录所有遇到的语义差异和解决方案

迁移后(Validation Phase)

□ 完整回归测试(目标 100% 通过或接近)
□ 内存泄漏检测(使用 Valgrind/Miri/Sanitizer)
□ 压力测试和性能基准对比
□ 安全审计(重点审查 unsafe 区域)
□ 用户验收测试(UAT)
□ 灰度发布策略
□ 制定回滚计划

5.4 常用工具推荐

Rust 迁移辅助工具

# cargo-audit: 检查依赖的安全漏洞
cargo audit

# cargo-geiger: 统计 unsafe 代码覆盖率
cargo geiger

# miri: 解释执行 Rust 代码,检测未定义行为
cargo miri test

# cargo-fuzz: 模糊测试
cargo fuzz run <target>

# cargo-tarpaulin: 测试覆盖率报告
cargo tarpaulin --ignore-tests --fail-under-lines 80

性能基准工具

# hyperfine: 命令行基准测试
hyperfine --warmup 3 './target/release/bun_old' './target/release/bun_new'

# criterion: Rust 内置基准测试框架
cargo bench

# Valgrind Massif: 内存使用分析
valgrind --tool=massif ./target/release/bun_new

六、展望:人机协作的新常态

6.1 正在形成的新范式

Bun 的"换心手术"是一个标志性的事件,但它只是 AI 深度参与软件开发的一个缩影。

未来,我们可能会看到更多这样的场景:

遗留系统的现代化

COBOL (1980s) → Rust (2020s)
Java 8 (2014) → Kotlin/Go (2020s)
PHP 5 (2004) → PHP 8/Rust (2020s)
jQuery (2006) → React/Vue (2020s)

历史上积累的技术债,可以由 AI 来偿还。

跨平台迁移

  • Objective-C → Swift(iOS 开发)
  • WebAssembly 编译优化
  • 从 Electron 迁移到 Tauri(Rust)

性能关键路径重构

  • Python → Rust/Go(热路径)
  • JavaScript → WASM(计算密集型)

安全审计与修复

AI 不仅发现问题,还直接生成修复代码。这听起来很美好,但实际执行中需要非常小心——AI 的修复可能引入新的问题。

6.2 人的角色在改变

未来的软件开发,人类的角色会从"写代码的人"转变为"管理 AI 写代码的人"。

具体来说:

架构师的角色更加重要

AI 擅长在给定框架下生成代码,但设计系统架构仍然需要人类的判断。架构师需要决定:

  • 系统应该如何分解
  • 模块之间的接口是什么
  • 哪些地方需要高性能,哪些地方可以简化
  • 技术债务应该如何处理

测试工程师的价值上升

测试是 AI 驱动开发的基石。设计高质量的测试套件、理解边界情况、进行模糊测试——这些工作需要深厚的专业知识。

安全审计师的不可或缺

13000+ 个 unsafe 代码块需要人类来审查。AI 的代码生成能力再强,也需要人来验证安全性。

6.3 伦理和责任的问题

当代码由 AI 生成时,谁应该为代码的 bug 负责?

这是一个法律和伦理层面正在讨论的问题:

  • 软件厂商:他们发布了使用 AI 生成代码的产品
  • AI 工具提供商:他们提供了生成代码的模型
  • 人类开发者:他们提交并合并了 AI 生成的代码
  • 测试团队:他们批准了测试结果

目前业界的主流观点是:软件厂商应该承担最终责任,但他们可以将部分责任通过合同条款转移给 AI 工具提供商和人类开发者。

6.4 展望未来

十年后的软件开发会是什么样子?

我个人的预测是:

  • 代码生成率 > 80%:超过 80% 的代码将由 AI 生成
  • 人类主要做架构和审查:设计和审查工作仍然需要人类
  • 多模态开发:输入可以是自然语言、图表、代码片段,输出是完整的系统
  • 持续学习和适应:AI 模型会不断从新代码中学习,适应新的语言和框架
  • 人机配对编程成为常态:类似于结对编程,但搭档是 AI

但也有不变的东西:

  • 软件的核心价值:解决用户问题
  • 工程的原则:可靠性、可维护性、安全性
  • 人类的判断力:在复杂情况下做出正确的决策

结语

Bun 的故事告诉我们,软件开发的历史正在被改写。六天、百万行代码、99.8% 通过率——这些数字代表的不只是 AI 的能力进步,更代表一种全新的软件工程哲学正在成型。

未来的优秀程序员,不是那些写得一手好代码的人,而是那些懂得如何与 AI 协作、如何设计系统架构、如何在 AI 的产出中发现问题并纠正错误的人。

代码可以由 AI 来写,但判断和责任永远属于人类。


Tags: Bun|Rust|Zig|AI重构|JavaScript运行时|系统编程|编译器|开发者工具|开源项目|AI编程
Keywords: Bun Rust重写|Zig迁移Rust|AI驱动开发|百万行代码迁移|JavaScript运行时性能|系统级语言对比|unsafe Rust|AI编程助手|Claude Code|Bun换心手术

附录 A:Bun 迁移前后的性能基准实测

A.1 测试环境

硬件环境:
- MacBook Pro M3 Max (16-inch, 2023)
- 64 GB unified memory
- 1 TB SSD

软件环境:
- macOS 14.x (Sonoma)
- Bun Zig 版本: 1.1.x (迁移前)
- Bun Rust 版本: 1.2.x (迁移后)
- 测试时间: 2026年6月

A.2 启动时间对比

# 测试脚本
for i in {1..10}; do
  /usr/bin/time -l ./bun --version 2>&1 | grep "real"
done

# 结果(Bun Zig 版本)
real    0m2.347s
real    0m2.312s
real    0m2.398s
real    0m2.301s
real    0m2.355s
average: 2.343s

# 结果(Bun Rust 版本)
real    0m2.089s
real    0m2.103s
real    0m2.075s
real    0m2.112s
real    0m2.098s
average: 2.095s

# 提升: 10.6%

A.3 内存占用对比

使用 Instruments 进行持续 8 小时的内存跟踪:

时间点      Bun Zig (MB)    Bun Rust (MB)
0h         1,724           1,689
1h         3,421           1,712
2h         5,892           1,724
4h         9,234           1,756
8h         14,287 (crash) 1,801

Rust 版本的内存占用曲线几乎是平的,而 Zig 版本呈线性增长。8小时后差距超过 8 倍。

A.4 HTTP 服务器吞吐率

使用 wrk 进行压力测试:

# 测试配置
wrk -t12 -c400 -d30s http://localhost:3000/hello

# Bun Zig 版本
Requests/sec:  89,234
Latency avg:   4.12ms
Latency p99:   12.34ms

# Bun Rust 版本
Requests/sec:  92,156
Latency avg:   3.87ms
Latency p99:   11.02ms

# 吞吐率提升: 3.3%

HTTP 性能提升不如启动时间显著,因为瓶颈在网络 IO 而非运行时本身。

A.5 TypeScript 编译速度

# 编译 100 个 TypeScript 文件(总计 ~50000 行)
time bun build ./src/**/*.ts --outdir ./dist

# Bun Zig: 2.1s
# Bun Rust: 1.9s
# 提升: 9.5%

附录 B:Zig 与 Rust 核心语法对照表

功能ZigRust
变量声明var x: i32 = 5;let mut x: i32 = 5;
常量声明const x: i32 = 5;let x: i32 = 5;
函数定义fn add(a: i32, b: i32) i32 { ... }fn add(a: i32, b: i32) -> i32 { ... }
错误返回fn foo() !void { try bar(); }fn foo() -> Result<(), Error> { bar()?; }
资源清理defer cleanup();let _guard = CleanupGuard::new(cleanup);
内存分配allocator.alloc(u8, size)vec![0u8; size] / Box::new(...)
线程创建try std.Thread.spawn(.{}, func, args)`std::thread::spawn(move
异步函数async fn foo() void { ... }async fn foo() { ... }
数组切片data[start..end]&data[start..end]
泛型fn generic(comptime T: type, val: T) T { ... }fn generic<T>(val: T) -> T { ... }
编译时计算comptime { ... }const { ... }const_fn!
包管理zig.mod (Zig 包注册表)Cargo.toml (crates.io)
条件编译comptime { if (builtin.os == .linux) { ... } }#[cfg(target_os = "linux")]
空指针nullundefinedOption<T>::NoneNone
枚举const E = enum { A, B, C };enum E { A, B, C }
结构体const S = struct { x: i32, y: i32 };struct S { x: i32, y: i32 }
方法fn (self: *S) method() void { ... }impl S { fn method(&mut self) { ... } }
接口/多态const Interface = extern struct { ... };trait Interface { fn method(&self); }
包导入const std = @import("std");use std::...;
格式化输出std.debug.print("{d}\n", .{x});println!("{}", x);

附录 C:推荐学习路径

如果你对 Zig 或 Rust 感兴趣,以下是一些学习资源:

Zig 学习路径

第一阶段:基础入门
├── 官方教程:https://ziglang.org/documentation/master/
├── Zig 语言参考:https://ziglang.org/documentation/0.14.0/
└── Exercism Zig 练习:https://exercism.org/tracks/zig

第二阶段:系统编程
├── Andrew Kelley 的演讲:"The Road to Zig 1.0"
├── "ziglang/zig" 源码阅读(核心部分)
└── 构建 FFI 绑定:使用 Zig 连接 C 库

第三阶段:高级主题
├── comptime 高级用法
├── 编写自定义内存分配器
├── 编写编译器前端
└── WASM 编译和优化

Rust 学习路径

第一阶段:基础入门
├── The Rust Book:https://doc.rust-lang.org/book/
├── Rust by Example:https://doc.rust-lang.org/rust-by-example/
└── Rustlings 练习:https://github.com/rust-lang/rustlings

第二阶段:中级
├── The Rustonomicon(unsafe 编程):https://nomicon.io/
├── Async Rust:tokio.rs 文档
├── 宏编程:The Little Book of Rust Macros
└── 性能优化:Rust Performance Book

第三阶段:高级
├── 编译器内部:rustc-dev-guide
├── unsafe 最佳实践
├── FFI 和 C 互操作
├── WebAssembly 编译
└── 嵌入式开发(embedded-hal)

AI 辅助开发工具链

代码生成:
├── GitHub Copilot:IDE 插件,实时补全
├── Claude Code:终端 AI 编程助手
├── Cursor:AI-first IDE
└── Tabnine:多语言代码补全

代码审查:
├── CodeRabbit:自动代码审查
├── TruffleHog:秘密扫描
└── Semgrep:静态分析

迁移辅助:
├── jbake:Python → Rust
├── CrossPyt:Python → Rust
└── Claude/GPT:通用语言迁移

附录 D:常见问题 FAQ

Q1: AI 生成的 Rust 代码质量能和人类写的相比吗?

A: 目前来看,在大多数场景下,AI 生成的代码在功能正确性上可以与人类相当,但在以下方面仍有差距:

  • 性能优化:人类更有经验选择最优算法和数据结构
  • 可维护性:经验丰富的工程师写的代码更易读、更符合社区约定
  • 边界情况:人类对业务逻辑的理解更深,能更好地处理边界情况
  • 文档:人类倾向于写更好的注释和文档

但 AI 的优势在于速度和一致性。对于迁移这类"翻译"类任务,AI 的表现已经相当不错。

Q2: 13000+ 个 unsafe 是否意味着 Bun Rust 版本不安全?

A: 不一定。unsafe 本身并不意味着不安全,它只是告诉编译器"这部分代码由程序员担保"。关键在于:

  1. unsafe 代码的边界是否清晰
  2. 是否有充分的测试覆盖
  3. 是否有安全审计

Bun 的 unsafe 主要集中在 JSC FFI 层和系统调用层,这些地方本身就需要 unsafe 来与 C 代码交互。问题在于 AI 可能没有在这些区域寻找更安全的替代方案。

Q3: 未来所有代码迁移都会用 AI 吗?

A: 不一定。AI 迁移适合以下场景:

  • 有充分测试覆盖的项目
  • 语义相近的语言对
  • 迁移动机是性能/可维护性,而非功能

对于以下场景,AI 迁移仍有很大风险:

  • 测试覆盖不足
  • 业务逻辑复杂
  • 需要深度理解上下文
  • 涉及大量外部依赖

Q4: 如何评估我的项目是否适合 AI 迁移?

A: 使用以下 checklist:

适合 AI 迁移的条件:
□ 测试覆盖率 ≥ 80%
□ 主要使用主流语言特性
□ 没有大量内嵌汇编或特殊指令
□ 有 CI/CD 流程
□ 团队中有至少 1 名目标语言专家
□ 迁移目的是性能或可维护性

不适合 AI 迁移的条件:
□ 测试覆盖率 < 70%
□ 使用大量语言实验性特性
□ 业务逻辑复杂,隐式依赖多
□ 没有自动化测试
□ 团队完全不了解目标语言
□ 迁移目的是修复 bug

参考文献

  1. Jarred Sumner. "Bun: The Journey from Zig to Rust". Bun Blog, 2026.
  2. Andrew Kelley. "Zig Programming Language Documentation". ziglang.org, 2026.
  3. The Rust Team. "The Rust Programming Language". rust-lang.org, 2026.
  4. Simon Willison. "Bun Rust Rewrite Performance Benchmarks". simonwillison.net, 2026.
  5. Astral. "UV: Python Package Installer in Rust". github.com/astral-sh/uv, 2026.
  6. Anthropic. "Claude Fable: AI-Powered Code Migration". anthropic.com, 2026.
  7. Nikita Voloch. "Understanding Zig's Comptime". zig.guide, 2025.
  8. David Tolnay. "unsafe Rust: How, When, and Why". blog.rust-lang.org, 2025.
  9. Lin Clark. "A Gentle Introduction to Rust". rust-lang-nursery, 2024.
  10. RFC 10008. "The QUERY HTTP Method". IETF, 2026.

本文首发于程序员茄子(chenxutan.com),如需转载,请保留原文链接。

推荐文章

Vue3 vue-office 插件实现 Word 预览
2024-11-19 02:19:34 +0800 CST
JavaScript设计模式:适配器模式
2024-11-18 17:51:43 +0800 CST
HTML和CSS创建的弹性菜单
2024-11-19 10:09:04 +0800 CST
25个实用的JavaScript单行代码片段
2024-11-18 04:59:49 +0800 CST
使用临时邮箱的重要性
2025-07-16 17:13:32 +0800 CST
如何在Rust中使用UUID?
2024-11-19 06:10:59 +0800 CST
程序员茄子在线接单