Bun

Rewriting Bun in Rust用 Rust 重写 Bun


Jarred Sumner · July 8, 2026

Disclosure: Bun was acquired by Anthropic in December 2025. I and others on the Bun team work at Anthropic. I used a pre-release version of Claude Fable 5 for much of the Rust rewrite.披露:Bun 已于 2025 年 12 月被 Anthropic 收购。我和 Bun 团队的其他成员目前都在 Anthropic 工作。在这次 Rust 重写过程中,我大量使用了 Claude Fable 5 的预发布版本。

Bun started as a line-for-line port of esbuild's JavaScript & TypeScript transpiler from Go to Zig. I wrote my first line of Zig on April 16, 2021. I bet on Zig after seeing the single-page Zig Language Reference on Hacker News and getting really excited about the low-level control and care for performance.Bun 最初是 esbuild 的 JavaScript 和 TypeScript 转译器从 Go 到 Zig 的逐行移植。我于 2021 年 4 月 16 日写下了第一行 Zig 代码。在 Hacker News 上看到单页的《Zig 语言参考》并对其底层控制能力和对性能的追求感到兴奋后,我决定押注 Zig。

From the start, Bun's scope was massive:从一开始,Bun 的目标就非常宏大:

  • JavaScript, TypeScript, and CSS transpiler, minifier, and bundlerJavaScript、TypeScript 和 CSS 的转译器、压缩器及打包工具
  • npm-compatible package manager兼容 npm 的包管理器
  • Jest-like test runner类似 Jest 的测试运行器
  • Node.js & TypeScript-compatible module resolution兼容 Node.js 和 TypeScript 的模块解析
  • HTTP/1.1 & WebSocket clientHTTP/1.1 和 WebSocket 客户端
  • Node.js API implementations like fs, net, tls, and dozens of other modulesNode.js API 实现,如 fs、net、tls 以及其他数十个模块

The initial version of Bun was written by me in 1 year, in a cramped Oakland apartment, pre-LLM, in Zig. The default outcome for ambitiously-scoped projects like Bun is joining the graveyard of dead side projects on a GitHub profile page. Zig made Bun possible. I would never have been able to build this much in 1 year if it wasn't for Zig.最初版本的 Bun 是我在奥克兰一间狭小的公寓里,在 LLM 出现之前,用 Zig 花了一年时间独自编写的。像 Bun 这样目标宏大的项目,通常的结局就是成为 GitHub 个人主页上那些死掉的副项目墓地中的一员。Zig 让 Bun 的诞生变为可能。如果不是因为 Zig,我绝不可能在一年内构建出如此多的功能。

Nowadays, Bun's CLI gets over 22 million monthly downloads. Popular tools like Claude Code and OpenCode bet on Bun as their runtime. Vercel, Railway, DigitalOcean and more have 1st-party support for Bun.如今,Bun 的 CLI 每月下载量已超过 2200 万次。Claude Code 和 OpenCode 等热门工具都将 Bun 作为其运行时。Vercel、Railway、DigitalOcean 等平台也已提供对 Bun 的原生支持。

Bun's scope has also been a challenge for stability. Here's a small sample of bugs we fixed in Bun v1.3.14:Bun 的庞大范围也给稳定性带来了挑战。以下是我们修复的 Bun v1.3.14 中部分 Bug 的示例:

  • heap-use-after-free crash in node:zlib when calling .reset() on a zlib, Brotli, or Zstd stream while an async .write() is still in progress on the threadpool在 node:zlib 中,当线程池中仍有异步 .write() 操作在进行时,对 zlib、Brotli 或 Zstd 流调用 .reset() 会导致 heap-use-after-free 崩溃
  • use-after-free crash in node:zlib when an onerror callback issued a re-entrant write() followed by close() on native handles在 node:zlib 中,当 onerror 回调触发了重入式 write(),随后又对原生句柄调用 close() 时,会导致 use-after-free 崩溃
  • use-after-free crashes in node:http2 when re-entrant JS callbacks (e.g. session.request() inside a timeout listener, an options getter, or a write callback) triggered a hashmap rehash, invalidating internal stream pointers在 node:http2 中,当重入式 JS 回调(例如在超时监听器、属性获取器或写入回调中调用 session.request())触发哈希表重哈希,导致内部流指针失效时,会引发 use-after-free 崩溃
  • use-after-free in UDPSocket.send() and sendMany() where user code in valueOf() or toString() callbacks could detach an ArrayBuffer between payload capture and the actual send在 UDPSocket.send() 和 sendMany() 中,用户代码在 valueOf() 或 toString() 回调中可能会在载荷捕获与实际发送之间分离 ArrayBuffer,导致 use-after-free
  • crash and out-of-bounds read in Buffer#copy and Buffer#fill when a valueOf callback detaches or resizes the underlying ArrayBuffer during argument coercion在 Buffer#copy 和 Buffer#fill 中,当 valueOf 回调在参数强制转换期间分离或调整底层 ArrayBuffer 大小时,会导致崩溃和越界读取
  • heap out-of-bounds write in UDPSocket.sendMany() when the socket's connection state changed mid-iteration via user JS callbacks在 UDPSocket.sendMany() 中,当套接字的连接状态通过用户 JS 回调在迭代过程中发生改变时,会导致堆越界写入
  • memory leak in crypto.scrypt where the callback and protected password/salt buffers were never released when the output buffer allocation failedcrypto.scrypt 中的内存泄漏,当输出缓冲区分配失败时,回调和受保护的密码/盐缓冲区从未被释放
  • SSLWrapper.init leaked the strdup'd passphrase on error pathsSSLWrapper.init 在错误路径中泄漏了 strdup 出来的密码短语
  • memory leak in tlsSocket.setSession() where each call leaked one SSL_SESSION (~6.5 KB per call) due to a missing SSL_SESSION_free after d2i_SSL_SESSIONtlsSocket.setSession() 中的内存泄漏,由于 d2i_SSL_SESSION 后缺少 SSL_SESSION_free,每次调用都会泄漏一个 SSL_SESSION(每次约 6.5 KB)
  • memory leak where fs.watch() watchers were never garbage collected after .close(), caused by a reference count underflow that permanently pinned each watcher as a GC rootfs.watch() 观察者在 .close() 后从未被垃圾回收的内存泄漏,这是由引用计数下溢导致的,使得每个观察者被永久固定为 GC 根
  • double-free crash in the CSS parser when background-clip had vendor prefixes and multi-layer backgroundsCSS 解析器中的双重释放崩溃,发生在 background-clip 具有供应商前缀和多层背景时
  • DuplexUpgradeContext was never freed — a full leak per tls.connect({ socket: duplex })DuplexUpgradeContext 从未被释放——每次 tls.connect({ socket: duplex }) 都会导致完全的内存泄漏
  • race condition crash in MessageEvent where the GC marker thread could observe a torn variant in m_data during concurrent access from a BroadcastChannel or MessagePortMessageEvent 中的竞态条件崩溃,当来自 BroadcastChannel 或 MessagePort 的并发访问时,GC 标记线程可能会观察到 m_data 中不完整的变体

We could have kept fixing these kinds of bugs one-off in perpetuity, but we owe it to our users counting on us to do better than that, and systematically prevent these kinds of bugs from recurring.我们本可以永远这样零散地修复此类 Bug,但我们有义务对依赖我们的用户负责,做得更好,并从根本上防止此类 Bug 再次发生。

What we were already doing我们已经在做的事情

  • We patched the Zig compiler to add Address Sanitizer support. We run our test suite with ASAN on every commit.我们修补了 Zig 编译器以增加对地址消毒器(Address Sanitizer)的支持。我们在每次提交时都会使用 ASAN 运行测试套件。
  • We ship Zig safety-checked ReleaseSafe builds on Windows我们在 Windows 上发布了经过 Zig 安全检查的 ReleaseSafe 构建版本
  • We fuzz Bun's runtime APIs 24/7 using Fuzzilli, the JavaScript engine fuzzer used by V8 & JavaScriptCore我们使用 Fuzzilli(V8 和 JavaScriptCore 使用的 JavaScript 引擎模糊测试工具)全天候对 Bun 的运行时 API 进行模糊测试
  • We have a whole lot of end-to-end memory leak tests我们拥有大量端到端的内存泄漏测试

This is more than many projects do.这已经比许多项目做得更多了。

Just be really smart and don't make mistakes?难道只要足够聪明、不犯错就行了吗?

