AI 编程 4.0 · 优秀 2026-07-20 · 文章

Rewriting Bun in Rust

Bun 团队复盘用预发布 Claude Fable 5 和 Claude Code dynamic workflows,在 11 天内把 1,448 个 Zig 文件约 53.5 万行主体迁到 Rust文章的价值不在AI 写代码很快,而在生产流程:实现 agent 不审查自己的代码,两个独立 review agent 找 bug,编译错误测试失败和 CI 失败都进入 work queue,最后 6 个平台 CI 全绿才合并

打开原文回到归档

Rewriting Bun in Rust

  • source_url: https://bun.com/blog/bun-in-rust
  • source_type: article
  • platform: blog
  • author: Bun Team
  • original_date: 2026-07-20
  • added_date: 2026-07-20
  • local_path: OpenClaw定时任务/ClawFeed24小时高价值一览/2026-07-20-ClawFeed24小时高价值一览.md
  • quality_score: 4

摘要(中文)

Bun 团队复盘用预发布 Claude Fable 5 和 Claude Code dynamic workflows,在 11 天内把 1,448 个 Zig 文件约 53.5 万行主体迁到 Rust文章的价值不在AI 写代码很快,而在生产流程:实现 agent 不审查自己的代码,两个独立 review agent 找 bug,编译错误测试失败和 CI 失败都进入 work queue,最后 6 个平台 CI 全绿才合并

Summary (English)

The Bun team describes an 11-day, agent-assisted Rust rewrite with dynamic workflows, separate implementation and adversarial review agents, CI feedback loops and detailed failure cases.

One-liner

Bun 迁 Rust 是生产级 coding agent 工程样板,重点在审查和失败回收

原文 / 元数据抓取

Rewriting Bun in Rust

原文链接: https://bun.com/blog/bun-in-rust

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 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.

From the start, Bun's scope was massive:

  • JavaScript, TypeScript, and CSS transpiler, minifier, and bundler
  • npm-compatible package manager
  • Jest-like test runner
  • Node.js & TypeScript-compatible module resolution
  • HTTP/1.1 & WebSocket client
  • Node.js API implementations like fs, net, tls, and dozens of other modules

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.

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's scope has also been a challenge for stability. Here's a small sample of bugs we fixed in Bun v1.3.14:

  • 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
  • use-after-free crash in node:zlib when an onerror callback issued a re-entrant write() followed by close() on native handles
  • 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
  • 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
  • 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
  • heap out-of-bounds write in UDPSocket.sendMany() when the socket's connection state changed mid-iteration via user JS callbacks
  • memory leak in crypto.scrypt where the callback and protected password/salt buffers were never released when the output buffer allocation failed
  • SSLWrapper.init leaked the strdup'd passphrase on error paths
  • 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_SESSION
  • 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 root
  • double-free crash in the CSS parser when background-clip had vendor prefixes and multi-layer backgrounds
  • DuplexUpgradeContext was never freed — a full leak per 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 MessagePort

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.

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.
  • We ship Zig safety-checked ReleaseSafe builds on Windows
  • We fuzz Bun's runtime APIs 24/7 using Fuzzilli, the JavaScript engine fuzzer used by V8 & JavaScriptCore
  • 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.

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.

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?

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).

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.

| Language | Cleanup | | --- | --- | | Zig | defer, errdefer | | C++ | ~Destructor, &&Move | | Rust | Drop |

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:

  • 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)
  • 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.

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:

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:

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

What about C/C++?

About 20% of Bun's code is written in C++ and Bun embeds several C/C++ libraries:

  • JavaScriptCore, the JavaScript engine that powers Safari
  • uWebSockets & usockets - our HTTP/WebSocket server, and event loop
  • lshpack & lsquic - HPACK and HTTP/3 libraries
  • BoringSSL, Google's OpenSSL fork
  • SQLite

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.

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.

Why 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.

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.

Fortunately, Bun's own test suite is written in TypeScript which means it doesn't depend on the runtime's programming language.

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.

But honestly, I didn't want to do it. Homegrown smart pointers offer worse ergonomics than Rust, with none of the guarantees.

What if, instead, I spend a week testing if Anthropic's new model can rewrite Bun in Rust?

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".

