← 返回 Siami 首頁

Bun 正式以 Rust 改寫並合併:11 天、64 個 Claude 並行、128 個 Bug 修復

▲ 368 💬 198
Bun 正式以 Rust 改寫並合併:11 天、64 個 Claude 並行、128 個 Bug 修復

編按:本文綜合整理自 Bun 官方部落格、Prisma 工程部落格、DevClass 報導 與 Anthropic 收購公告,並加入 Siami 編輯部觀點與分析。

JavaScript 工具鏈生態出現重大變化。Bun 創辦人 Jarred Sumner 於 2026 年 7 月 8 日正式發表部落格,揭露整個 runtime 已從 Zig 全面移植到 Rust,並以 Bun v1.4.0 形式進入 canary 頻道。這次重寫並非循序漸進的函式庫替換,而是把 53 萬 5,496 行的 Zig 程式碼,在 11 天內由 Claude 模型機械式翻譯成 Rust,並通過全部跨平台測試套件。

整個專案最令人震驚的數字是:64 個 Claude 實例並行運作,總共消耗 59 億 uncached input tokens、6.9 億 output tokens、720 億 cached input token reads,光 API 成本就大約 16.5 萬美元。這是 Anthropic 於 2025 年 12 月收購 Bun 後,第一次向外界完整展示「Mythos 等級模型可承擔多大工程任務」的具體案例。


為什麼放棄 Zig:穩定性長期困擾

Jarred 在文中坦承,Bun 1.3.14 修不完的 bug 並非 Zig 語言本身的問題,而是「手動記憶體管理遇上垃圾回收物件」這類混合場景本來就難。Zig 沒有 constructor 與 destructor,多數清理必須在每個呼叫端用 defer 顯式寫出。

團隊其實做了很多努力:為 Zig 編譯器加上 AddressSanitizer 支援、每個 commit 都跑 ASAN、Windows 版本用 ReleaseSafe 安全建置、24/7 用 Fuzzilli 對 runtime API 模糊測試、寫了大量 end-to-end 記憶體洩漏測試。但 v1.3.14 仍持續出現 use-after-free、double-free、忘記在 error path 釋放記憶體這類錯誤。

每一個記憶體配置都得逐行 review:這塊 byte 什麼時候釋放?如何確保只釋放一次?JavaScript 例外有沒有正確處理?這個指標是 GC 看得見的保守 stack 物件,還是手動管理的記憶體?

文中列出的 bug 清單包括 node:zlib 在執行緒池中呼叫 .reset() 時的 heap-use-after-free、UDPSocket.sendMany() 中 valueOf() 回呼可以中途 detaching ArrayBuffer 導致 out-of-bounds write、tlsSocket.setSession() 每次呼叫洩漏一個 SSL_SESSION(約 6.5 KB)、fs.watch() 因為 reference count underflow 永遠無法被 GC 等。這些都是 Zig 風格下手動生命週期管理的典型坑。


為什麼選 Rust,而不是 C++

Bun 約 20% 程式碼是 C++,內嵌 JavaScriptCore、uWebSockets、usockets、lshpack、lsquic、BoringSQL、SQLite 等函式庫。Jarred 考慮過三條路:

  • C++:能拿到 RAII,可以砍掉很多 extern C 樣板,但記憶體損壞與洩漏還是會發生,仍然要靠 style guide 與 code review 強制執行。
  • 自己實作 Rust 風格的 smart pointer:沒編譯器保證,只是 ergonomics 變差的 Rust。
  • 直接用 Rust:借用檢查器把 use-after-free 變成編譯期錯誤,Drop 自動處理清理。debug_assert! 不會在 release build 中被擦掉導致行為差異(Zig 的 assert 在每個 build 都會執行參數,這是後面會提到的「HMR 在 release 壞掉」的根因)。

關鍵決策是:歷史經驗告訴我們,rewrite 通常是糟糕的主意。但 Bun 自己的測試套件是用 TypeScript 寫的,跟 runtime 語言無關,這讓「機械式移植、用相同測試套件驗證」變得可行。Jarred 最後一週測試 Claude 能不能做這件事,結果「幾天內高比例測試開始通過」,他立刻決定合入。