Our bugfix list felt bad and I was tired of going to sleep worrying about crashes in Bun. I don't blame Zig for that - other users of Zig don't have the bugs we had, and mixing GC with manually-managed memory is an uncommon enough thing for software to need that no language really designs for it. We wouldn't have gotten this far if not for Zig, and I'll always be grateful. Until very recently, programming language choice was a one-way decision for a project like Bun.我们的 Bug 修复列表看起来很糟糕,我厌倦了每天晚上担心 Bun 会崩溃。我不怪 Zig——其他 Zig 用户并没有遇到我们这样的 Bug,而且将 GC 与手动管理的内存混合使用在软件开发中非常罕见,没有哪种语言是为此专门设计的。如果不是因为 Zig,我们不可能走到今天,我将永远心存感激。直到最近,对于像 Bun 这样的项目来说,编程语言的选择还是一个单向决策。

JavaScript is a garbage-collected language and modern JavaScript engines like JavaScriptCore (and V8) have strict rules around exception handling and the garbage collector. Zig, like C, doesn't manage memory for you and this is a tradeoff that for many projects is a great reason to use Zig. Zig does not have constructors/destructors, and most cleanup is expected to be written out explicitly at each call site with defer.JavaScript 是一种垃圾回收语言,现代 JavaScript 引擎(如 JavaScriptCore 和 V8)对异常处理和垃圾回收有严格的规则。Zig 和 C 一样,不会为你管理内存,对于许多项目来说,这是一个选择 Zig 的绝佳理由。Zig 没有构造函数/析构函数,大多数清理工作需要在每个调用点通过 defer 显式编写。

For Bun, correctly handling the lifetimes of garbage-collected values and manually-managed values has been a major source of stability issues - most often small memory leaks and occasionally, crashes. Every memory allocation has to be meticulously reviewed. Where do these bytes get freed? How do we ensure it only gets freed once? Did we check for JavaScript exceptions properly? Is this garbage-collected pointer visible to the conservative stack scanner? Is this garbage collected memory or manually managed memory?对于 Bun 而言,正确处理垃圾回收值与手动管理值之间的生命周期一直是稳定性问题的主要来源——最常见的是微小的内存泄漏,偶尔也会引发崩溃。每一次内存分配都必须经过仔细审查。这些字节在哪里被释放?我们如何确保它只被释放一次?我们是否正确检查了 JavaScript 异常?这个垃圾回收指针对于保守的栈扫描器是否可见?这是垃圾回收内存还是手动管理的内存?

For stability issues, knowing as early as possible is best. Fuzzing happens after code is merged. CI happens when code is pushed. Runtime safety checks & address sanitizer happens when code is run (hopefully in development, before CI).对于稳定性问题,越早发现越好。模糊测试发生在代码合并之后。CI 发生在代码推送时。运行时安全检查和地址消毒器发生在代码运行时(最好是在开发阶段,CI 之前)。

One common way to reduce this class of issue is to ensure cleanup code is always run exactly once for code that needs it. Zig is designed to be a simple language with no hidden control flow, and so it prefers the explicit defer keyword to run code at the end of a scope over C++'s implicit ~Destructor or Rust's implicit Drop.减少此类问题的一种常见方法是确保清理代码对于需要它的场景始终只运行一次。Zig 被设计为一种没有隐藏控制流的简单语言,因此它倾向于使用显式的 defer 关键字在作用域结束时运行代码,而不是 C++ 的隐式析构函数或 Rust 的隐式 Drop。

LanguageCleanup
Zigdefer, errdefer
C++~Destructor, &&Move
RustDrop

For Zig code, when exactly should we be running the cleanup code? If we're passing the same *T to many different functions, how do we know when it's no longer accessible and can be cleaned up? How does it work when some functions need to continue to reference the memory after the function is called? Our current approach is a mix of:对于 Zig 代码,我们到底应该在什么时候运行清理代码?如果我们把同一个 *T 传递给许多不同的函数,我们怎么知道它何时不再可访问并可以被清理?当某些函数需要在函数调用后继续引用该内存时,它是如何工作的?我们目前的方法是混合了以下几种方式:

  • arena lifetimes, where the scope of when it's accessible is clear (parser state doesn't escape the calling function and so AST nodes are a good choice there)Arena 生命周期,其可访问范围很明确(解析器状态不会逃逸出调用函数,因此 AST 节点在这里是一个很好的选择)
  • reference-counting引用计数
  • pay really close attention保持高度警惕

Many projects opt to answer these kinds of questions through a style guide. TigerBeetle's TigerStyle is an example in Zig and Google's 31,000 word C++ style guide is another. The challenge with style guides is enforcement. How do you make sure the style guide is followed? Historically, code review was the answer with best-effort enforcement via linters & static analyzers.许多项目选择通过风格指南来回答这些问题。TigerBeetle 的 TigerStyle 是 Zig 中的一个例子,谷歌 31,000 字的 C++ 风格指南是另一个。风格指南的挑战在于执行。你如何确保风格指南得到遵循?历史上,代码审查是答案,辅以通过 Lint 工具和静态分析器进行的尽力而为的强制执行。

Having a rigid style guide with clear ownership expectations explicitly spelled out in the type system was a real option for Bun. Since Zig has no operator overloading, we would likely end up with a lot of code looking something like this:拥有一个严格的风格指南,并在类型系统中明确规定所有权预期,这对 Bun 来说是一个切实可行的选择。由于 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();

  // ...
}

This is less ergonomic than the Zig we expect:这比我们预期的 Zig 代码更不符合人体工程学:

fn foo(a: *TCPSocket) !void {
  const b = try do_something_with_a(a);
  // ...
}

What about C/C++?那 C/C++ 呢?

About 20% of Bun's code is written in C++ and Bun embeds several C/C++ libraries:Bun 大约 20% 的代码是用 C++ 编写的,并且 Bun 嵌入了多个 C/C++ 库:

  • JavaScriptCore, the JavaScript engine that powers SafariJavaScriptCore,驱动 Safari 的 JavaScript 引擎
  • uWebSockets & usockets - our HTTP/WebSocket server, and event loopuWebSockets 和 usockets——我们的 HTTP/WebSocket 服务器和事件循环
  • lshpack & lsquic - HPACK and HTTP/3 librarieslshpack 和 lsquic——HPACK 和 HTTP/3 库
  • BoringSSL, Google's OpenSSL forkBoringSSL,谷歌的 OpenSSL 分支
  • SQLiteSQLite

C++ instead of Zig would be a reasonable choice for Bun. We would get constructors & destructors. We could delete lots of extern "C" wrapper code.对于 Bun 来说,选择 C++ 而非 Zig 是合理的。我们将获得构造函数和析构函数,可以删除大量的 extern "C" 包装代码。

But, we would still be reliant on style guides enforced through code review, and even with ASAN, memory corruption and memory leaks would still happen.但是,我们仍然需要依赖通过代码审查强制执行的风格指南,即使有 ASAN,内存损坏和内存泄漏仍然会发生。

Why Rust?为什么选择 Rust?

A large percentage of bugs from that list are use-after-free, double-free, and "forgot to free" in an error path. In safe Rust, these are compiler errors and RAII-like automatic cleanup with Drop. Compiler errors are a better feedback loop than a style guide.列表中的大量 Bug 都是 use-after-free、double-free 以及在错误路径中“忘记释放”。在安全的 Rust 中,这些是编译器错误,并具有类似 RAII 的 Drop 自动清理机制。编译器错误比风格指南提供了更好的反馈循环。

Historically, rewrites are a terrible idea. Excluding comments, Bun is 535,496 lines of Zig. A rewrite in another language would take a small team of engineers a full year. It would mean freezing bugfixes, security fixes or feature development for that time. The least risky approach to getting something shippable would be a mechanical port from Zig to Rust, with the minimal number of behavioral changes, using the exact same test suite we already use for testing Bun.从历史上看,重写是一个糟糕的主意。排除注释,Bun 有 535,496 行 Zig 代码。用另一种语言重写需要一个小型的工程师团队花费整整一年时间。这意味着在这一年里,Bug 修复、安全修复或功能开发都将停滞。获得可交付成果风险最小的方法是进行从 Zig 到 Rust 的机械移植,尽可能减少行为改变,并使用我们现有的 Bun 测试套件进行测试。

Fortunately, Bun's own test suite is written in TypeScript which means it doesn't depend on the runtime's programming language.幸运的是,Bun 自己的测试套件是用 TypeScript 编写的,这意味着它不依赖于运行时本身的编程语言。

A year of zero user-facing impact is not a realistic option we could consider. So, enforcement through code-style to fix stability issues was our best bet, and was our plan when we added Rust-inspired smart pointers to Bun's codebase.一年时间对用户没有任何影响,这不是我们能考虑的现实选择。因此,通过代码风格来强制执行以解决稳定性问题是我们最好的选择,这也是我们在 Bun 代码库中添加受 Rust 启发的智能指针时的计划。

But honestly, I didn't want to do it. Homegrown smart pointers offer worse ergonomics than Rust, with none of the guarantees.但老实说,我并不想这样做。自制的智能指针的人体工程学比 Rust 差,而且没有任何保证。