Claude, rewrite Bun in Rust.

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.

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.

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?

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.

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);
}
`

## Obsidian intake evidence excerpt

# ClawFeed 24小时高价值一览 · 2026-07-20
- status: completed
- Obsidian: /Users/gracker/Library/Mobile Documents/iCloud~md~obsidian/Documents/Obsidian/OpenClaw定时任务/ClawFeed24小时高价值一览/2026-07-20-ClawFeed24小时高价值一览.md

任务信息:
- 任务名称:ClawFeed 24小时高价值一览(For You+Bookmarks)
- 处理数量:候选 65 篇,认真阅读 10 篇,入选 3 篇
- 数据源:OpenCLI Hacker News top、DuckDuckGo 开发者生态/AI 工具检索、OpenCLI web read、OpenCLI Twitter thread
- 落盘路径:/Users/gracker/Library/Mobile Documents/iCloud~md~obsidian/Documents/Obsidian/OpenClaw定时任务/ClawFeed24小时高价值一览/2026-07-20-ClawFeed24小时高价值一览.md
- 验证状态:已落盘且非空

可发布正文如下:

## 今日精选

1. Bun 用 11 天把 53.5 万行 Zig 主体迁到 Rust,这篇不是“AI 写代码真快”的热闹,而是一份可复用的大规模 agent 工程记录:50 个动态工作流、64 个 Claude 并行、双 adversarial reviewer、6,502 个提交、全平台 CI 绿灯后才合并。
2. MCP 2026-07-28 release candidate 把协议往生产环境推了一大步:去掉协议层 session,改成每个请求携带版本和能力信息;授权对齐 OAuth 2.1、RFC 9728、RFC 8707;长任务和 Apps 进入扩展机制。
3. OpenAI Frontier 的信息量在于企业 agent 平台形态:共享业务上下文、agent 执行环境、评估优化、身份权限边界,加上 FDE 进入企业现场,把模型能力、组织流程和权限治理放在同一个产品里。

- 标题:Rewriting Bun in Rust
  评分:9.3/10
  推荐语:这篇最值得看的是工程细节,不是结论。作者把 1,448 个 Zig 文件迁到 Rust 的过程拆成 porting guide、lifetimes.tsv、实现 agent、两个独立审查 agent、修复 agent、CI 失败回收循环,并给出真实的失败案例:`git stash`/`git reset` 互相踩、agent 为了编译加 stub、`unwrap_or` eager evaluation 造成 panic、libuv async close 触发 UAF/double-free。
  摘要:Bun 团队用预发布 Claude Fable 5 和 Claude Code dynamic workflows,在 11 天内完成 Rust 迁移,最终 6 个平台 CI 全绿,0 个测试被跳过或删除。文章给出迁移成本和收益:5.9B uncached input tokens、690M output tokens、约 16.5 万美元 API 成本,换来 128 个已复现 bug 修复、`Bun.build()` 内存泄漏收敛、Linux/Windows binary 约 20% 变小、若干 workload 2%–5% 提速。
  链接:https://bun.com/blog/bun-in-rust

- 标题:The biggest MCP spec update ships July 28: What changes for AI agent authentication
  评分:8.5/10
  推荐语:这篇适合正在跑 MCP server 的人读,因为它把 release candidate 里的破坏性变化翻成迁移清单。最有用的点是去 session 后的部署模型变化:不用 sticky session 和共享 session store,网关可以按 `Mcp-Method` / `Mcp-Name` header 路由;需要状态的应用改用显式 handle,比如 `basket_id`、`browser_id`。
  摘要:MCP 2026-07-28 候选规范移除 `initialize`/`initialized` handshake 和 `Mcp-Session-Id`,每次请求携带协议版本、client info 和 capability,并通过 `server/discover` 拉取服务端能力。授权部分补上 OAuth 2.1 resource server、Protected Resource Metadata、Resource Indicators、issuer verification、refresh token 行为和 client application type,解决多 MCP server 场景下的 token 混用风险。
  链接:https://workos.com/blog/mcp-2026-spec-agent-authentication

- 标题:Introducing OpenAI Frontier
  评分:8.1/10
  推荐语:这篇虽然是产品发布,但给出了 OpenAI 对企业 agent 平台的分层判断:业务上下文、执行环境、评估优化、身份权限边界必须一起出现。案例也比普通发布稿更具体:制造业生产优化从 6 周压到 1 天,硬件测试失败的 root-cause identification 从约 4 小时降到几分钟。
  摘要:Frontier 把企业 agent 当“AI coworker”管理:接入数据仓库、CRM、工单和内部应用,提供跨本地环境、企业云和 OpenAI-hosted runtime 的执行环境,并用评估反馈让 agent 在真实工作里改进。它的战略信号是 OpenAI 不只卖模型和 API,而是在争夺企业 agent 的上下文层、运行时、权限治理和现场交付入口。
  链接:https://openai.com/index/introducing-openai-frontier/

## 可直接发布文案

今天最值得读的是 Bun 迁 Rust 的复盘。它不像普通“AI 写代码”故事,细节很硬:1,448 个 Zig 文件、约 53.5 万行原代码、50 个动态工作流、最多 64 个 Claude 并行跑 11 天,最后 6 个平台 CI 全绿才合并。

有参考价值的是它的工作流设计:实现 agent 不审查自己的代码,另外两个 Claude 只负责找 bug;编译错误、测试失败、CI 失败都变成 work queue;agent 一旦开始用 stub 糊编译,直接改 prompt 让 reviewer 拒绝这种做法。

这类项目能跑通,不靠“把需求丢给模型”,靠的是测试套件、隔离、审查角色、失败回收和人盯关键节点。以后评价 coding agent,不能只看 demo,要看它能不能进入这种生产循环。

https://bun.com/blog/bun-in-rust

## 备选短文案

- Bun 迁 Rust 这篇很适合当 agent 工程样板看:64 个 Claude 并行不是重点,重点是实现、审查、修复、CI 回收被拆成了可重复的循环。
- MCP 7 月新规范的方向很清楚:少一点隐藏 session,多一点显式状态、标准授权和网关可治理性。生产环境跑 MCP server 的团队该提前看迁移点。
- OpenAI Frontier 的信号是:企业 agent 平台竞争不在聊天框,而在业务上下文、执行环境、评估、权限边界和现场交付。