Claude,rewrite Bun in Rust:流程與方法論

Jarred 用的不是「丟一句 prompt 給 Claude 然後祈禱」,而是一套可重現的工程流程。核心概念是把工程工作視為「會自動重跑的迴圈」:

while ((task = todoList.pop())) {
    const result = task();
    const feedback = await Promise.all([review(result), review(result)]);
    await apply(feedback, result);
}

他用 Claude Code 跑了約 50 個 dynamic workflows,連續 11 天不中斷,涵蓋:

  • 把 Zig 模式/型別對應到 Rust 模式的 porting guide
  • 把 1,448 個 .zig 檔機械式翻譯成 .rs,對應 LIFETIMES.tsv
  • 修每個 crate 的編譯錯誤
  • 讓 bun test、bun build 等子指令能跑
  • 跑過整個 Bun 測試套件
  • 幾輪大規模 refactor 與清理

每個 workflow 都是迴圈形式,Jarred 在 11 天內的工作是監看 workflow 輸出、讀差異、提示 Claude 改迴圈參數。

對抗式審查(Adversarial Review)

合併一百萬行程式碼到主分支,需要的不是更多人類 reviewer,而是「第二個 Claude 在獨立 context window 裡專門找錯」:

  • 1 個 implementer Claude(拿到 Zig 原文、port plan、自己的推理)
  • 2 個以上 adversarial reviewer Claude(只拿到 diff,被告知「假設這份程式碼是錯的,找理由說明為什麼不 work」)
  • reviewer 不寫程式、implementer 不審別人的 code

文中展示了 3 個 reviewer 抓到的真實 bug,全部能編譯成功、看起來都正確,但都有致命缺陷:

  1. js_bun_spawn_bindings.rs:uv_close 是非同步的,libuv 會在下一個 loop tick 才呼叫 on_pipe_close 釋放資源,但 pipe: Box<uv::Pipe> 在 match arm 結束時就被 drop 掉,導致 libuv 拿到已釋放的記憶體,然後 callback 又釋放第二次。修法是 Box::leak(pipe)。
  2. node_fs.rs:負數非整數時間(例如 1970 年之前的檔案 mtime)用 trunc() 會產生負 nsec 的 timespec,應該用 floor()。
  3. color.rs:unwrap_or(1.0 - second.percentage.unwrap()) 是 eager evaluation,當 first.percentage 是 Some 時,括號裡的 second.percentage.unwrap() 已經先 panic 了。應該用 lazy 的 unwrap_or_else。

每個 bug 的 commit message 開頭都帶有審查者標記(win-review 或 crossplat review),這套記帳機制讓 Jarred 能事後審查 reviewer 真的有效。


11 天內的完整時間軸

部落格放了一個可重播的時間軸視覺化,記錄了從 phase-a 起草 batch head-1(100 個檔案)一路到最後 phase-e harvest 的所有 commit。摘要如下:

  • 5 月 3 日:實驗開始
  • 5 月 14 日:Rust port 合併到主分支
  • 6,502 commits(已排除 merge commit)
  • 最終 diff 淨新增 +1,009,272 行
  • 0 tests skipped or deleted
  • 3 平台測試覆蓋:Debian 13 x64(138 萬 expect calls)、macOS 14 arm64(126 萬)、Windows 2019 x64(100 萬)

合併後還完成 11 輪 Claude Code Security 審查,並對每個 parser 加上 24/7 coverage-guided 模糊測試(JavaScript、TypeScript、JSX、CSS、JSON5、JSONC、TOML、YAML、Markdown、INI、Bun Shell、semver、.patch 檔、CSS 顏色)。fuzzer 找到 bug 後會自動派給 Claude 開 PR 修復,人類再 review PR。目前 fuzzer 已經執行 parser 1,000 億次,產出約 15 個 PR。


移植過程踩到的 19 個迴歸

Rust port 主軸是穩定性,但 100 萬行的變更不可能零迴歸。文中詳列了幾個經典案例:

debug_assert! 的副作用陷阱