What if, instead, I spend a week testing if Anthropic's new model can rewrite Bun in Rust?如果我花一周时间测试 Anthropic 的新模型能否用 Rust 重写 Bun,会怎样?

At first, I didn't expect it to work. A few days in, a high % of the test suite started passing and I saw how much the new Rust code matched up with the original Zig codebase. My opinion went from "this is worth trying" to "I'm going to merge this".起初,我没指望它能成功。几天后,很大比例的测试套件开始通过,我看到新的 Rust 代码与原始 Zig 代码库的匹配度非常高。我的想法从“值得一试”变成了“我要合并它”。

Claude, rewrite Bun in Rust.Claude,用 Rust 重写 Bun。

There are a lot of ways to do a terrible job of this. For example, prompting Claude "Rewrite Bun in Rust. Don't make any mistakes." and then praying it would work is not what I did.有很多方法会把这件事搞砸。例如,提示 Claude “用 Rust 重写 Bun,不要犯任何错误”,然后祈祷它能成功,这绝不是我所做的方法。

Think about how a person would do this. The first big question is:想想一个人会怎么做。第一个大问题是:

Incremental rewrite? Or, everything all at once?增量重写?还是全部一次性重写?

In my experience porting esbuild's transpiler from Go to Zig for the initial version of Bun (without LLMs), everything all at once is better. An incremental rewrite adds temporary code that you hope gets deleted eventually, and would be painful in the short-medium term.根据我最初移植 esbuild 转译器从 Go 到 Zig(没有使用 LLM)的经验,一次性重写更好。增量重写会增加临时代码,你希望最终能删除它们,这在短期到中期内会很痛苦。

The second big question: how?第二个大问题:如何做?

How do we keep Bun in Rust the same Bun as before, with the same architecture, performance, and feature-set while also getting the language features of Rust like the borrow checker? How do we ensure the team can still maintain it after the rewrite?我们如何在保持 Bun 与之前相同、具有相同架构、性能和功能集的同时,获得 Rust 的借用检查器等语言特性?我们如何确保团队在重写后仍然能够维护它?

Do the rewrite that looks like we transpiled our Zig code to Rust. We can gradually refactor it to reduce unsafe usage and look more like idiomatic Rust after Bun v1.4 ships.进行一次看起来像是把 Zig 代码转译成 Rust 的重写。在 Bun v1.4 发布后,我们可以逐步重构它以减少 unsafe 的使用,使其看起来更像地道的 Rust。

Those are the only two big questions. Everything else is tactics.这就是仅有的两个大问题。其他一切都是战术问题。

Loops that write & review code编写和审查代码的循环

A lot of day-to-day engineering work as software engineers can be over-simplified into loops.软件工程师的许多日常工程工作可以简化为循环。

// Pseudocode, not real code:
let task;
while ((task = todoList.pop())) {
  const result = task();
  const feedback = await Promise.all([review(result), review(result)]);
  await apply(feedback, result);
}

A task has some context associated with it (a Jira ticket, a GitHub issue, etc). The result is the code you wrote to fix it. Code reviewer(s) review the changes to check for regressions & correctness. And then you address the feedback.任务具有与之关联的上下文(Jira 工单、GitHub Issue 等)。结果是你编写的修复代码。代码审查者审查更改以检查回归和正确性。然后你处理反馈。

I rewrote Bun in Rust using about 50 dynamic workflows in Claude Code run continuously over the course of 11 days.我使用 Claude Code 中的大约 50 个动态工作流,在 11 天的时间里持续运行,完成了 Bun 的 Rust 重写。

Each dynamic workflow was a loop like this - a workflow for:每个动态工作流都是这样的循环——工作流用于:

  • Generate a porting guide mapping Zig patterns & types to Rust patterns & types生成将 Zig 模式和类型映射到 Rust 模式和类型的移植指南
  • Mechanically port every .zig file to a .rs file, matching the PORTING.md and LIFETIMES.tsv机械地将每个 .zig 文件移植为 .rs 文件,匹配 PORTING.md 和 LIFETIMES.tsv
  • Fix every crate's compiler errors修复每个 crate 的编译器错误
  • Get subcommands like bun test or bun build to work让 bun test 或 bun build 等子命令工作
  • Get every test in Bun's entire test suite to pass让 Bun 整个测试套件中的每个测试都通过
  • Several large refactors and cleanup passes几次大型重构和清理工作

For most of those 11 days (and after), I monitored workflows - manually reading the outputs to check for issues and bugs, and prompting Claude to edit the loop to fix things.在那 11 天的大部分时间里(以及之后),我监控着工作流——手动阅读输出以检查问题和 Bug,并提示 Claude 修改循环以修复问题。

How do you review a PR with +1 million lines added? How do you start to build the confidence needed to responsibly merge large quantities of LLM-authored code?你如何审查一个增加了 100 万行代码的 PR?你如何开始建立起负责任地合并大量 LLM 编写的代码所需的信心?

A language-independent test suite with a million assertions, adversarial code review and when something does go wrong, fixing the process that generates the code instead of hand-fixing the code.一个拥有数百万断言的语言无关测试套件、对抗性代码审查,当出错时,修复生成代码的流程,而不是手动修复代码。

Adversarial review对抗性审查

Adversarial review asks Claude (in a separate context window) to exhaustively come up with reasons why the changes create bugs or do not work.对抗性审查要求 Claude(在单独的上下文窗口中)详尽地提出这些更改为何会产生 Bug 或无法工作的理由。

Split context windows拆分上下文窗口

Usually with humans, the person reviewing the code is not the person who authored the code. The person writing the code wants to merge the code, which can bias their actions to ship before it's ready.通常在人类工作中,审查代码的人不是编写代码的人。编写代码的人想要合并代码,这可能会导致他们在代码准备好之前就急于发布。

Claude is the same way. The Claude that wrote the code wants the code to get accepted. The Claude that reviews wants to find issues in the code.Claude 也是一样。编写代码的 Claude 希望代码被接受。审查的 Claude 希望在代码中发现问题。

1 implementer, 2 or more adversarial reviewers per implementer. The reviewer's only job: find bugs & reasons why the code does not work. The implementer doesn't review. The reviewer doesn't implement.1 名实现者,每名实现者配备 2 名或以上对抗性审查者。审查者的唯一工作:发现 Bug 和代码无法工作的理由。实现者不进行审查。审查者不进行实现。

✻ claude code · dynamic workflow✻ claude code · 动态工作流adversarial review对抗性审查✻ claude code · 动态工作流对抗性审查3 of the many bugs adversarial review caught before merge对抗性审查在合并前捕获的众多 Bug 中的 3 个
bug 1 of 3 · the async close3 个 Bug 中的第 1 个 · 异步关闭
claudeclaudeimplementer实现者
its context: the .zig original, the port plan, its own reasoning其上下文:原始的 .zig 文件、移植计划、其自身的推理
claudeadversarial reviewer对抗性审查者
its context: only the diff. told to assume the code is wrong.其上下文:仅 diff。被告知假设代码是错误的。
src/runtime/api/bun/js_bun_spawn_bindings.rs · compiles clean· 编译干净src/runtime/api/bun/js_bun_spawn_bindings.rs · 编译干净
for对于 stdio stdioin [spawned_stdout, spawned_stderr] {[spawned_stdout, spawned_stderr] {
matchmatch stdio {stdio {
StdioResultStdioResult::BufferBuffer(mutmut pipe) pipe)=> {
// pipe: Box<uv::Pipe> — hand it to libuv to close// pipe: Box<uv::Pipe> — 交给 libuv 关闭
pipepipe.closeclose(SubprocessSubprocess::on_pipe_close)on_pipe_close)
}
StdioResult::Fd(fd) (fd)=> fd.close(),(),
StdioResult::UnavailableUnavailable => {}
}
}
uv_close is asynchronous: libuv keeps the raw handle pointer until the next loop tick, then calls on_pipe_close, which frees the allocation. But `pipe` is a Box that drops at the end of this match arm — libuv is left holding freed memory, and the close callback then frees it a second time. Use-after-free, then double-free.uv_close 是异步的:libuv 保留原始句柄指针直到下一个循环周期,然后调用 on_pipe_close,后者释放分配。但 `pipe` 是一个在 match 分支末尾被丢弃的 Box——libuv 此时持有了已释放的内存,而关闭回调随后又释放了它第二次。先是 Use-after-free,然后是 Double-free。
BoxBox::leakleak(pipe)(pipe).close(Subprocess::on_pipe_close)
f0a454376c7 · win-review: js_bun_spawn_bindings.rs leak Box<uv::Pipe> before async uv_close to avoid UAF/double-free in on_pipe_closef0a454376c7 · win-review: js_bun_spawn_bindings.rs 在异步 uv_close 之前泄漏 Box<uv::Pipe> 以避免 on_pipe_close 中的 UAF/double-free
Three bugs the adversarial reviewers actually caught — every cited commit carries its review attribution in the subject line. All three compiled; all three looked plausible. The reviewer is a second Claude in its own context window: it gets the diff and nothing else — none of the implementer's reasoning — and is told to find the way it's wrong. Code is condensed from the cited commits; same bugs, same fixes.这是对抗性审查者真正捕获到的三个 Bug——每个引用的提交都在主题行中带有其审查归属。所有三个都能编译;所有三个看起来都很合理。审查者是另一个在其自身上下文窗口中的 Claude:它只得到 diff,没有得到实现者的任何推理,并被告知找出它错误的方式。代码是从引用的提交中浓缩出来的;同样的 Bug,同样的修复。

