Bun 改用 Rust 重写
速览
Bun 是一个 JavaScript 运行时与工具链,现计划将其核心用 Rust 重写。此举将带来更快的启动速度、更低的内存占用,并有望改善类型安全。重写后 Bun 将保持与现有 JavaScript 生态兼容,同时获得 Rust 底层性能优势。
AI 深度解读
背景
Bun 是一个野心勃勃的 JavaScript 运行时与工具链,最初由 Jarred Sumner 在 2021 年 4 月用 Zig 语言编写。Bun 的起点是逐行将 esbuild 的 JavaScript/TypeScript 转译器从 Go 移植到 Zig。创始人选择 Zig 是因为被其低级控制能力和对性能的极致追求所吸引。
Bun 的规模从一开始就非常庞大:涵盖 JavaScript/TypeScript/CSS 转译器、压缩器、打包器,npm 兼容的包管理器,Jest 风格测试运行器,Node.js 兼容的模块解析,HTTP/1.1 与 WebSocket 客户端,以及大量 Node.js API 的实现(如 fs、net、tls 等)。在 2022 年没有 LLM 辅助的情况下,创始人独自在狭小的 Oakland 公寓里用一年时间完成了初版。Zig 让这一切成为可能,否则在如此短的时间内构建如此庞大的项目几乎不可能。
如今,Bun 的 CLI 每月下载量超过 2200 万次,Claude Code、OpenCode 等流行工具选择 Bun 作为运行时,Vercel、Railway、DigitalOcean 等平台也提供了对 Bun 的一流支持。2025 年 12 月,Bun 被 Anthropic 收购,包括创始人在内的团队成员现任职于 Anthropic。
核心内容
稳定性挑战:从 Zig 到 Rust 的转折点
Bun 的广泛功能范围也带来了稳定性方面的巨大挑战。尽管团队已经采取了多项措施(在 Zig 编译器中添加 Address Sanitizer 支持、在每次提交时运行 ASAN 测试、在 Windows 上发布 Zig 安全检查 ReleaseSafe 版本、使用 Fuzzilli 持续模糊测试 JavaScript 引擎 API、大量端到端内存泄漏测试),但 Bug 列表依然令人不安。
以下是 Bun v1.3.14 版本修复的部分 Bug 示例(原文列举了以下问题,此处以中文概括):
- 在
node:zlib中,当异步.write()仍在进行时调用.reset()导致堆释放后使用崩溃。 - 在
node:zlib中,onerror回调中重入的write()后跟close()导致释放后使用。 - 在
node:http2中,重入 JS 回调(如超时监听器内的session.request())触发哈希表重哈希,导致内部流指针失效。 - 在
UDPSocket.send()和sendMany()中,用户代码的valueOf()或toString()回调在捕获负载与实际发送之间分离ArrayBuffer。 - 在
Buffer#copy和Buffer#fill中,valueOf回调在参数强制转换期间分离或调整底层ArrayBuffer导致越界读取。 - 在
UDPSocket.sendMany()中,套接字连接状态在用户 JS 回调中发生改变导致堆越界写入。 - 在
crypto.scrypt中,输出缓冲区分配失败时回调与受保护的密码/盐缓冲区未释放。 - 在
SSLWrapper.init中,错误路径上strdup的密码泄露。 - 在
tlsSocket.setSession()中,每次调用泄露约 6.5 KB 的SSL_SESSION。 - 在
fs.watch()中,.close()后观察者从未被垃圾回收,因为引用计数下溢使其永久固定为 GC 根。 - 在 CSS 解析器中,
background-clip带有厂商前缀和多层背景时发生双重释放。 DuplexUpgradeContext从未释放,导致tls.connect({ socket: duplex })完全泄漏。- 在
MessageEvent中,GC 标记线程在并发访问BroadcastChannel或MessagePort时观察到m_data中的撕裂变体,导致竞态条件崩溃。
问题根源:Zig 与手动内存管理的局限性
Zig 像 C 一样,不管理内存,这对于许多项目来说是一个优势,但对于 Bun 这样需要同时处理垃圾回收(GC)语言和手动管理内存的项目,情况变得复杂。JavaScript 是垃圾回收语言,JavaScriptCore 和 V8 对异常处理和 GC 有严格规则。Zig 没有构造函数/析构函数,大多数清理工作需要在每个调用点显式使用 defer 编写。
对于 Bun,正确处理垃圾回收值和手动管理值的生命周期已成为稳定性的主要来源——最常见的是小内存泄漏,偶尔是崩溃。每次内存分配都需要仔细审查:这些字节在哪里释放?如何确保只释放一次?是否正确检查了 JavaScript 异常?这个 GC 指针是否对保守栈扫描可见?这是 GC 内存还是手动管理的内存?
现有方法及其不足
团队尝试通过风格指南来规范这些问题,比如 TigerBeetle 的 TigerStyle(Zig 示例)或 Google 的 3.1 万字 C++ 风格指南。但风格指南的挑战在于执行——如何确保代码遵循?历史上依赖代码审查,配合 linter 和静态分析器进行尽力而为的检查。
如果采用严格的风格指南并在类型系统中明确所有权,Zig 代码会变得冗长。例如,共享指针的 Zig 代码可能写成:
fn foo(a_ptr: SharedPtr(TCPSocket)) !void {
const a: *TCPSocket = a_ptr.get();
defer a_ptr.deref();
const b = try do_something_with_a(a);
defer b.deref();
// ...
}
这远不如期望的 Zig 代码简洁:
fn foo(a: *TCPSocket) !void {
const b = try do_something_with_a(a);
// ...
}
为什么不选 C++?
Bun 已有约 20% 的代码用 C++ 编写,并嵌入了多个 C/C++ 库(JavaScriptCore、uWebSockets、BoringSSL、SQLite 等)。C++ 拥有构造函数和析构函数,可以删除大量 extern "C" 包装代码。但即便如此,仍然依赖通过代码审查执行风格指南,并且即使有 ASAN,内存损坏和内存泄漏仍然会出现。
为什么选择 Rust
Rust 提供了 RAII(资源获取即初始化)和所有权模型