Zig 的 assert 是函式,參數在每個 build 都會執行;Rust 的 debug_assert! 是 macro,release build 中整個 expression(包括 insert_stale 呼叫)會被擦掉。Bun v1.3.14 用 Zig 寫時 HMR 正常,v1.4.0 release build 跑出 Cannot destructure property 'isLikelyComponentType' of 'k',debug build 卻沒事。修法是不要把有副作用的呼叫放進 debug_assert!。

奇數長度 slice panic

Zig 的 reinterpretSlice(u16, bytes) 用 @divTrunc 並忽略尾端奇數 byte。Rust 端的 bytemuck::cast_slice 對奇數長度會 panic。Blob.text() 在處理 UTF-16 BOM 加奇數 byte 時直接 crash 程序。修法改回忽略尾端 byte:buf[..buf.len() & !1]。

Bounds check 從 ReleaseFast 換成 Rust release

macOS/Linux 的 Bun Zig 版本用 ReleaseFast(移除 bounds check)。Rust release 保留 bounds check。Bun 的 module resolver 原本把長檔名 intern 到 global list,每個 overflow block 預設 2,048 個項目。移植時留了 placeholder BSS_OVERFLOW_BLOCK_SIZE: usize = 64,把上限從 840 萬個檔名砍到 27 萬,真實專案馬上踩到,連帶把 Zig 時代就存在的 ptrs[4095] off-by-one 從「寫到結尾外面」變成「Rust panic」。

comptime 格式字串

Output.pretty 在 Zig 端 comptime fmt 會在編譯期把顏色 marker 替換掉。Rust 沒有 comptime 參數,Output::pretty 只看到完成字串,marker parser 把 \r 後面的字母也吃進去。bun update -i 印出 oxfmt 時變成 oxfmtr。修法是把呼叫端從函式改成 macro。


Bun 1.4.0 的實質改善

移植不是純粹換語言,v1.4.0 同步修掉 128 個 v1.3.14 能重現的 bug,並帶來幾項可量測的改進:

記憶體不再無限成長

for (let i = 0; i < 2_000; i++) {
    await Bun.build({
        entrypoints: ["./index.js"],
        minify: true,
        sourcemap: "external",
    });
}
  • v1.3.14:每個 build 洩漏約 3 MB,跑 2,000 次後達到 6,745 MB
  • v1.4.0:跑 2,000 次後穩定在 609 MB

之前 Zig 嘗試修這條路徑時,因為沒有 Drop,合併信心不足而放棄。Rust 的 Drop 自動處理,順手把 Bun v1.3.14 所有可被 LeakSanitizer 追蹤的 native 記憶體洩漏都修完。

二進位檔大小

Rust 移植本身就把 binary 縮小 3.8 MB(Windows)、5.5 MB(macOS)、6.8 MB(Linux)。後續團隊又做了 Identical Code Folding、ICU 未用資料移除、用 zstd dictionary 懶載入 libicu 片段。最終成果:

版本平台大小
Bun v1.3.14Windows94 MB
Bun v1.4.0 (canary)Windows76 MB
Bun v1.3.14Linux88 MB
Bun v1.4.0 (canary)Linux70 MB

整體在 Linux/Windows 約縮小 20%。

Stack 使用量減少

TOML 與所有遞迴下降 parser(JSON、YAML、JavaScript、TypeScript 等)現在用更少 stack。原因是 Rust 的 LLVM IR codegen 會在 stack 變數不再使用時插入 llvm.lifetime.start/llvm.lifetime.end intrinsics,讓 LLVM 回收 stack slot。原本 Bun 手動把過大的函式拆成多個小函式來閃這個問題,現在不用了。

2%-5% 效能提升

Rust 支援 C/C++ 與 Rust 之間的 cross-language link-time optimization,這讓跨語言 inlining 成為可能(這在以前是不可能的)。EC2 Xeon Platinum 8488C 上 oha 與 hyperfine 的基準:

場景v1.3.14v1.4.0變化
Bun.serve (req/s)169.6k177.7k+4.8%
node:http (req/s)103.8k108.5k+4.5%
Elysia (req/s)158.9k163.3k+2.8%
express (req/s)64.5k66.6k+3.2%
fastify (req/s)91.5k95.9k+4.8%
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%