What does this look like?这看起来像什么?

If you're about to do something big and expensive, it saves time and money to de-risk it first.如果你打算做一些重大且昂贵的事情,先降低风险可以节省时间和金钱。

Prep work准备工作

Before writing any code, I spent about 3 hours talking to Claude about how to map patterns from our Zig codebase closely to Rust. Claude serialized this discussion into a PORTING.md document, which ended up on Hacker News.在编写任何代码之前,我花了大约 3 个小时与 Claude 讨论如何将我们 Zig 代码库中的模式紧密映射到 Rust。Claude 将这次讨论序列化为一份 PORTING.md 文档,该文档最终出现在 Hacker News 上。

The next question: how do you add Rust lifetimes to code that manually manages memory?下一个问题:你如何为手动管理内存的代码添加 Rust 生命周期?

That's where I prompted Claude something like this:这就是我提示 Claude 的地方,大致如下:

Me: Let's kick off a dynamic workflow to analyze the proper lifetimes of every struct field in the codebase. This workflow should read every struct field within every single file and trace the control flow. First, look for struct fields with complex lifetimes to express in Rust, then propose a lifetime for that field, then use 2 adversarial review agents to review that lifetime, then apply any feedback and serialize into a LIFETIMES.tsv for other claudes to look at.我:让我们启动一个动态工作流来分析代码库中每个结构体字段的正确生命周期。这个工作流应该读取每个文件中的每个结构体字段并跟踪控制流。首先,寻找具有复杂生命周期需要用 Rust 表示的结构体字段,然后为该字段提出一个生命周期,然后使用 2 个对抗性审查代理来审查该生命周期,然后应用任何反馈并序列化为 LIFETIMES.tsv 供其他 Claude 查看。

Then a round of adversarial reviews on the PORTING.md and the LIFETIMES.tsv together to fix any conflicting suggestions and double check everything. I also manually read over it.然后对 PORTING.md 和 LIFETIMES.tsv 进行一轮对抗性审查,以修复任何相互冲突的建议并仔细检查所有内容。我还手动阅读了一遍。

Trial run试运行

Before asking Claude to translate all 1,448 .zig files to .rs files, I started with just 3. For each of the 3 files, 1 implementer wrote the new .rs file, 2 adversarial reviewers checked the .rs file matched the behavior of the .zig file and that it followed the PORTING.md & LIFETIMES.tsv. After that, 1 fixer applied any suggestions.在要求 Claude 将所有 1,448 个 .zig 文件翻译成 .rs 文件之前,我先从 3 个文件开始。对于这 3 个文件中的每一个,1 名实现者编写了新的 .rs 文件,2 名对抗性审查者检查 .rs 文件是否匹配 .zig 文件的行为,以及它是否遵循 PORTING.md 和 LIFETIMES.tsv。之后,1 名修复者应用了任何建议。

False starts错误的开始

I asked Claude to loop the workflow on all 1,448 .zig files, and about 2 minutes in, one Claude ran git stash before committing. Another ran git stash pop. And then git reset HEAD --hard. They were stepping on each other! And if I put each Claude into a separate worktree, I would run out of disk space because Bun's git repository is too big and eventually the changes will need to be compiled and seen together.我要求 Claude 在所有 1,448 个 .zig 文件上循环运行工作流,大约 2 分钟后,一个 Claude 在提交之前运行了 git stash。另一个运行了 git stash pop。然后是 git reset HEAD --hard。它们在互相干扰!如果我把每个 Claude 放在单独的工作树中,我会耗尽磁盘空间,因为 Bun 的 git 仓库太大了,而且最终这些更改需要一起编译和查看。

So, I asked Claude to edit the workflow to instruct Claude to never run git stash or git reset or any git command that doesn't commit a specific file at once. No cargo either. No slow commands at all.所以,我要求 Claude 修改工作流,指示 Claude 永远不要运行 git stash 或 git reset,或任何不能一次提交特定文件的 git 命令。也不要运行 cargo。不要运行任何慢速命令。

Then, Claude resumed the workflows. And it was working! Too slowly, so I split it into just 4 workflow shards each with their own worktree (4 worktrees total), each running 16 claudes committing and pushing files.然后,Claude 恢复了工作流。它成功了!太慢了,所以我把它拆分成 4 个工作流分片,每个分片都有自己的工作树(总共 4 个工作树),每个分片运行 16 个 Claude 提交和推送文件。

Finally writing the code最终编写代码

Thanks to all the parallelization & this prep work, at peak Claude wrote about 1,300 lines of code per minute. Every line of code was reviewed by two separate adversarial reviewers (also Claude) and went through a round of fixes before committing. Absolutely none of it worked yet.多亏了所有的并行化和准备工作,Claude 在高峰期每分钟编写了大约 1,300 行代码。每一行代码都由两名独立的对抗性审查者(也是 Claude)审查,并在提交前经过了一轮修复。绝对没有一行代码能直接工作。

11 days × 24 hours · PDT11 天 × 24 小时 · PDT
6,502 commits6,502 次提交
1695 commits/hour695 次提交/小时
12am6am12pm6pmMay 4May 5May 6May 7May 8May 9May 10May 11May 12May 13May 14
Every commit on the port branch (merges excluded), bucketed by hour. Peak hour: 695 commits.移植分支上的每次提交(不包括合并),按小时分桶。高峰小时:695 次提交。

Notice the inconsistent timing? I forgot to increase the default IOPS on the EC2 instance this ran on. One slow grep command was all it took to freeze disk reads & writes for minutes.注意到不一致的时间了吗?我忘了增加运行此任务的 EC2 实例上的默认 IOPS。一个缓慢的 grep 命令就足以冻结磁盘读写几分钟。

Compiler errors as a work queue编译器错误作为工作队列

After writing all the code, I asked Claude to write a workflow fixing every compiler error. We went crate-by-crate.写完所有代码后,我要求 Claude 编写一个修复每个编译器错误的工作流。我们按 crate 逐个进行。

