Bunの100万行Rust移植がmainに入った
目次
Bunの話、最初に聞いたときはRustをBunに書き換えたように見えたが、逆だった。
JavaScriptランタイムのBunが、Zigで書かれていた本体部分をRustに移した。
2026年5月14日、oven-sh/bunのPR #30412 がmainにマージされた。
PRタイトルはそのまま Rewrite Bun in Rust。
claude/phase-a-port ブランチからmainへ、6755コミット、2188ファイル変更、1,009,257行追加、4,024行削除という規模だった。
PR本文でJarred Sumnerが書いていること
PR本文の説明は数行で終わっている。要点を並べると以下になる。
- Bunの既存テストスイートが全プラットフォームで通る
- メモリリークと不安定なテストをいくつか修正した
- バイナリサイズが3MB〜8MB縮んだ
- ベンチマークは中立から速くなる範囲
- 一番の狙いはメモリバグをコンパイラに拾わせる仕組み
- コードベースは大部分同じ。同じアーキテクチャ、同じデータ構造、サードパーティライブラリは少ないまま、async Rustも使わない
- canaryで
bun upgrade --canaryを実行すると試せる - non-canaryに入る前の最適化が残っている。クリーンアップは後続PRで進める
bun upgrade --canary
公式リリースとしては Bun v1.3.14 が5月13日に出ていて、これがZig版の最後のリリースになる。リリースノートの主役は Bun.Image、isolated linkerのglobal virtual store、HTTP/2とHTTP/3まわりで、Rust化の話はそこには出ていない。
既存アーキテクチャをそのままRustへ
PR本文でJarred Sumnerは、Rust移植を別物の再設計とはしていないと書いている。
同じアーキテクチャ、同じデータ構造のまま、サードパーティライブラリは少なく抑え、async Rustも使わない。Zig実装の構造をRustに写し取る方針だ。
Bun自体は引き続きJavaScriptCore上で動くJavaScript/TypeScriptランタイムで、V8をRustで作り直したわけではない。JavaScriptCoreとのFFI、Node互換API、パッケージマネージャ、バンドラ、テストランナーといった責務は残ったままで、それぞれの内部実装の言語だけが変わった。
Sumnerが第一の狙いに挙げているのは性能ではなくメモリ安全性だった。
Zigで書いていた数年でメモリバグの調査と修正に時間を取られ続けてきたので、use-after-free、double-free、エラー経路での解放漏れをコンパイル時に拾える状態にしたい、と説明している。Rustでも、参照を長く持ちすぎるリークやJavaScript境界をまたぐ再入処理までは自動で消えない。それでも、Zig側で実行時まで沈んでいた種類のバグはコンパイル時の作業に移る。
Zigの特性をRustに写すための調整
「同じアーキテクチャをRustに写す」と言葉で書くと地味だが、コミットを見ると個別の挙動をZig版に揃えるための作業が大量に積まれている。コミットメッセージから拾える例:
LineOffsetTable SmallVec<[i32;256]> stack scratch — match Zig stack— Zig版がスタック上に持っていたバッファを、Rust側でもSmallVec<[i32; 256]>で同じスタック使用にそろえる#[inline] Method::which/find — match Zig comptime-map inlining— Zigのcomptimeで強制インラインされていた箇所をRust側の#[inline]注釈で再現するlazy JsonCache arena — avoid mi_heap_new per bundler worker— ワーカ起動ごとにmi_heap_newを呼ぶコストを避けるTranspilerJob::run stack-local Arena — drop per-worker leaked memory— ワーカ単位のリークを止めるParseTask: dealloc Result without Drop (Zig destroy is struct-only)— Zigのdestroyは構造体専用だったので、RustのDropに合わせて書き換えるread VM thread-local directly + inline drain_microtasks— VM参照をTLSから直接引いてくる
mimalloc(mi_heap_*)のヒープ操作、ワーカごとのアリーナ確保、TLS経由のVMアクセスなど、低レベル制御はZig版と地続きで残っている。Rust側でも #[inline(never)]、SmallVec、Box ドロップ経路など、必要な箇所では地金が見える書き方が選ばれている。
async Rustを使わない選択も、この方針と一致する。BunのI/Oは独自のイベントループ(uws)と既存スレッドモデルに依存していて、tokioランタイムやFutureベースの設計に乗せ替えるとアーキテクチャ自体が変わってしまう。Sumnerは「同じデータ構造のまま」と明言しているので、async Rustを採用しなかったのは方針として整合する。
ブランチ名から見えるAIの使い方
メインブランチ名は claude/phase-a-port だった。Phase Aという呼び方は、後続のPhase B以降を想定した命名だ。本文にも「クリーンアップは後続PRで進める」とあり、Phase B以降は重複除去や内部APIの整理、最適化が中心になりそうだ。
PRに取り込まれているマージコミットを追うと、claude/* 名前空間のサブブランチが複数並行で走っていた:
claude/phase-a-port— メインの移植claude/bench-until-green— ベンチが緑になるまで回し続けるループ用のブランチ。性能リグレッションを潰す作業を分離しているclaude/code-dedup— 重複コードの集約。コミットを足すと-968,-692,-1320,-1946,-480,-717などのLOC削減が続き、合計で5000行超を共通実装に寄せているclaude/ci-auto-fix-53852/claude/ci-auto-fix-53863— CI失敗を自動で直すブランチ。issueナンバー単位で切られている
6755コミット・100万行のdiffを通常のPRレビューで人間が読むのは現実的ではない。
代わりに支えになったのは、Bun側に既存テストスイートと既存ベンチがあったこと、そして移植・重複除去・CI修正・ベンチ調整をテーマごとに別ブランチに分け、それぞれをマージで集約していく運用だ。「同じテストとベンチを通す」という機械判定可能なゴールがあると、AIの生成→検証ループが回しやすくなる。
PR上にはClaude botとCodeRabbitのレビュー痕跡も残っていて、AIによる生成と別のAIによる確認が同居している。テストに出てこない性能劣化、プラットフォーム差、unsafe境界、長期保守のしやすさは、テストとベンチでは拾えない。ここは引き続き人間と長期運用が担当する領域として残る。
同じリポジトリにmainで入れた判断
数字を最初に見たときに引っかかったのが、これを oven-sh/bun のmainに直で入れた点だった。2188ファイル変更・100万行追加というサイズなら、普通は別リポジトリ(bun-rust のような)か長命ブランチで先に育てて、ある程度安定してから本体に統合するスケールに見える。
ただ、PRとコミットの流れを追っていくと、同じリポジトリ運用に寄せた背景は読める。今回の移植は同じアーキテクチャ・同じデータ構造でやっているので、フォークではなく言語の差し替えに近い。別リポジトリを切ってもコードベースは結局同じものになる。テストとベンチも既存リポジトリに揃っていて、別リポジトリにすると claude/bench-until-green のような「緑になるまで回す」AIループの検証基盤が二重持ちになってしまう。Phase Aという命名から後続のPhase B/Cが続く前提も透けて見えるので、別リポジトリで先に育ててからmainに統合する運用だと、もう一度同じ規模のマージを抱えることになる。
代わりに履歴は荒れる。git blame はこの1マージコミットでZig時代の作者情報がほぼ塗り潰され、READMEも記事執筆時点で written in Zig のままだ。コード移行とドキュメント更新は同期していない。Zig版の履歴を追いたい人は、マージコミットの親をたどることになる。
なぜAnthropicがBunを書き換えるのか
Bunは2025年にAnthropic傘下に入り、Claude Codeのランタイムとして使われている。以前 Claude CodeのnpmソースマップからBun利用が見えていた ように、Claude CodeはNode.jsではなくBunで動いている。
このつながりが今回の移植の前提になる。AnthropicはClaude Codeのランタイム保守にコストを払い続ける立場で、Sumnerによれば、Zig版で長年メモリバグの調査と修正に時間を取られてきた。一方で、単一バイナリ配布、JavaScriptCoreとのFFI、低レベル制御という要件はそのまま残したい。後ろにはテストスイートとベンチがあり、AI移植の検証基盤として使える。
ここまで条件が揃うと、コンパイラに型・借用・所有権の検査を任せて、メモリバグの調査時間を減らすという投資判断がRust化の主目的になる。AIで100万行を移植したという見出しの先にあるのは、Claude Code開発基盤の長期保守コストを下げる話だ。
Zigエコシステムから見た位置づけ
BunはZigの代表的な大規模実用プロジェクトだった。Zig採用の実例としてBunを思い浮かべる開発者は多かったし、Bunの存在がZigの知名度を押し上げていた。
GitHub上のmainブランチは、この記事を書いている時点でもREADMEに「written in Zig」と残っている。一方で、GitHubの言語統計はRust 46.6%、Zig 32.2%になっていて、コードはすでにRust優位に逆転している。READMEや公式サイトの文言は遅れて追従するはずだ。
Zigが悪いから乗り換えたという単純化は当たらない。Bunが要求した低レベル制御、JavaScriptCoreとのFFI、単一バイナリ配布はZigで成立していたし、初期実装でZigを選んだ判断は今でも筋が通る。今回の切り替えは、長期保守を担う主体がAnthropicになり、メモリバグをコンパイラに拾わせるトレードオフを取った結果だ。
Zigコミュニティ側に残るのは、Bunが示した実用パターン(JSC統合、独自イベントループ、単一バイナリ配布、低レベルなI/O実装)と、その実装が次のZigプロジェクトの参照点として読まれ続けることになる。Bun自体の言語選択が変わっても、Zigで実用ランタイムを書くときに踏むべき道筋はBunのコミット履歴に残っている。
canaryで触れる段階
Bunをツールチェーンとして使っているだけなら、いまの段階で動く話は少ない。通常の bun upgrade で入るv1.3.14はZig版の最後のリリースで、リリースノート上はBun.ImageやHTTP/3が前面に出ている。Rust移植版に触れる入口はcanaryで、bun upgrade --canary で切り替えられる。
本番のCIやサーバーランタイムでBunを使っているなら、Rust化そのものより手元の挙動を先に確認した方がいい。具体的には:
bun installの挙動と依存解決の差bun testの安定性と既存テストの通過- native addonやN-API依存パッケージの動作
- ファイル監視・watcherまわり
- LinuxとmacOSとWindowsそれぞれでの差
PR本文では全プラットフォームの既存テスト通過と書かれているが、自分の依存パッケージと実行パターンまで含めた保証ではない。
Rust移植版が通常版に入ったら、クラッシュ率、メモリ使用量、起動時間、CI時間、3プラットフォームの挙動差を順番に確かめていく作業に入る。手元の数字が変わるかどうかはここから先で見えてくる。