Claude Code v2.1.181(2026 年 6 月 17 日釋出)之後版本用的就是 Rust 版的 Bun,Linux 啟動快 10%,其他幾乎沒人注意到。「無聊就是好事」(Boring is good.)

生產驗證

Prisma 已經把 Rust port 部署到 Prisma Compute 公開 beta:「之前遇到記憶體洩漏與 connection pool 在 VM 暫停恢復後無法恢復的問題。換到 Rust rewrite 後,這些故障模式全部消失。」這段引文來自 Prisma 工程師 Alexey Orlenko。


Siami 觀點:這是軟體工程史上的一次工程奇觀

為什麼這件事重要

把 53 萬行的執行檔機械式翻譯到另一個語言、用 11 天時間通過 360 萬個 expect calls、且中途完全不凍結 bugfix,這在 2024 年之前會被視為科幻小說。它代表三件事同時到位:

  1. Mythos 等級模型的長脈絡工程能力——Claude Fable 5(同基礎模型於 Claude Mythos 5)能在 11 天內維持對 1,448 個檔案、數萬個函式簽名的脈絡,這是以前任何 LLM 都做不到的。
  2. AI 自身的除錯閉環——「implementer + adversarial reviewer」的分離,比人類 PR review 更便宜、更可重現,且能並行擴展。
  3. Rust 工具鏈成熟度——C/C++ 與 Rust 之間的 LTO、LLVM 的 lifetime intrinsics 優化、Miri 與 LeakSanitizer 整合,這些 5 年前還不存在的基礎設施現在都到位了。

1 位工程師搭配 Fable 加上緊盯 Claude Code,11 天就從開工到 100% 測試套件通過、跨平台都過。一年前的話,這要 3 位熟悉 codebase 的工程師大約一年。

數據解讀與質疑

不過數字漂亮歸漂亮,仍有幾個面向值得追問:

  • unsafe 比例:目前 Bun 整個 Rust codebase 約 4% 在 unsafe 區塊內(13,000 個 unsafe 關鍵字、~27,000 行 / ~780,000 行程式碼)。78% 的 unsafe 區塊只有 1 行——多半是個從 C++ 傳來的指標或一次 C 函式庫呼叫。Jarred 預期這個比例會隨著從「忠實移植」轉向「idiomatic Rust」而下降。
  • Zero regression ≠ zero bug:19 個已知迴歸都被修了,但模糊測試顯示 parser 已跑 1,000 億次才產出 15 個 PR,這代表 fuzz 對真實應用覆蓋到的 bug 密度比想像中低。社群尚未在 production 規模跑超過 2 個月。
  • 社群分裂訊號:Hacker News 上有「Bun is effectively dead. Anthropic bought it in a somewhat dumb attempt to solve their ‘performance’ issues」這類批評;也有「Bun in Rust is better than the original」的樂觀派。Rust subreddit 的主流反應是懷疑「this is full blown vibe coded using Claude」。Siami 認為這類 LLM 寫百萬行 port 的信任赤字需要至少 6 個月的 production data 才能緩解。
  • Token 成本外推:16.5 萬美元聽起來很貴,但若分攤到原本要凍結功能 1 年的機會成本,對 Anthropic 這種每月消耗數億美元 token 的公司而言是划算交易。對其他想複製的開源專案而言,模型 + token 成本是主要門檻。

為什麼 Anthropic 必須這樣做

Claude Code 的啟動速度、tool call 延遲、bun install 的磁碟 I/O 表現,每一項都直接影響 Claude 對使用者的回應時間。Anthropic 在 2025 年 7 月就開始用 Bun 跑 Claude Code、12 月乾脆收購 Bun,這條路徑很清楚:當 runtime 是你的競爭優勢時,擁有它比外包給 Node.js 維護者更合理。這次 Rust rewrite 把 Bun 從「獨立開源專案」轉變成「Anthropic 內部基礎設施的開源窗口」,定位已經不一樣。


參考資料