✻ claude code · dynamic workflow
≈16,000≈16,000 errors left个错误剩余
Wed, May 6, 12:40 AM PDT5 月 6 日,周三,凌晨 12:40 PDT
errors.txterrors.txt0 fix commits0 次修复提交
error错误: deref *mut EventLoop before field access: 在字段访问前解引用 *mut EventLoop
error: js_parser/ast/E.rs: port json_stringify for Number/BigInt/RegExp: js_parser/ast/E.rs: 为 Number/BigInt/RegExp 移植 json_stringify
error: NodeHTTPResponse.rs: wire JSNodeHTTPResponse cached accessors vi: NodeHTTPResponse.rs: 连线 JSNodeHTTPResponse 缓存访问器
error[E0034]错误[E0034]: multiple applicable items in scope: 作用域中有多个适用的项
error: test_command.rs: wire coverage façade to bun_sourcemap_jsc::code: test_command.rs: 将覆盖率外观连线到 bun_sourcemap_jsc::code
error: bundler/ungate_support.rs: un-gate bun_css shim to real ::bun_cs: bundler/ungate_support.rs: 取消 bun_css shim 的门控,指向真正的 ::bun_css
error: dns.rs: implement pending_cache_for/get_key/get_or_put_into_reso: dns.rs: 实现 pending_cache_for/get_key/get_or_put_into_reso
error: css/css_parser.rs: port DefineShorthand contract, parse_bundler,: css/css_parser.rs: 移植 DefineShorthand 合约,parse_bundler,
error: runtime/crypto/mod.rs: create_crypto_error delegates to boringss: runtime/crypto/mod.rs: create_crypto_error 委托给 boringssl
error: bun_core/fmt.rs: implement format_ip reborrow (offset-based slic: bun_core/fmt.rs: 实现 format_ip 重借用(基于偏移量的切片)
error: event_loop/EventLoopTimer.rs: port Timespec::ns from bun.zig: event_loop/EventLoopTimer.rs: 从 bun.zig 移植 Timespec::ns
divvied up · 64 claudes分配 · 64 个 Claude
worktree 1工作树 1
worktree 2工作树 2
worktree 3工作树 3
worktree 4工作树 4
1 fixes1 次修复2 review2 次审查1 applies1 次应用
→ commits land per crate→ 提交按 crate 落地
 
 
 
How phase D worked, replayed from its 1,610 real commits (May 6, PDT): cargo check wrote ≈16,000 errors to a file, grouped by crate; the workflow divvied them up among 64 Claudes — 16 loops across 4 worktrees, each one Claude fixing, two reviewing, one applying. Every chip is a batch of real commits: it lands on its actual crate and only then do the counters move. Error lines are real commit subjects.阶段 D 的工作方式,从其 1,610 次实际提交(5 月 6 日,PDT)中重放:cargo check 将 ≈16,000 个错误写入文件,按 crate 分组;工作流将它们分配给 64 个 Claude——跨 4 个工作树的 16 个循环,每个 Claude 进行修复,两个审查,一个应用。每个芯片是一批实际的提交:它落地到其真正的 crate,然后计数器才会移动。错误行是实际的提交主题。

The trickiest class of error was cyclical dependencies.最棘手的错误类别是循环依赖。

Our Zig codebase was one compilation unit (effectively one crate). I wanted to split the new Rust codebase into ~100 crates so the Rust would compile faster, but this needed to avoid cyclical dependencies while minimizing changes compared to the original Zig implementation. My PR to do this immediately before starting the Rust rewrite was insufficient. Instead of starting over, I ran another workflow to classify where the code with cyclical dependencies should go and write it all down - and then another workflow to do the refactor.我们的 Zig 代码库是一个编译单元(实际上是一个 crate)。我想把新的 Rust 代码库拆分成 ~100 个 crate,这样 Rust 编译得更快,但这需要避免循环依赖,同时最小化与原始 Zig 实现相比的更改。我在 Rust 重写开始前立即提交的 PR 是不够的。我没有重新开始,而是运行了另一个工作流来分类具有循环依赖的代码应该去哪里,并把它们全部写下来——然后是另一个工作流来执行重构。

Fixing the cyclical dependencies revealed about 16,000 compiler errors. A massive number for 1 human, but not a crazy number for 64 claudes at once.修复循环依赖揭示了大约 16,000 个编译器错误。对于 1 个人来说是一个巨大的数字,但对于 64 个同时工作的 Claude 来说并不是一个疯狂的数字。

To maximize parallelism, the workflow looped over each crate.为了最大化并行性,工作流循环遍历每个 crate。

  • For each crate, run cargo check, group the output by file and save the errors to a file对于每个 crate,运行 cargo check,按文件对输出进行分组,并将错误保存到文件中
  • Fix all the compiler errors within that crate修复该 crate 内的所有编译器错误
  • 2 adversarial reviewers for the crate's changes2 名对抗性审查者审查 crate 的更改
  • 1 fixer applies the fixes1 名修复者应用修复

To prevent claudes from stepping on each other, cargo check only ran at the very start and like the other runs, no git until the end.为了防止 Claude 相互干扰,cargo check 只在最开始运行,并且像其他运行一样,直到最后才使用 git。

Another false start另一个错误的开始

Claude interpreted "let's get all the crates to compile" as "stub out the functions with compilation errors". Claude also started adding suspiciously long explanatory comments to document workarounds, so I added this rule for the adversarial reviewers to reject:Claude 将“让我们让所有 crate 编译通过”解释为“用编译错误存根函数”。Claude 还开始添加可疑的长解释性注释来记录变通方法,所以我为对抗性审查者添加了这个拒绝规则:

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code.如果你需要一段长注释来证明为什么变通方法是可以的,那么代码就是错的——修复代码。

One prompt edit and a few hours later, these things stopped happening.一个提示编辑和几个小时后,这些事情就不再发生了。

Smoke tests冒烟测试

Models love saying "smoke tests"模型喜欢说“冒烟测试”

Once cargo check passed, getting it to compile and run bun --version was next. It had linker errors. Then, it panicked immediately on start.一旦 cargo check 通过,接下来就是让它编译并运行 bun --version。它有链接器错误。然后,它在启动时立即崩溃。

The next goal was to get it to run bun test <file>. Once that worked, we could start running tests! Time for another workflow, looping over bun CLI subcommands:下一个目标是让它运行 bun test <file>。一旦成功,我们就可以开始运行测试了!是时候进行另一个工作流了,循环遍历 bun CLI 子命令:

  • Save each failing stacktrace to a file along with its subcommand将每个失败的堆栈跟踪与子命令一起保存到文件中
  • For each failing stacktrace grouped by subcommand, have 1 Claude fix对于每个按子命令分组的失败堆栈跟踪,让 1 个 Claude 修复
  • 2 adversarial reviewers2 名对抗性审查者
  • 1 fixer applies the suggestions1 名修复者应用建议

Get the test suite passing locally让测试套件在本地通过

This workflow looped on test files.这个工作流在测试文件上循环。

Run about 100 random test files sharded to one of 4 worktrees by folder in the codebase. For each failing test, save the stacktrace & errors to a file, 1 implementer proposes a fix, 2 adversarial reviewers, then 1 fixer applies.运行大约 100 个随机测试文件,按代码库中的文件夹分片到 4 个工作树中的一个。对于每个失败的测试,将堆栈跟踪和错误保存到文件中,1 名实现者提出修复,2 名对抗性审查者,然后 1 名修复者应用。

Even more false starts更多的错误开始

Our test suite has lots of memory leak tests and a handful of integration tests that can take more than a minute - for example: a test that runs next dev and checks hot module reloading can pick up on changes 100 times. Several of these tests timeout in debug builds.我们的测试套件有很多内存泄漏测试和少数需要超过一分钟的集成测试——例如:一个运行 next dev 并检查热模块重新加载的测试可以捕获 100 次更改。其中几个测试在调试构建中超时。

We also have stress tests that exhaust the max number of TCP sockets on the machine, tests that read & write gigabytes to disk, and tests that spawn ~10k processes.我们还有耗尽机器上最大 TCP 套接字数量的压力测试,读写 GB 数据到磁盘的测试,以及生成 ~10k 进程的测试。

This needed stronger isolation than "please", so we used systemd-run (cgroups) to limit memory & CPU usage and isolate pid namespaces. The machine ran out of disk space and crashed several times anyway.这需要比“请”更强的隔离,所以我们使用 systemd-run (cgroups) 来限制内存和 CPU 使用并隔离 pid 命名空间。机器磁盘空间不足,无论如何都崩溃了几次。

Get the test suite passing in CI让测试套件在 CI 中通过

Two days after the first CI run, the failing list was down from 972 test files to 23. A day and a half after that, Linux went fully green — and for the first time, it felt like this Rust rewrite was actually going to work.在第一次 CI 运行两天后,失败列表从 972 个测试文件减少到 23 个。在那之后的一天半,Linux 完全变绿了——第一次,我觉得这次 Rust 重写真的要成功了。

✻ claude code · dynamic workflowbuildkite · the race to green, by platformbuildkite · 按平台划分的绿色竞赛✻ claude code · 动态工作流buildkite · 按平台划分的绿色竞赛Windows finished last · May 11, 6:23 AM PDTWindows 最后完成 · 5 月 11 日,6:23 AM PDT
6 / 66 / 6 platforms green平台绿色
build #54202 · Thu, May 14, 12:23 AM PDT构建 #54202 · 5 月 14 日,周四,凌晨 12:23 AM PDT
May 8May 9May 10May 11May 12May 13May 14
macOS x64 · 2 shards· 2 个分片macOS x64 · 2 个分片
Linux arm64 · 60 shards· 60 个分片Linux arm64 · 60 个分片
Linux x64 · 60 shardsLinux x64 · 60 个分片
macOS arm64 · 4 shards· 4 个分片macOS arm64 · 4 个分片
Windows x64 · 8 shards· 8 个分片Windows x64 · 8 个分片
Windows arm64 · 8 shardsWindows arm64 · 8 个分片
✓ all 6 platforms green · build #54202 → merged✓ 所有 6 个平台均已通过 · 构建 #54202 → 已合并
Every CI build's test shards, by platform, across 135 builds that ran tests (420 mined from BuildKite). Bright green: every shard passed. Dim green: no failures, but the run was cut short (superseded). Red: at least one shard failed. Each lane is stamped when its full suite first passes — Linux's 60 shards were green almost a full day before Windows. Platforms kept wobbling red until the last failing tests fell; the final all-green build was #54202.这是每个 CI 构建的测试分片情况,按平台划分,涵盖了 135 个运行过测试的构建(从 BuildKite 中提取了 420 个)。亮绿色表示所有分片均通过。暗绿色表示没有失败,但运行被提前终止(被后续构建取代)。红色表示至少有一个分片失败。当每个通道的完整测试套件首次通过时,都会打上标记——Linux 的 60 个分片比 Windows 早了近一天变为绿色。各平台一直处于红色波动状态,直到最后失败的测试被修复;最终全绿的构建版本为 #54202。

The rest of the time leading up to merging it was straightforward. A workflow that looped on fixing CI test failures for each platform until there were no more test failures. Several workflows for Windows-related cleanup, to deduplicate code, to reduce unsafe usage, and to generally clean up some code.在此之后,直到合并前的过程都很顺利。工作流程主要是循环修复各平台的 CI 测试失败,直到不再有测试错误。此外还有几个针对 Windows 的清理工作流,用于消除重复代码、减少 unsafe 的使用,并对部分代码进行常规清理。

Merging the Rust rewrite合并 Rust 重写版本

Once 100% of Bun's test suite passed in CI on all platforms (and I manually verified the tests were in fact running and not being skipped), I ran a bunch of commands locally to test things - and then I pressed the merge button.当 Bun 的测试套件在所有平台上的 CI 中 100% 通过后(且我手动验证了测试确实在运行而非被跳过),我在本地运行了一系列命令进行测试,然后按下了合并按钮。

Merging into main isn't a versioned release. At this point, I was confident enough to move forward and commit to the rewrite, but not yet confident enough to release it.合并到主分支并不等同于版本发布。此时,我有足够的信心推进并提交这次重写,但还不足以将其正式发布。

Stats统计数据

At peak, we were running 4 of these workflows at once each in a separate worktree, each with 16 Claudes per workflow. About 64 Claudes at a time.高峰期,我们同时运行 4 个这样的工作流,每个工作流在独立的工作树中,每个工作流配备 16 个 Claude 实例。大约同时有 64 个 Claude 在工作。

git log · claude/phase-a-portgit 日志 · claude/phase-a-portpeak: 58 commits in one minute峰值:一分钟内提交 58 次
0
commits提交次数
+0
lines written, rewrites included编写的代码行数(含重写)
Mon, May 4, 7:05 AM PDT5 月 4 日,周一,太平洋夏令时间上午 7:05
 
 
 
All 6,502 commits (merges excluded), replayed. Pink粉色 bars are mostly new code; cyan青色 bars are mostly deletion. The line counter counts every rewrite along the way — the diff that landed was +1,009,272. The log is real commit messages.回放了全部 6,502 次提交(不含合并)。粉色条大多为新代码;青色条大多为删除的代码。行数计数器统计了整个过程中的每次重写——最终落地的差异代码量为 +1,009,272 行。日志使用的是真实的提交信息。

0 tests skipped or deleted0 个测试被跳过或删除

11 days (May 3 → merged May 14) · 6,778 commits11 天(5 月 3 日 → 5 月 14 日合并) · 6,778 次提交

Platformexpect() callsTestsFiles
Debian 13 x641,386,82660,6244,174
macOS 14 arm641,259,95358,8504,175
Windows 2019 x641,007,54457,3374,173

Pre-merge, this took 5.9 billion uncached input tokens, 690 million output tokens, and 72 billion cached input token reads — around $165,000 at API pricing. By hand, I think this would've taken 3 engineers with full context on the codebase about a year, during which time we wouldn't be able to improve Node.js compatibility, fix bugs, fix security issues or implement new features. We never would've done that. The realistic alternative was to do nothing and keep fixing the bugs at the top of this post forever.合并前,这一过程消耗了 59 亿个未缓存的输入 Token、6.9 亿个输出 Token 以及 720 亿次缓存输入 Token 读取——按 API 定价计算约为 165,000 美元。如果由人工完成,我认为需要 3 名对代码库有全面了解的工程师耗时约一年,而在此期间我们将无法改进 Node.js 兼容性、修复错误、解决安全问题或实现新功能。我们绝不会那样做。现实的选择只能是维持现状,永远修复本文开头提到的那些错误。

This is the bleeding edge of what's possible today. I used a pre-release version of Claude Fable 5, a Mythos-class model. Claude Code's dynamic workflows kept 64 Claudes running for 11 days (I would've had to write my own harness to pull this off otherwise).这是当今技术前沿的极限。我使用了 Claude Fable 5 的预发布版本,这是一款 Mythos 级别的模型。Claude Code 的动态工作流让 64 个 Claude 实例连续运行了 11 天(否则我必须自己编写工具链才能实现这一点)。

The work continues工作仍在继续

Since merging the Rust port, we've completed 11 rounds of security review from Claude Code Security and addressed the findings.自合并 Rust 移植版本以来,我们已经完成了 11 轮来自 Claude Code Security 的安全审查,并解决了发现的问题。

We've also added 24/7 coverage-guided fuzzing of every parser in Bun — JavaScript, TypeScript, JSX, CSS, JSON5, JSONC, TOML, YAML, Markdown, INI, Bun Shell scripts, semver ranges, .patch files, and CSS colors. The fuzzer automatically sends the bugs it finds to Claude to submit a PR reproducing & fixing, and humans review the PRs. So far, it's executed our parsers 100 billion times which has led to around 15 PRs.我们还为 Bun 中的每个解析器添加了全天候的覆盖率引导模糊测试(fuzzing)——包括 JavaScript、TypeScript、JSX、CSS、JSON5、JSONC、TOML、YAML、Markdown、INI、Bun Shell 脚本、semver 范围、.patch 文件和 CSS 颜色。模糊测试器会自动将其发现的错误发送给 Claude,以提交复现并修复该问题的 PR,随后由人工审核这些 PR。到目前为止,它已经执行了我们的解析器 1000 亿次,产生了约 15 个 PR。

At the time of writing, about 4% of Bun's Rust code sits inside an unsafe block (~13,000 unsafe keywords across ~27,000 lines / ~780,000 lines), and 78% of those blocks are a single line — a pointer that came from C++, or one call into a C library. I expect this number to go down over time as we refactor from a faithful Zig port (which had no greppable unsafe keyword) to idiomatic Rust, but we are going to continue using C & C++ libraries like JavaScriptCore so it will always have more unsafe than pure Rust projects.在撰写本文时,Bun 的 Rust 代码中约有 4% 位于 unsafe 代码块内(约 13,000 个 unsafe 关键字,分布在约 27,000 行代码中,总计约 780,000 行),其中 78% 的代码块只有一行——通常是指向来自 C++ 的指针,或者对 C 库的一次调用。随着我们从忠实的 Zig 移植版(没有可搜索的 unsafe 关键字)重构为地道的 Rust 代码,我预计这个数字会下降。但由于我们将继续使用 JavaScriptCore 等 C 和 C++ 库,因此它始终会比纯 Rust 项目包含更多的 unsafe 代码。

Porting mistakes移植错误

The focus of the Rust rewrite is stability, but it would be impossible to ship a massive change like this and introduce zero regressions.Rust 重写版的重点是稳定性,但要在发布如此巨大的变更时做到零回归是不可能的。

This rewrite introduced 19 known regressions, each of which has been fixed.此次重写引入了 19 个已知回归问题,目前均已修复。

Most of the regressions came from code that's syntactically identical in both languages but semantically different.大多数回归问题源于那些在两种语言中语法相同但语义不同的代码。

Side effect inside debug_assert!debug_assert! 内部的副作用

These two snippets look similar but behave differently. Zig's assert is a function, so its argument runs in every build. Rust's debug_assert! is a macro, so in release builds the whole expression is erased, including the insert_stale call.这两个片段看起来相似,但行为不同。Zig 的 assert 是一个函数,因此其参数在每次构建中都会运行。Rust 的 debug_assert! 是一个宏,所以在发布版本中整个表达式会被擦除,包括 insert_stale 调用。

// Zig:
if (dev.framework.react_fast_refresh) |rfr| {
    assert(try dev.client_graph.insertStale(rfr.import_source, false) == IncrementalGraph(.client).react_refresh_index);
}

// Rust:
if let Some(rfr) = &dev.framework.react_fast_refresh {
    debug_assert!(dev.client_graph.insert_stale(&rfr.import_source, false)? == react_refresh_index);
}

insert_stale adds a file to the frontend dev server's hot reload graph. In release builds it stopped running, and HMR broke in certain cases for projects with HTML routes that use React while a hot reloaded file gets invalidated: Cannot destructure property 'isLikelyComponentType' of 'k'. Debug builds worked. #30678insert_stale 会将文件添加到前端开发服务器的热重载图中。在发布版本中,它停止运行,导致某些项目在使用 React 的 HTML 路由时,当热重载文件失效时 HMR 崩溃:Cannot destructure property 'isLikelyComponentType' of 'k'。调试版本则工作正常。#30678

Slices of odd length奇数长度的切片

Bun's Zig helper reinterpretSlice(u16, bytes) (predating builtin casts supporting slices) used @divTrunc and ignored a trailing odd byte. bytemuck::cast_slice panics on it instead. Blob.text() on a UTF-16 byte order mark followed by an odd number of bytes stopped returning a string and panicked the process. We went back to ignoring the odd byte: &buf[..buf.len() & !1]. #31188Bun 的 Zig 辅助函数 reinterpretSlice(u16, bytes)(在内置转换支持切片之前)使用了 @divTrunc 并忽略了尾部的奇数字节。而 bytemuck::cast_slice 会对此引发恐慌(panic)。Blob.text() 在处理带有 UTF-16 字节顺序标记且后续字节数为奇数的情况时,停止返回字符串并导致进程崩溃。我们改回了忽略奇数字节的做法:&buf[..buf.len() & !1]。#31188

Bounds checks边界检查

On macOS & Linux, we compiled Bun's Zig code with ReleaseFast, which removes bounds checks. Rust's release builds keep them.在 macOS 和 Linux 上,我们使用 ReleaseFast 编译 Bun 的 Zig 代码,这会移除边界检查。而 Rust 的发布版本则保留了这些检查。

Bun's module resolver interns long filenames into a global list that spills into overflow blocks. The original Zig code sized each block at count / 4, or 2048. The port left a placeholder:Bun 的模块解析器会将长文件名驻留到一个全局列表中,该列表会溢出到溢出块中。原始 Zig 代码将每个块的大小设置为 count / 4,即 2048。移植版本留下了一个占位符:

/// ... so use a nonzero stand-in until Phase B threads the
/// per-instantiation value through.
pub const BSS_OVERFLOW_BLOCK_SIZE: usize = 64;

That lowered the ceiling from 8.4 million interned filenames to 270,272, which real projects hit, and made a ptrs[4095] off-by-one we ported from Zig reachable. Rust panicked instead of writing past the end. Zig would also panic in this case, if we used ReleaseSafe (we only did on Windows). #31503这使得驻留文件名的上限从 840 万降低到了 270,272,实际项目会触及此限制,并使得我们从 Zig 移植过来的 ptrs[4095] 越界错误变为可触发。Rust 会引发恐慌,而不是在末尾之后写入。如果我们使用 ReleaseSafe(仅在 Windows 上使用),Zig 在这种情况下也会引发恐慌。#31503

comptime format stringscomptime 格式字符串

Output.pretty rewrites <r> and <d> color markers into ANSI escapes. In Zig, fmt is comptime, so the markers are gone before the arguments are substituted. Rust functions don't have comptime parameters, so Output::pretty only ever saw the finished string, and rewrote markers over the arguments too.Output.pretty 将 <r> 和 <d> 颜色标记重写为 ANSI 转义符。在 Zig 中,fmt 是 comptime 的,因此标记在参数替换之前就已经消失了。Rust 函数没有 comptime 参数,所以 Output::pretty 只能看到完整的字符串,并把参数中的标记也重写了。

// Zig:
pub inline fn pretty(comptime fmt: string, args: anytype) void;
Output.pretty("<r>{f}<r>", .{hyperlink});

// Rust:
pub fn pretty(payload: impl PrettyFmtInput);
Output::pretty(format_args!("<r>{}<r>", hyperlink));

bun update -i prints package names as OSC 8 hyperlinks, terminated by ESC \. That backslash sits right before the < of the trailing <r>, the marker parser eats it, and the r prints as text.bun update -i 将包名打印为 OSC 8 超链接,以 ESC \ 结尾。那个反斜杠正好位于尾部 <r> 的 < 之前,标记解析器吃掉了它,导致 r 被作为文本打印出来。

it should say oxfmt, not oxfmtr它应该显示为 oxfmt,而不是 oxfmtr

In Rust it has to be a macro: bun_core::pretty!("<r>{}<r>", hyperlink). #30693在 Rust 中,它必须是一个宏:bun_core::pretty!("<r>{}<r>", hyperlink)。#30693

Bun is better in RustRust 版的 Bun 更出色

So far, Bun v1.4.0 fixes 128 bugs that reproduce in v1.3.14. These range from memory leaks to crashes to miscolored help text.到目前为止,Bun v1.4.0 修复了 128 个在 v1.3.14 中复现的错误。范围涵盖从内存泄漏到崩溃,再到帮助文本颜色错误等。

Reduced memory usage降低内存占用

Rust has a powerful language-level tool for cleaning up memory: Drop. When Drop is implemented, the drop function is automatically called every time the value goes out of scope.Rust 拥有一种强大的语言级内存清理工具:Drop。当实现了 Drop 特性时,每当值超出作用域,drop 函数就会自动调用。

impl Drop for Bytes {
    fn drop(&mut self) {
        if !self.pinned.is_empty() {
            JSC__JSValue__unpinArrayBuffer(self.pinned);
        }
    }
}

In Zig, defer can be used to run code at the end of a scope:在 Zig 中,可以使用 defer 在作用域末尾运行代码:

const bytes: ArrayBuffer = try .fromPinned(global, value);
defer bytes.unpin();

In Zig, defer needs to be added to every individual call site that might need cleanup. It's easy to end up forgetting to clean up (a memory leak), or to run cleanup code twice in rarely-reached error handling code (a double-free). In Rust, Drop runs automatically when the value is no longer accessible - trading "no hidden control flow" for preventing a common footgun.在 Zig 中,必须在每个可能需要清理的调用点手动添加 defer。很容易忘记清理(导致内存泄漏),或者在极少执行的错误处理代码中重复运行清理代码(导致重复释放)。在 Rust 中,当值不再可访问时,Drop 会自动运行——这以放弃“无隐藏控制流”为代价,防止了常见的编程陷阱。

Drop fixed several memory leaks in Bun related to file paths in error handling code.Drop 修复了 Bun 中与错误处理代码中的文件路径相关的多个内存泄漏问题。

We fixed every instrumentable memory leak我们修复了所有可检测的内存泄漏

We improved Bun's LeakSanitizer integration to track all native code memory allocations.我们改进了 Bun 对 LeakSanitizer 的集成,以跟踪所有原生代码的内存分配。

Here's an example: every in-process Bun.build() call leaked several megabytes of memory — parsed source text and AST symbol tables that outlived the build they belonged to.举个例子:每个进程内的 Bun.build() 调用都会泄漏数兆字节的内存——解析后的源文本和 AST 符号表超出了其所属构建的生命周期。

// Bundle the same 60-module project 2,000 times in one process
for (let i = 0; i < 2_000; i++) {
  await Bun.build({
    entrypoints: ["./index.js"],
    minify: true,
    sourcemap: "external",
  });
}

In Bun v1.3.14, every build leaks about 3 MB, forever — tools like dev servers that bundle on every request eventually run out of memory. In Bun v1.4.0, memory levels off:在 Bun v1.3.14 中,每次构建都会永久泄漏约 3 MB 内存——像在每次请求时进行打包的开发服务器等工具最终会耗尽内存。在 Bun v1.4.0 中,内存使用趋于平稳:

BuildsBun v1.3.14Bun v1.4.0
5001,914 MB526 MB
1,0003,506 MB586 MB
1,5005,097 MB608 MB
2,0006,745 MB609 MB

A previous attempt to do this in Zig was not merged because the lack of an equivalent of Drop made it more difficult to feel confident merging.之前在 Zig 中尝试这样做时并未被合并,因为缺乏类似 Drop 的机制使得我们难以对合并充满信心。

Smaller binary size更小的二进制体积

The initial changes in the Rust rewrite reduced binary size by 3.8 MB on Windows, 5.5 MB on macOS, and 6.8 MB on Linux. This is largely because we used too much comptime in our Zig code.Rust 重写版的初步变更使 Windows 上的二进制体积减少了 3.8 MB,macOS 上减少了 5.5 MB,Linux 上减少了 6.8 MB。这在很大程度上是因为我们在 Zig 代码中过度使用了 comptime。

After that initial shrinkage, the team explored more opportunities for binary size reduction using linker optimizations like Identical Code Folding, removing unused data from ICU, and lazily decompressing small parts of libicu with a zstd dictionary on-demand.在初步缩减后,团队探索了更多通过链接器优化来减小体积的机会,例如“相同代码折叠”(Identical Code Folding)、从 ICU 中移除未使用的数据,以及使用 zstd 字典按需延迟解压 libicu 的小部分内容。

Combined with the Rust rewrite, ICU changes, and identical code folding, Bun's binary size shrinks by ~20% on Linux & Windows.结合 Rust 重写、ICU 变更和相同代码折叠,Bun 的二进制体积在 Linux 和 Windows 上缩减了约 20%。

VersionPlatformSize
Bun v1.4.0 (canary)Windows76 MB
Bun v1.3.14Windows94 MB
Bun v1.4.0 (canary)Linux70 MB
Bun v1.3.14Linux88 MB

Reduced stack space usage降低栈空间使用

The TOML parser, and all of the other recursive-descent parsers in Bun (JSON, YAML, JavaScript, TypeScript, and more) now use less stack space.TOML 解析器以及 Bun 中的所有其他递归下降解析器(JSON、YAML、JavaScript、TypeScript 等)现在使用的栈空间更少了。

This caused some test failures before merging the Rust rewrite:在合并 Rust 重写版之前,这导致了一些测试失败:

bun test v1.3.14-canary.1 (e99311e58)
.......

105 | });
106 |
107 | it("Bun.TOML.parse throws on deeply nested inline tables instead of crashing", () => {
108 |   const depth = 25_000;
109 |   const deepToml = "a = " + "{ b = ".repeat(depth) + "1" + " }".repeat(depth);
110 |   expect(() => Bun.TOML.parse(deepToml)).toThrow(RangeError);
                                               ^
error: expect(received).toThrow(expected)

Expected constructor: RangeError

Received function did not throw
Received value: {
  a: {
    b: {
      b: {
        b: {
          b: {
            b: {
              b: {
                b: {
                  b: [Object ...],
                },
              },
            },
          },
        },
      },
    },
  },
}

      at <anonymous> (/var/lib/buildkite-agent/build/test/js/bun/resolve/toml/toml.test.js:110:42)

✗ Bun.TOML.parse throws on deeply nested inline tables instead of crashing [2907.64ms]

Rust's LLVM IR codegen emits LLVM's llvm.lifetime.start and llvm.lifetime.end intrinsics for stack variables when they are no longer in use, which lets LLVM reuse stack space slots. This lets large functions with nested scopes use significantly less stack space.Rust 的 LLVM IR 代码生成会在栈变量不再使用时发出 LLVM 的 llvm.lifetime.start 和 llvm.lifetime.end 内在函数,这允许 LLVM 重用栈空间槽位。这使得具有嵌套作用域的大型函数能显著减少栈空间使用。

Previously, we manually worked around an open issue by refactoring particularly large functions into many smaller functions.此前,我们通过将特别大的函数重构为许多较小的函数来手动规避这一悬而未决的问题。

2% - 5% faster速度提升 2% - 5%

Rust supports cross-language link-time optimization between C/C++ and Rust, which enables inlining across programming languages (how cool is that!!).Rust 支持 C/C++ 与 Rust 之间的跨语言链接时优化,这实现了跨编程语言的内联(这不是很酷吗!!)。

We benchmarked Bun v1.3.14 against Bun v1.4.0 on Linux x64 (EC2, Xeon Platinum 8488C). HTTP throughput measured with oha against hello-world servers, app workloads measured with hyperfine.我们在 Linux x64(EC2,Xeon Platinum 8488C)上对 Bun v1.3.14 和 Bun v1.4.0 进行了基准测试。使用 oha 对 hello-world 服务器进行 HTTP 吞吐量测量,使用 hyperfine 对应用工作负载进行测量。

HTTP throughput (req/s, avg of 3 rounds)HTTP 吞吐量(每秒请求数,3 轮平均值)

serverBun v1.3.14Bun v1.4.0Δ
Bun.serve169.6k177.7k+4.8%
node:http103.8k108.5k+4.5%
Elysia158.9k163.3k+2.8%
express64.5k66.6k+3.2%
fastify91.5k95.9k+4.8%

Apps / CLI (hyperfine)应用 / CLI(hyperfine)

workloadBun v1.3.14Bun v1.4.0Δ
next build13.62 s13.03 s+4.5%
vite build (tsc + vite)1.69 s1.65 s+2.2%
tsc -b --force0.94 s0.89 s+4.7%

Production生产环境

Prisma launched the Prisma Compute public beta on Bun's Rust rewrite.Prisma 在 Bun 的 Rust 重写版上发布了 Prisma Compute 公测版。

"We ran into memory leaks and a connection pool that couldn't recover after a VM was paused and resumed. When the Rust rewrite appeared, we tested it against the same failure modes. It handled them perfectly." - Alexey Orlenko“我们遇到了内存泄漏和连接池在虚拟机暂停并恢复后无法恢复的问题。当 Rust 重写版出现时,我们针对同样的故障模式进行了测试。它处理得非常完美。” - Alexey Orlenko

Claude Code v2.1.181 (released June 17th) and later use the Rust port of Bun. Startup got 10% faster on Linux but otherwise, barely anyone noticed. Boring is good.Claude Code v2.1.181(6 月 17 日发布)及更高版本使用了 Rust 版的 Bun。Linux 上的启动速度提高了 10%,但除此之外,几乎没人注意到区别。平淡即是好。

Claude Code startup time from production telemetry (Linux p50): v2.1.179 at 517ms vs v2.1.181, the first release on Rust Bun, at 464ms — 10% faster

Shipping发布

Bun v1.3.14 was the last version of Bun written in Zig. Bun v1.4.0 will be the first version of Bun written in Rust. It's available in canary now - please report any issues you find:Bun v1.3.14 是最后一个用 Zig 编写的 Bun 版本。Bun v1.4.0 将是第一个用 Rust 编写的 Bun 版本。它现在已在 canary 通道中提供——请报告您发现的任何问题:

bun upgrade --canary

Maintainability可维护性

For myself and the team, our new Rust codebase feels very similar to the old Zig codebase. For example, here's a snippet of the original Zig code and the new Rust code:对于我和团队来说,新的 Rust 代码库感觉与旧的 Zig 代码库非常相似。例如,这是原始 Zig 代码和新 Rust 代码的片段:

pub fn canMergeSymbols(
    scope: *Scope,
    existing: Symbol.Kind,
    new: Symbol.Kind,
    comptime is_typescript_enabled: bool,
) SymbolMergeResult {
    if (existing == .unbound) {
        return .replace_with_new;
    }

    if (comptime is_typescript_enabled) {
        // In TypeScript, imports are allowed to silently collide with symbols within
        // the module. Presumably this is because the imports may be type-only:
        //
        //   import {Foo} from 'bar'
        //   class Foo {}
        //
        if (existing == .import) {
            return .replace_with_new;
        }

        // ...
    }

    // ...
}
pub fn can_merge_symbol_kinds<const IS_TYPESCRIPT_ENABLED: bool>(
    scope_kind: Kind,
    existing: symbol::Kind,
    new: symbol::Kind,
) -> SymbolMergeResult {

    if existing == symbol::Kind::Unbound {
        return SymbolMergeResult::ReplaceWithNew;
    }

    if IS_TYPESCRIPT_ENABLED {
        // In TypeScript, imports are allowed to silently collide with symbols within
        // the module. Presumably this is because the imports may be type-only:
        //
        //   import {Foo} from 'bar'
        //   class Foo {}
        //
        if existing == symbol::Kind::Import {
            return SymbolMergeResult::ReplaceWithNew;
        }

        // ...
    }

    // ...
}

Anyone who understands the original Zig code understands the mechanically translated Rust code. I reviewed the original Rust rewrite PR by checking the adversarial code review agents were correctly catching discrepancies between the Zig code and the Rust code, that they were ensuring the porting guide and lifetime guide were being followed, and also manually reading a lot of the code myself side-by-side with the Zig vs Rust.任何理解原始 Zig 代码的人都能理解机械翻译后的 Rust 代码。我通过检查对抗性代码审查代理是否正确捕捉到了 Zig 代码和 Rust 代码之间的差异,确保移植指南和生命周期指南得到遵循,并亲自手动逐行对比阅读了大量代码,从而审查了原始的 Rust 重写 PR。

What's next未来展望

Bun v1.4 makes Bun faster, smaller, use less memory and gives the team incredibly powerful tools for systematically improving stability going forward: Rust's borrow checker, Miri (which runs for a growing chunk of code in CI), LeakSanitizer, and 24/7 coverage-guided fuzzing for parsers. There's still more to refactor, but things are off to a great start.Bun v1.4 让 Bun 更快、更小、内存占用更低,并为团队提供了极其强大的工具,以便在未来系统性地提高稳定性:Rust 的借用检查器、Miri(在 CI 中运行越来越多的代码)、LeakSanitizer 以及全天候的解析器覆盖率引导模糊测试。还有更多地方需要重构,但目前一切都已有一个良好的开端。

This Rust rewrite would've taken a team of engineers with full-context on the codebase a year of work. With 1 engineer using Fable & closely monitoring Claude Code, we went from start to 100% of the test suite passing on all platforms in 11 days.这次 Rust 重写如果由一个对代码库有全面了解的工程师团队来完成,需要一年的工作量。而通过 1 名工程师使用 Fable 并密切监控 Claude Code,我们仅用了 11 天就从零开始实现了所有平台测试套件的 100% 通过。

One engineer can do a lot more today than a year ago.今天,一名工程师所能完成的工作远超一年前。