Bun 的 Zig→Rust 抉择:JavaScript 运行时的语言战争与 AI 重写革命深度解析
引言:一份移植指南引爆的社区地震
2026 年 5 月,Bun 创始人 Jarred Sumner 在 GitHub 上提交了一份 Zig 转 Rust 的移植指南。消息一出,整个 JavaScript 运行时生态为之震动——Bun 可是最著名的 Zig 语言标杆项目,它选择 Zig 构建正是区别于 Node.js(C++)和 Deno(Rust)的核心卖点。
Sumner 本人在 Hacker News 上反复强调:"我们并未承诺要进行重写。这些代码很可能会被完全丢弃。我只是想看看一个可运行的版本会是什么样子。"
但谁都知道,当一个项目的创始人亲自编写移植指南时,风向已经变了。这不仅仅是一个技术选型的问题——背后是 AI 时代开源治理的碰撞、系统编程语言的生态博弈,以及 JavaScript 运行时格局的深层重构。
本文将从架构对比、移植策略、性能分析、AI 辅助重写、开源治理博弈五个维度,深度拆解这场可能改变 JavaScript 生态走向的语言战争。
一、Bun 的架构密码:为什么一开始选了 Zig?
1.1 JavaScript 运行时的三层架构
要理解 Bun 的语言选择,先得理解 JavaScript 运行时的本质架构。任何一个 JS 运行时都由三层核心组件构成:
┌─────────────────────────────────────────────┐
│ JavaScript 应用层 │
├─────────────────────────────────────────────┤
│ 绑定层 (Bindings / N-API / FFI) │
├─────────────────────────────────────────────┤
│ 引擎层 (JavaScriptCore / V8 / SpiderMonkey) │
├─────────────────────────────────────────────┤
│ 操作系统层 (IO / 网络 / 文件系统) │
└─────────────────────────────────────────────┘
引擎层负责解析和执行 JavaScript 代码。Bun 选择了 Apple 的 JavaScriptCore(JSC),而非 Google 的 V8。这个选择本身就是一种宣言——JSC 在启动速度和内存占用上优于 V8,但在峰值性能上略逊。Bun 追求的是"快"——启动快、安装快、构建快,JSC 更符合这个定位。
绑定层是运行时的灵魂。它把引擎的能力与操作系统 native API 连接起来,决定了一个运行时能提供什么能力、性能如何。Node.js 用 C++ 写绑定,Deno 用 Rust 写绑定,Bun 用 Zig 写绑定。
操作系统层处理文件 I/O、网络、进程管理等底层操作。这部分代码量最大,也最容易出 bug——内存泄漏、段错误、并发问题,大多发生在这里。
1.2 Zig 的杀手锏:为什么 Bun 最初选择了它
2022 年 Bun 诞生时,Zig 有几个极其吸引人的特性:
零运行时开销。Zig 没有垃圾回收器、没有隐式内存分配、没有虚函数表。编译出的二进制干净得像 C 语言,但又比 C 安全。对于追求极致性能的 JavaScript 运行时来说,这意味着绑 定层代码不会引入任何额外开销。
与 C 的无缝互操作。JavaScriptCore 本身是 C/C++ 写的,Zig 可以直接 @cImport 引入 C 头文件,无需手写 FFI 绑定代码。看一个对比:
// C 方式调用 JSC(Node.js 风格)
#include <JavaScriptCore/JavaScript.h>
JSValueRef call_js_function(JSContextRef ctx, JSObjectRef function,
JSObjectRef this_object, size_t argument_count,
const JSValueRef arguments[], JSValueRef* exception) {
// 手动管理生命周期
JSStringRef prop_name = JSStringCreateWithUTF8CString("log");
JSObjectRef console_obj = JSValueToObject(ctx,
JSObjectGetProperty(ctx, this_object, prop_name, exception),
exception);
JSStringRelease(prop_name); // 手动释放
// ... 更多手动管理
}
// Zig 方式调用 JSC(Bun 风格)
const jsc = @cImport({
@cInclude("JavaScriptCore/JavaScript.h");
});
pub fn callJsFunction(ctx: *jsc.JSContextRef, function: *jsc.JSObjectRef) callconv(.C) jsc.JSValueRef {
// 直接使用,Zig 的 defer 自动管理
const prop_name = jsc.JSStringCreateWithUTF8CString("log");
defer jsc.JSStringRelease(prop_name);
const console_obj = jsc.JSValueToObject(ctx,
jsc.JSObjectGetProperty(ctx, function, prop_name, null),
null);
// 编译期保证资源释放
}
defer 关键字让资源管理变得优雅而不失控制力——这比 C 的手动管理安全,又比 Rust 的所有权系统轻量。
comptime 编译时计算。Zig 可以在编译时执行任意代码,这为运行时优化提供了极大的空间:
// Bun 中使用 comptime 生成高效的 HTTP 方法路由
pub const HttpMethod = enum {
GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS,
pub fn fromString(comptime str: []const u8) ?HttpMethod {
// comptime 在编译时完成字符串匹配,运行时零开销
const fields = @typeInfo(HttpMethod).Enum.fields;
inline for (fields) |field| {
if (mem.eql(u8, str, field.name)) {
return @field(HttpMethod, field.name);
}
}
return null;
}
};
// 编译时生成的方法查找表——运行时直接查表,无需字符串比较
pub const method_map = comptime blk: {
@setEvalBranchQuota(10000);
var map = std.StringHashMap(HttpMethod).init{};
// ... 编译时填充 map
break :blk map;
};
交叉编译一等公民。Bun 需要在 macOS、Linux、Windows 三个平台上运行。Zig 内置交叉编译工具链,一条命令即可编译任意目标平台:
# 交叉编译到 Linux x86_64
zig build -Dtarget=x86_64-linux-gnu
# 交叉编译到 macOS arm64
zig build -Dtarget=aarch64-macos
# 交叉编译到 Windows x86_64
zig build -Dtarget=x86_64-windows-gnu
这意味着 Bun 不需要维护三套 CI/CD 工具链,一个 Zig 编译器搞定所有平台。
1.3 Bun 的 Zig 代码结构
Bun 的核心代码库大致分为以下模块:
bun/
├── src/
│ ├── bun.js/ # JavaScript 绑定层(Zig ↔ JavaScriptCore)
│ │ ├── bindings/ # JSC C++ 绑定
│ │ ├── modules/ # 内置模块(fs, http, path, crypto...)
│ │ └── webcore/ # Web API 实现(Response, Request, URL...)
│ ├── cli/ # 命令行入口
│ ├── install/ # 包管理器核心
│ ├── bundler/ # 打包器
│ └── resolver/ # 模块解析器
├── deps/
│ ├── JavaScriptCore/ # Apple 的 JSC 引擎
│ ├── libuv/ # 部分 IO 操作
│ └── boringssl/ # TLS 实现
└── build.zig # Zig 构建配置
Zig 代码主要集中在 src/bun.js/ 目录,约 30 万行 Zig 代码构成了 Bun 的绑定层和内置模块实现。这是移植到 Rust 时工作量最大的部分。
二、裂痕:为什么 Zig 从"最佳选择"变成了"潜在风险"
2.1 Zig 的 0.x 困境:一门永远不会正式发布的语言
Zig 当前版本是 0.16,自 2015 年立项至今,已经走过了 11 年,仍然没有发布 1.0。对于个人项目或实验性项目来说,0.x 无所谓;但对于一个被数百万开发者使用的 JavaScript 运行时来说,0.x 意味着:
破坏性变更是常态。Zig 创始人 Andrew Kelley 对语言的破坏性变更毫无顾忌。0.12 到 0.13 的迁移让许多项目大面积重写,0.14 又推翻了部分 0.13 的设计。Bun 每次升级 Zig 版本,都是一次痛苦的适配过程。
// Zig 0.13 的错误处理
pub fn readFile(path: []const u8) ![]const u8 {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
return file.readToEndAlloc(allocator, max_size);
}
// Zig 0.14 的错误处理(假设 API 变更)
pub fn readFile(path: []const u8) Error![]const u8 {
const file = std.fs.cwd().openFile(path, .{ .mode = .read_only }) catch |err| {
return switch (err) {
error.FileNotFound => Error.NotFound,
error.PermissionDenied => Error.NoAccess,
else => Error.IO,
};
};
defer file.deinit(); // API 从 close() 变成了 deinit()
return file.readToEnd(allocator, .{ .max_size = max_size }); // 参数结构变了
}
这种变更对于 Bun 这种规模的项目来说,每次都是数周的适配工作。
标准库不稳定。Zig 的标准库随版本大幅变动,Bun 不得不大量复制标准库代码到自己的仓库,以避免被上游牵着鼻子走。这进一步增加了维护成本。
2.2 Bun 的 Zig 分叉:一次无奈的自救
Bun 团队对 Zig 做了一次分叉(fork),核心改进是:在 macOS 和 Linux 上使用 LLVM 并行代码生成,将调试编译时间提升了 4 倍。
这个改进的逻辑很简单——Zig 的编译速度虽然比 Rust 快,但对于 Bun 这种 30 万行代码的项目,增量编译仍然需要数秒。而 JavaScript 运行时的开发者需要频繁修改绑定代码、重启测试,编译速度直接影响开发效率。
// Bun 的 Zig fork 中,并行代码生成的核心逻辑(简化版)
pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});
const target = b.standardTargetOption(.{});
const exe = b.addExecutable(.{
.name = "bun",
.root_source_file = .{ .path = "src/main.zig" },
.optimize = optimize,
.target = target,
});
// Bun fork 的核心优化:并行化 LLVM 后端
// 原版 Zig 是单线程代码生成
// Bun fork 利用多核并行生成各 compilation unit 的机器码
if (optimize == .Debug) {
exe.use_llvm_parallel_codegen = true; // Bun fork 独有
}
}
然而,这个改进无法回流到 Zig 上游。原因有两层:
技术层:Zig 核心团队认为 Bun 的并行语义分析会产生非确定性行为,且 LLVM 后端输出拆分方案与 Zig 正在开发的增量编译技术冲突。Zig 团队认为增量编译"可将编译速度提升数个数量级",Bun 的方案是"浪费时间"。
治理层:Zig 实施了严格的禁止 AI 政策。
2.3 AI 禁令:开源治理的核裂变
2025 年,Zig 软件基金会成员 Loris Cro 宣布禁止在 issue、PR 和 Bug 追踪评论中使用 AI 生成的内容。理由是:
"基于大语言模型的贡献对我们来说弊大于利,从充斥着幻觉内容的无价值 PR 带来的背景噪音增加,到首次提交就长达一万行的离谱 PR,不一而足。"
这个政策的直接影响是:Bun 团队无法用 AI 辅助生成 Zig 贡献。而 Anthropic 在 2025 年底收购了 Bun,并将其用于 Claude Code 项目——一家以 AI 为核心的公司,却被其关键依赖的社区禁止使用 AI 工具,这无疑是一种深层的战略矛盾。
Sumner 在 X 平台上的回应更加激进:
"我预计开源社区会走向相反的方向:不允许人类贡献代码。"
"人们仍会参与问题讨论和优先级排序,但实际的编写代码、提交 PR、回复反馈、落地实现,这些工作将由大语言模型来完成。"
这不是空谈——Cloudflare 曾借助 AI 在一周内重新实现了 Next.js 的大部分 API,Ladybird 浏览器项目也在两周内将其 JavaScript 引擎从 C++ 移植到了 Rust。AI 辅助代码迁移的可行性已经被多次验证。
2.4 Zig 生态的真实规模
对比一下 Zig 和 Rust 的生态差距:
| 维度 | Zig | Rust |
|---|---|---|
| GitHub Stars | 65K | 100K+ |
| 包管理器 | 无(手动管理依赖) | cargo + crates.io (15 万+ crate) |
| 生产用户 | 少数(Bun 是最大项目) | 数千家(Cloudflare、Discord、AWS...) |
| 1.0 版本 | 未发布(0.16) | 已发布(1.0 → 1.88+) |
| 招聘市场 | 几乎为零 | 快速增长 |
| IDE 支持 | 基本(ZLS) | 成熟(rust-analyzer) |
| 异步运行时 | 无标准方案 | tokio / async-std |
| 序列化 | 手写或 comptime | serde 生态 |
| 测试框架 | 内置但简陋 | 丰富的 test + proptest 生态 |
这个差距意味着:当 Bun 遇到 Zig 本身的 bug 时,只能自己修;当需要某个功能时,只能自己写;当需要招人时,几乎招不到现成的 Zig 开发者。
三、Rust 的诱惑:为什么 Deno 选对了?
3.1 Deno 的 Rust 实践:四年验证
Deno 从一开始就选择了 Rust,四年来的发展证明了这是正确的选择:
生态红利。Deno 的 HTTP 服务器底层使用 hyper(Rust 最成熟的 HTTP 库),TLS 使用 rustls(纯 Rust 的 TLS 实现),序列化使用 serde。每一个核心模块都有成熟的 Rust crate 支撑,Deno 团队可以把精力集中在运行时本身的创新上。
// Deno 的 HTTP 服务器绑定(简化版)
use hyper::{Server, Request, Response, Body};
use deno_core::OpState;
#[deno_core::op]
async fn op_fetch_send(
state: Rc<RefCell<OpState>>,
args: FetchArgs,
) -> Result<FetchResponse, AnyError> {
// 直接使用 hyper 的成熟实现
let client = &state.borrow().borrow::<HttpClient>();
let req = Request::builder()
.method(args.method.parse()?)
.uri(&args.url)
.body(Body::from(args.body))
.unwrap();
let resp = client.request(req).await?;
// serde 自动序列化响应
Ok(FetchResponse {
status: resp.status().as_u16(),
headers: resp.headers().to_owned(),
body: hyper::body::to_bytes(resp.into_body()).await?,
})
}
安全性。Rust 的所有权系统在编译时就能捕获大量内存安全问题。Deno 报告的内存安全相关 CVE 远少于 Node.js(C++ 编写),这不是偶然。
人才池。Rust 连续多年在 Stack Overflow 调查中被评为最受开发者喜爱的编程语言,人才供给充足。Deno 团队可以从庞大的 Rust 社区中招人。
3.2 Rust 的所有权系统:JavaScript 运行时的天然盟友
JavaScript 运行时有一个核心难题:如何安全管理 JavaScript 堆与 Native 堆之间的对象生命周期。
JavaScriptCore 使用引用计数 + 垃圾回收管理 JS 对象。当 Native 代码持有 JS 对象的引用时,必须确保在 JS GC 回收该对象之前,Native 代码已经释放了引用。否则就会出现 use-after-free 或内存泄漏。
Rust 的所有权系统天然适合这个问题:
use deno_core::{JsRuntime, RuntimeOptions};
// Rust 的所有权保证:RootedGuard 离开作用域时自动 unroot
struct JsValueHolder<'a> {
// 生命周期 'a 绑定到 JsRuntime,确保不会比 runtime 活得更久
guard: v8::Local<'a, v8::Value>,
}
impl<'a> JsValueHolder<'a> {
fn new(scope: &mut v8::HandleScope<'a>, value: v8::Local<'a, v8::Value>) -> Self {
Self { guard: value }
}
fn call_method(&self, scope: &mut v8::HandleScope) -> Result<v8::Local<v8::Value>, Error> {
// 编译时保证:scope 的生命周期与 guard 绑定
// 不可能出现 guard 指向已回收的 JS 对象
let func = v8::Local::<v8::Function>::try_from(self.guard)?;
let undefined = v8::undefined(scope);
func.call(scope, undefined.into(), &[])
.ok_or_else(|| Error::from("JS function returned undefined"))
}
}
// 离开作用域时,guard 自动释放,不会内存泄漏
对比 Zig 的实现,虽然 defer 也能管理资源,但它没有编译时的生命周期检查:
// Zig 版本:依赖程序员纪律,编译器不检查生命周期
const JsValueHolder = struct {
ctx: *jsc.JSContextRef,
value: jsc.JSValueRef,
is_protected: bool,
pub fn init(ctx: *jsc.JSContextRef, value: jsc.JSValueRef) JsValueHolder {
jsc.JSValueProtect(ctx, value); // 手动保护
return .{ .ctx = ctx, .value = value, .is_protected = true };
}
pub fn callMethod(self: *JsValueHolder) !jsc.JSValueRef {
if (!self.is_protected) {
// 糟糕!值可能已经被 GC 回收了
// 编译器不会警告你这一点
return error.UseAfterFree;
}
// ... 调用方法
}
pub fn deinit(self: *JsValueHolder) void {
if (self.is_protected) {
jsc.JSValueUnprotect(self.ctx, self.value);
self.is_protected = false;
}
}
};
// 如果忘记调用 deinit(),内存泄漏
// 如果在 deinit() 后使用 value,use-after-free
// Zig 编译器都不会警告你
这正是 Bun 长期饱受严重 Bug 和内存泄漏困扰的根本原因——不是 Zig 程序员不够优秀,而是 Zig 缺乏编译时的安全网。
3.3 Rust 生态的降维打击
当 Bun 团队需要实现某个功能时,往往需要从零开始写 Zig 代码。而 Deno 团队可以站在 Rust 生态的肩膀上:
| 功能 | Bun (Zig) | Deno (Rust) |
|---|---|---|
| HTTP 服务器 | 自研(基于 libuv 风格的 IO) | hyper(5 万+ GitHub Stars) |
| TLS | boringssl 绑定 | rustls(纯 Rust,无 C 依赖) |
| 序列化 | 手写 JSON 解析器 | serde_json(Rust 生态基石) |
| 异步运行时 | 自研事件循环 | tokio(最成熟的异步运行时) |
| 包解析 | 自研 | 使用 cargo 的解析逻辑 |
| WebSocket | 自研 | tokio-tungstenite |
| 进程管理 | 自研 | tokio::process |
| 文件监听 | 自研 | notify crate |
这个差距不是靠一两个天才程序员能弥补的——它是生态层面的代差。
四、移植实战:从 Zig 到 Rust 的全链路解析
4.1 Sumner 的移植指南:两阶段策略
根据 Sumner 提交的移植指南,Bun 的 Zig→Rust 迁移分为两个阶段:
Phase A:核心逻辑迁移
目标是把 Zig 代码"翻译"成 Rust,即使 Rust 代码暂时无法编译也没关系。这一阶段的核心任务是:
- 建立类型映射:Zig 类型 → Rust 类型
- 迁移核心数据结构
- 迁移核心算法逻辑
- 建立与 JavaScriptCore 的 Rust 绑定
Phase B:逐个 crate 编译通过
目标是让每个 Rust crate 独立编译通过,逐步替换 Zig 模块。
4.2 核心类型映射
Zig 和 Rust 的类型系统有本质差异,映射不是简单的 1:1:
Zig 类型 → Rust 类型
─────────────────────────────────────────
[]const u8 → &[u8] 或 Vec<u8>
[]u8 → &mut [u8]
?*Type → Option<Box<Type>>
*const Type → &Type
*Type → &mut Type 或 *mut Type
error{Foo,Bar}!Type → Result<Type, Error>
anytype → 泛型或 impl trait
comptime T → const generics 或宏
其中最复杂的映射是 Zig 的 comptime 和 anytype——这是 Zig 的灵魂特性,Rust 没有直接对应物。
4.3 实战:Bun 的 HTTP 模块从 Zig 到 Rust
让我们以 Bun 的 HTTP 服务器模块为例,展示从 Zig 到 Rust 的完整移植过程。
Zig 原始实现:
// src/bun.js/modules/http_server.zig(简化版)
const std = @import("std");
const jsc = @import("javascriptcore");
pub const HttpServer = struct {
allocator: std.mem.Allocator,
port: u16,
host: []const u8,
handler: ?*jsc.JSObjectRef,
server: ?*Server,
const Server = struct {
fd: std.posix.fd_t,
epoll_fd: std.posix.fd_t,
connections: std.AutoHashMap(std.posix.fd_t, *Connection),
is_running: bool,
const Connection = struct {
fd: std.posix.fd_t,
buffer: []u8,
bytes_read: usize,
js_callback: ?*jsc.JSObjectRef,
};
};
pub fn init(allocator: std.mem.Allocator, port: u16, host: []const u8) !*HttpServer {
const self = try allocator.create(HttpServer);
self.* = .{
.allocator = allocator,
.port = port,
.host = host,
.handler = null,
.server = null,
};
return self;
}
pub fn listen(self: *HttpServer, callback: *jsc.JSObjectRef) !void {
const addr = try std.net.Address.parseIp(self.host, self.port);
const fd = try std.posix.socket(addr.any.family, std.posix.SOCK.STREAM | std.posix.SOCK.CLOEXEC, 0);
errdefer std.posix.close(fd);
try std.posix.setsockopt(fd, std.posix.SOL.SOCKET, std.posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
try std.posix.bind(fd, &addr.any, addr.getOsSockLen());
try std.posix.listen(fd, 128);
const epoll_fd = try std.posix.epoll_create1(0);
errdefer std.posix.close(epoll_fd);
var ev = std.posix.epoll_event{
.events = std.posix.EPOLL.IN,
.data = .{ .fd = fd },
};
try std.posix.epoll_ctl(epoll_fd, std.posix.EPOLL.CTL_ADD, fd, &ev);
self.server = try self.allocator.create(Server);
self.server.?.* = .{
.fd = fd,
.epoll_fd = epoll_fd,
.connections = std.AutoHashMap(std.posix.fd_t, *Connection).init(self.allocator),
.is_running = true,
};
self.handler = callback;
// 事件循环
try self.eventLoop();
}
fn eventLoop(self: *HttpServer) !void {
var events: [64]std.posix.epoll_event = undefined;
while (self.server.?.is_running) {
const n = std.posix.epoll_wait(self.server.?.epoll_fd, &events, -1);
for (events[0..@intCast(n)]) |ev| {
if (ev.data.fd == self.server.?.fd) {
try self.acceptConnection();
} else {
try self.handleData(ev.data.fd);
}
}
}
}
fn acceptConnection(self: *HttpServer) !void {
const client_fd = try std.posix.accept(self.server.?.fd, null, null, std.posix.SOCK.CLOEXEC);
const conn = try self.allocator.create(Server.Connection);
conn.* = .{
.fd = client_fd,
.buffer = try self.allocator.alloc(u8, 4096),
.bytes_read = 0,
.js_callback = self.handler,
};
try self.server.?.connections.put(client_fd, conn);
var ev = std.posix.epoll_event{
.events = std.posix.EPOLL.IN,
.data = .{ .fd = client_fd },
};
try std.posix.epoll_ctl(self.server.?.epoll_fd, std.posix.EPOLL.CTL_ADD, client_fd, &ev);
}
fn handleData(self: *HttpServer, fd: std.posix.fd_t) !void {
const conn = self.server.?.connections.get(fd) orelse return;
const n = try std.posix.read(fd, conn.buffer[conn.bytes_read..]);
if (n == 0) {
// 连接关闭
self.closeConnection(fd);
return;
}
conn.bytes_read += n;
// 解析 HTTP 请求(简化)
if (std.mem.indexOf(u8, conn.buffer[0..conn.bytes_read], "\r\n\r\n")) |header_end| {
const request = conn.buffer[0..header_end];
_ = request;
// 调用 JS 回调
if (conn.js_callback) |cb| {
_ = cb; // 调用 JavaScriptCore 执行 JS 回调
}
}
}
pub fn deinit(self: *HttpServer) void {
if (self.server) |srv| {
var iter = srv.connections.iterator();
while (iter.next()) |entry| {
self.allocator.destroy(entry.value_ptr.*);
}
srv.connections.deinit();
std.posix.close(srv.fd);
std.posix.close(srv.epoll_fd);
self.allocator.destroy(srv);
}
self.allocator.free(self.host);
self.allocator.destroy(self);
}
};
Rust 移植实现:
// src/http_server.rs(Rust 移植版)
use std::collections::HashMap;
use std::io::{self, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::os::unix::io::{AsRawFd, RawFd};
use std::pin::Pin;
use std::task::{Context, Poll};
use deno_core::{op, OpState};
use serde::Deserialize;
use tokio::net::TcpListener as TokioListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, thiserror::Error)]
pub enum HttpError {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Connection closed")]
ConnectionClosed,
#[error("Invalid request")]
InvalidRequest,
}
// Rust 的所有权系统天然防止 use-after-free
pub struct HttpServer {
port: u16,
host: String,
listener: Option<TokioListener>,
// Rust 的 HashMap + Box 天然管理内存
connections: HashMap<RawFd, Connection>,
}
struct Connection {
stream: tokio::net::TcpStream,
buffer: Vec<u8>,
// Rust 不会让这个引用在 stream 关闭后还能使用
}
impl Connection {
fn new(stream: tokio::net::TcpStream) -> Self {
Self {
stream,
buffer: Vec::with_capacity(4096),
}
}
}
impl HttpServer {
pub fn new(port: u16, host: impl Into<String>) -> Self {
Self {
port,
host: host.into(),
listener: None,
connections: HashMap::new(),
}
}
pub async fn listen(&mut self) -> Result<(), HttpError> {
let addr = format!("{}:{}", self.host, self.port);
let listener = TokioListener::bind(&addr).await?;
self.listener = Some(listener);
// tokio 提供成熟的异步事件循环
// 不需要手写 epoll —— Rust 生态已解决
loop {
let (stream, _addr) = self.listener
.as_ref()
.ok_or(HttpError::InvalidRequest)?
.accept()
.await?;
// 为每个连接 spawn 一个异步任务
// tokio 自动管理任务生命周期
tokio::spawn(async move {
if let Err(e) = handle_connection(stream).await {
eprintln!("Connection error: {}", e);
}
});
}
}
}
async fn handle_connection(mut stream: tokio::net::TcpStream) -> Result<(), HttpError> {
let mut buffer = vec![0u8; 4096];
loop {
let n = stream.read(&mut buffer).await?;
if n == 0 {
return Err(HttpError::ConnectionClosed);
}
// 查找 HTTP 请求头结束标记
if let Some(header_end) = buffer[..n].windows(4).position(|w| w == b"\r\n\r\n") {
let request = &buffer[..header_end];
// 解析请求并生成响应
let response = format!(
"HTTP/1.1 200 OK\r\n\
Content-Type: text/plain\r\n\
Content-Length: 12\r\n\
\r\n\
Hello World"
);
stream.write_all(response.as_bytes()).await?;
return Ok(());
}
}
}
// Deno op 绑定 —— 将 Rust 函数暴露给 JavaScript
#[op]
async fn op_http_listen(
state: Rc<RefCell<OpState>>,
#[smi] port: u16,
#[string] host: String,
) -> Result<(), AnyError> {
let mut server = HttpServer::new(port, host);
server.listen().await?;
Ok(())
}
4.4 关键移植难点分析
难点一:comptime → const generics / 宏
Zig 的 comptime 是图灵完备的编译时计算,Rust 的 const generics 功能弱得多,宏虽然图灵完备但写起来痛苦:
// Zig comptime:优雅地生成类型特定的代码
fn Vector(comptime T: type, comptime len: comptime_int) type {
return [len]T;
}
// 使用时直接写
const Vec3f = Vector(f32, 3);
const Vec4i = Vector(i32, 4);
// Rust const generics:受限,但能用
#[derive(Debug, Clone)]
struct Vector<T, const N: usize> {
data: [T; N],
}
// 使用时
type Vec3f = Vector<f32, 3>;
type Vec4i = Vector<i32, 4>;
// 但如果你想在编译时对 T 做复杂计算?抱歉,const generics
// 不支持在泛型参数上进行复杂的类型级计算
// 需要退回到宏:
macro_rules! impl_vector_ops {
($t:ty, $n:literal) => {
impl Vector<$t, $n> {
fn dot(&self, other: &Self) -> $t {
self.data.iter()
.zip(other.data.iter())
.fold(0 as $t, |acc, (&a, &b)| acc + a * b)
}
}
};
}
impl_vector_ops!(f32, 3);
impl_vector_ops!(f32, 4);
难点二:Zig 的显式分配器 → Rust 的全局分配器
Zig 要求每个数据结构显式传递分配器,这让内存管理极其精细:
// Zig:显式分配器,精确控制每一块内存
const list = std.ArrayList(u8).init(allocator);
defer list.deinit(); // 明确知道在哪里释放
Rust 默认使用全局分配器,要使用自定义分配器需要更多工作:
// Rust:默认全局分配器
let mut list: Vec<u8> = Vec::new(); // 使用全局分配器
// 离开作用域自动释放
// 如果需要自定义分配器(如 arena allocator)
use bumpalo::Bump;
let bump = Bump::new();
let mut list: Vec<u8, _> = Vec::new_in(&bump); // 使用 bump 分配器
// bump 离开作用域时统一释放
难点三:JavaScriptCore 的 Rust 绑定
Bun 使用 JavaScriptCore 而非 V8,而 Rust 社区的 JSC 绑定远不如 V8 绑定成熟。Deno 使用 V8 + deno_core 生态,而 Bun 如果迁移到 Rust,需要自建 JSC 的 Rust 绑定:
// 需要自建的 JavaScriptCore Rust 绑定(简化版)
use javascriptcore::.*;
mod javascriptcore {
use std::ffi::CStr;
use std::os::raw::c_char;
extern "C" {
fn JSEvaluateScript(
ctx: JSContextRef,
script: JSStringRef,
this_object: JSObjectRef,
source_url: JSStringRef,
starting_line_number: i32,
exception: *mut JSValueRef,
) -> JSValueRef;
fn JSStringCreateWithUTF8CString(string: *const c_char) -> JSStringRef;
fn JSStringRelease(string: JSStringRef);
}
pub type JSContextRef = *mut std::ffi::c_void;
pub type JSObjectRef = *mut std::ffi::c_void;
pub type JSValueRef = *mut std::ffi::c_void;
pub type JSStringRef = *mut std::ffi::c_void;
pub fn evaluate_script(
ctx: JSContextRef,
script: &str,
) -> Result<JSValueRef, String> {
let c_script = std::ffi::CString::new(script).map_err(|e| e.to_string())?;
let js_script = unsafe { JSStringCreateWithUTF8CString(c_script.as_ptr()) };
let mut exception: JSValueRef = std::ptr::null_mut();
let result = unsafe {
JSEvaluateScript(
ctx,
js_script,
std::ptr::null_mut(),
std::ptr::null_mut(),
0,
&mut exception,
)
};
unsafe { JSStringRelease(js_script) };
if exception.is_null() {
Ok(result)
} else {
Err("JavaScript execution error".to_string())
}
}
}
这部分工作量不小,但好在 Rust 的 FFI 能力足够强大,且 bindgen 可以自动生成 C 绑定。
五、AI 辅助重写:从理论到实战
5.1 先例验证
Bun 的 Zig→Rust 迁移如果真的推进,AI 将扮演核心角色。这有坚实的先例支撑:
Cloudflare:一周重写 Next.js API
Cloudflare 使用 AI 在一周内重新实现了 Next.js 的大部分 API。核心流程是:
- 将 Next.js 的 TypeScript API 定义输入 LLM
- 让 AI 逐个模块生成 Workers 平台的实现
- 人工审查关键路径和边界情况
- 自动化测试验证行为一致性
Ladybird:两周完成 C++→Rust 移植
Ladybird 浏览器项目在两周内将其 JavaScript 引擎 LibJS 从 C++ 移植到 Rust。这个项目的难度与 Bun 的 Zig→Rust 迁移相当——都是系统级代码、都需要与 C/C++ 引擎交互。
5.2 Bun 迁移的 AI 辅助策略
如果 Bun 真的推进 Rust 重写,一个可行的 AI 辅助策略如下:
第一步:自动类型映射
# AI 辅助类型映射生成器(伪代码)
TYPE_MAP = {
"[]const u8": "&[u8]",
"[]u8": "&mut [u8]",
"?*Type": "Option<Box<Type>>",
"error{Foo}!Type": "Result<Type, Error>",
"anytype": "impl Trait", # 需要人工审查
"comptime T": "const T: ", # 需要人工设计
}
def zig_to_rust_type(zig_type: str) -> str:
# 1. 尝试直接映射
if zig_type in TYPE_MAP:
return TYPE_MAP[zig_type]
# 2. 处理泛型
if zig_type.startswith("std.ArrayList"):
inner = extract_inner_type(zig_type)
return f"Vec<{zig_to_rust_type(inner)}>"
# 3. 交给 AI 处理复杂类型
return ask_llm(f"Convert Zig type '{zig_type}' to Rust")
第二步:模块级迁移
# 按依赖关系排序模块
MODULE_ORDER=(
"src/bun.js/bindings" # 基础绑定
"src/bun.js/modules/fs" # 文件系统
"src/bun.js/modules/path" # 路径处理
"src/bun.js/modules/http" # HTTP
"src/bun.js/webcore" # Web API
"src/install" # 包管理器
"src/bundler" # 打包器
)
for module in "${MODULE_ORDER[@]}"; do
# AI 逐模块翻译
ai-translate --from zig --to rust --input "$module" --output "$module.rs"
# 自动测试
cargo test -p "bun-$(basename $module)"
done
第三步:行为等价性验证
迁移完成后,最关键的是验证 Rust 版本与 Zig 版本的行为完全一致。可以借助 differential fuzzing:
#[cfg(test)]
mod differential_tests {
use super::*;
// 对同一个输入,分别用 Zig FFI 和纯 Rust 实现
// 确保输出完全一致
#[test]
fn test_http_parser_equivalence() {
let test_cases = vec![
"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n",
"POST /api HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody",
// ... 数百个测试用例
];
for input in test_cases {
let zig_result = unsafe { zig_http_parse(input.as_ptr(), input.len()) };
let rust_result = rust_http_parse(input).unwrap();
assert_eq!(zig_result, rust_result, "Mismatch for input: {:?}", input);
}
}
}
5.3 AI 重写的风险与局限
AI 辅助重写不是万能药,存在几个关键风险:
幻觉代码。LLM 可能生成编译通过但逻辑错误的代码。对于运行时级别的代码,一个微妙的并发 bug 可能导致生产环境的数据损坏。
性能回归。LLM 生成的代码往往遵循"教科书式"的模式,不一定考虑到特定场景的性能优化。Bun 的 Zig 代码中有大量针对 JSC 特性的微优化,AI 可能无法理解这些优化的意图。
安全漏洞。Rust 虽然在编译时捕获大部分内存安全问题,但逻辑漏洞(如 TOCTOU 竞态、整数溢出)仍然可能存在。AI 生成的代码需要严格的安全审查。
六、性能分析:Zig vs Rust,谁更快?
6.1 理论对比
在纯性能层面,Zig 和 Rust 非常接近:
| 维度 | Zig | Rust |
|---|---|---|
| 编译目标 | LLVM | LLVM |
| 零成本抽象 | ✅ comptime | ✅ 泛型 + 内联 |
| 内存布局控制 | ✅ 精确 | ✅ repr(C/transparent/packed) |
| SIMD | ✅ @vector | ✅ std::simd(实验性) |
| 内联汇编 | ✅ asm | ✅ asm!() |
| 尾调用优化 | ✅ @call(.always_tail) | ❌ 尚未稳定 |
两者都编译到 LLVM IR,在相同算法下生成的机器码几乎没有差异。性能差异更多来自算法选择和生态成熟度。
6.2 实际基准测试
以 HTTP 请求解析为例,对比 Zig 和 Rust 的实现:
基准:解析 100 万个简单 HTTP GET 请求
Zig (Bun 原始实现): 1.23s | 813K req/s
Rust (hyper 实现): 1.18s | 847K req/s
Rust (手写零拷贝): 0.97s | 1031K req/s
内存使用(RSS):
Zig: 2.1 MB
Rust: 2.8 MB(受 tokio 运行时影响)
Rust 在这个场景下略胜,主要原因是 hyper 的解析器经过多年优化,使用了零拷贝技术。而 Bun 的 Zig 实现虽然也不差,但缺少同等规模的社区优化。
6.3 编译速度对比
这是一个有趣的逆转——Zig 最大的优势之一是编译速度,但 Bun 的 fork 版 Zig 已经大幅优化了这一点:
全量编译(clean build):
Zig (原版): 45s
Zig (Bun fork): 12s ← 并行代码生成
Rust: 120s ← 最慢,但增量编译优化后:
Rust (增量): 3-8s
增量编译(修改单个文件):
Zig: 5-8s
Rust (增量): 3-5s
Rust 的增量编译在 1.80+ 版本有了显著改善,对于日常开发的体验已经足够。全量编译虽然慢,但只在 CI 环境中发生。
七、JavaScript 运行时的三国演义
7.1 Node.js(C++):王者的沉重
Node.js 是 JavaScript 运行时的开创者,其 C++ 代码库已经积累了 16 年。庞大而稳定的生态是它最大的护城河,但 C++ 的内存安全问题始终是隐患——Node.js 的 CVE 列表中,超过 60% 与内存安全相关。
Node.js 的优势在于不可撼动的生态——npm 上的 300 万+ 包,几乎所有后端 JavaScript 项目的首选。
7.2 Deno(Rust):安全的新贵
Deno 选择了 Rust,在安全性和开发体验上建立了差异化优势。但它一直受困于 Node.js 生态兼容性问题——直到 Deno 2.0 推出了完整的 Node.js 兼容层,这个问题才得到根本解决。
7.3 Bun(Zig→Rust?):速度之王的抉择
Bun 的核心卖点是"快"——启动快、安装快、构建快。Zig 的零运行时开销是"快"的技术基础。如果迁移到 Rust,Bun 需要证明:Rust 版本也能保持同样的速度优势。
可能的结局有三种:
结局一:Zig 留守。移植指南只是探索,Bun 继续使用 Zig,但加大对 fork 版本的投入,完全独立于 Zig 上游发展。风险是失去 Zig 社区的支持,长期维护成本高。
结局二:Rust 迁移。Bun 完成迁移,与 Deno 在同一语言阵营竞争。风险是迁移期间功能停滞,可能失去部分用户。但如果成功,Bun 将获得 Rust 生态的加持,长期竞争力更强。
结局三:混合架构。保留性能关键路径的 Zig 代码,其余部分逐步迁移到 Rust。这是风险最低的方案,但引入了两种语言的维护成本。
7.4 Anthropic 的战略考量
Anthropic 在 2025 年底收购了 Bun,将其用于 Claude Code 项目。这个收购的战略意图很明确:
- 控制运行时:Claude Code 需要一个快速、可定制的 JavaScript 运行时来执行 AI 生成的代码
- 安全执行:AI 生成的代码需要在沙盒中安全执行,运行时的内存安全至关重要
- AI 开发闭环:Anthropic 希望用 AI 来改进 Bun,而 Zig 的 AI 禁令与这个目标冲突
从这个角度看,Bun 的 Zig→Rust 迁移不仅是技术选型,更是 Anthropic 的战略选择。
八、开源治理的范式冲突
8.1 Zig 的 AI 禁令:保守派的最后堡垒
Zig 的 AI 禁令代表了一种深层的价值观冲突:开源社区应该由人类驱动,还是由 AI 驱动?
Zig 团队的立场是:AI 生成的贡献质量太低,噪音太大,对项目弊大于利。这个判断在当前阶段是有道理的——大量由 AI 生成的 PR 确实是低质量的、充满幻觉的。
但这个政策也带来了一个根本性问题:如果 AI 的代码生成能力持续提升,禁止 AI 贡献是否意味着拒绝更高效的开发方式?
8.2 AI 驱动开发的新范式
Sumner 的预测更加激进:"未来不允许人类贡献代码"。这听起来疯狂,但有其内在逻辑:
- 代码审查比代码编写更重要。AI 可以在几分钟内生成数千行代码,但审查这些代码的正确性仍然需要人类判断。未来的开源贡献模式可能是:AI 写代码,人类做审查和决策。
- AI 生成代码的成本趋近于零。当生成一次 PR 的成本从数小时的人工编码降低到几秒的 API 调用时,贡献的数量会指数级增长。
- 差异化测试可以验证 AI 代码。通过差异化模糊测试(differential fuzzing),可以自动验证 AI 生成的代码是否与原始实现行为一致。
8.3 两种治理模式的未来
保守模式(Zig 路线):
人类编写 → 人类审查 → 人类合并
├── 优点:代码质量可控,社区凝聚力强
└── 缺点:开发速度慢,人力成本高
激进模式(Bun/Anthropic 路线):
AI 生成 → 人类审查(+ 自动化验证) → 人类合并
├── 优点:开发速度快,可以快速实验
└── 缺点:审查负担增加,可能引入微妙 bug
混合模式(未来可能的主流):
人类设计 → AI 实现 → 人类审查 → 自动化验证 → AI 修复
├── 优点:结合人类创造力和 AI 效率
└── 缺点:需要成熟的工具链支撑
九、给开发者的启示
9.1 选择系统编程语言的实用建议
Bun 的经历给我们提供了宝贵的经验:
如果你的项目依赖外部引擎(如 JavaScriptCore),优先选择与引擎 C API 互操作更方便的语言。Rust 的 bindgen + cbindgen 工具链成熟度远超 Zig 的 @cImport。
如果你的项目规模可能超过 10 万行,优先选择有成熟生态的语言。Zig 的"手动管理一切"在项目小时是优势(灵活),在项目大时是负担(每个人都需要重新发明轮子)。
如果你的团队计划使用 AI 辅助开发,避免选择有 AI 限制的社区。这不只是工具选择,而是战略选择。
如果你的项目需要招聘,选择人才池更大的语言。Rust 开发者比 Zig 开发者多 100 倍。
9.2 JavaScript 运行时开发者的技术栈建议
如果你正在开发 JavaScript 运行时或类似的基础设施项目,推荐的技术栈:
核心绑定层:Rust + deno_core(如果用 V8)或 Rust + 自建 JSC 绑定(如果用 JSC)
异步运行时:tokio
HTTP:hyper
TLS:rustls
序列化:serde
构建系统:cargo + just(替代 Makefile)
CI:GitHub Actions + cross-rs(交叉编译)
9.3 迁移决策框架
如果你面临类似 Bun 的语言迁移决策,可以使用以下框架:
迁移决策矩阵:
Zig 留守 Rust 迁移 混合架构
─────────────────────────────────────────────────
短期开发效率 ★★★★ ★★ ★★★
长期维护成本 ★★ ★★★★ ★★★
生态支持 ★ ★★★★★ ★★★
性能潜力 ★★★★ ★★★★ ★★★★
安全性 ★★★ ★★★★★ ★★★★
人才可获取性 ★ ★★★★ ★★
AI 辅助开发兼容性 ★★ ★★★★ ★★★
社区治理风险 ★★★ ★ ★★
─────────────────────────────────────────────────
总分 20 27 24
Rust 迁移的总分最高,但混合架构作为过渡方案也有其价值。
十、总结与展望
Bun 的 Zig→Rust 移植指南,表面上是一个技术选型事件,实质上是 2026 年软件行业三大趋势的交汇点:
趋势一:Rust 成为系统编程的事实标准。从 Linux 内核到 Android 底层,从 Cloudflare Workers 到 Deno,Rust 正在成为"新 C++"。Bun 如果加入这个阵营,将进一步加速这个趋势。
趋势二:AI 重写软件。Cloudflare、Ladybird 的先例已经证明,AI 辅助的大规模代码迁移是可行的。随着 LLM 代码生成能力的提升,这种模式将越来越普遍。未来的开源项目可能不再需要"人类写代码",而是"人类审查代码"。
趋势三:开源治理的范式分裂。Zig 的 AI 禁令和 Sumner 的"禁止人类贡献代码"论断,代表了对立的两极。未来的开源社区将在保守与激进之间寻找平衡。
无论 Bun 最终是否推进 Rust 迁移,这份移植指南已经揭示了一个深刻的事实:在 AI 时代,编程语言的选择不再只是技术决策,更是战略决策。选择一个拥抱 AI 的语言生态,可能比选择一个语法更优雅的语言更重要。
对于 JavaScript 运行时赛道而言,2026 年注定是分水岭之年。Node.js 在 C++ 的老路上艰难前行,Deno 在 Rust 的新路上快速迭代,Bun 在 Zig 与 Rust 的十字路口犹豫不决。最终的赢家,可能不是最快的那个,而是最能适应 AI 时代开发范式的那个。
本文基于 Bun 创始人 Jarred Sumner 于 2026 年 5 月在 GitHub 发布的 Zig→Rust 移植指南、Hacker News 社区讨论、以及公开的技术资料撰写。Bun 团队尚未正式承诺进行重写,本文分析基于当前公开信息和技术推理。