needhelp
← ブログに戻る

Bun の Rust 移植:11日間、6,778コミット、そして実際に何が変わったか

著者 needhelp
Bun
Rust
Zig
JavaScript Runtime
LLM
Software Engineering

Bun のチームは 2026 年 5 月、珍しいことをやった。ランタイムのネイティブの中核をまるごと Rust 版に置き換えたのだ。しかも 11 日で。

冒頭の開示が全体のトーンを決めている。Bun は 2025 年 12 月に Anthropic に買収された。創設者の Jarred Sumner は、リリース前の Claude —— 彼らが「Mythos 級モデル」と呼ぶ Fable 5 —— を作業の大部分に使った。これは思考実験ではない。最初の Rust 版である Bun v1.4.0 は今、canary で配信されている。

生の数字はこうだ。

  • Zig の 535,496 行(コメント除く)が Rust のコードベースになった
  • 5 月 3 日から 5 月 14 日までに 6,778 コミット
  • 1,448 個の .zig ファイルが機械的に .rs へ移植
  • API 利用料は約 $165,000(キャッシュ外入力トークン 5.9B、出力 690M)
  • ピーク時は 64 個の Claude が同時に 4 つの worktree で稼働

Rust 移植後の Bun の起動(Claude Code 上で実行)

Bun はそもそも Zig への賭けだった

Bun は、esbuild の JS/TS トランスパイラを Go から Zig へ一行ずつ移植したところから始まった。Sumner が Zig の一行目を書いたのは 2021 年 4 月 16 日、Hacker News で Zig の一枚紙の言語リファレンスを見てからだった。初期バージョン —— トランスパイラ、ミニファイア、バンドラ、npm 互換のパッケージマネージャ、Jest 風のテストランナー、Node.js の API 互換層 —— は、LLM がこういう仕事に使えるようになる前に、一人で一年かけて、手狭なオークランドのアパートで書かれた。

その賭けは実を結んだ。Bun の CLI は今、月間 2,200 万ダウンロードを超える。Claude Code と OpenCode がランタイムとして使っている。Vercel、Railway、DigitalOcean が公式サポートを提供している。

ただ、その規模こそが問題でもあった。

負債:GC メモリの隣にある手動メモリ

JavaScript はガベージコレクション方式だ。Bun は JavaScriptCore(Safari のエンジン)に加え、C/C++ のライブラリを山のように埋め込んでいる。uWebSockets、BoringSSL、SQLite、lsquic だ。Zig は C と同じく、メモリ管理を自動ではやってくれない。Bun の役割は、GC される言語と手動管理のネイティブコードの間に立つことだが、その境界こそが最悪のバグのほとんどが潜んでいた場所だ。

アナウンスには、v1.3.14 だけで直したものの例が並んでいる。node:zlib の heap-use-after-free、re-entrant なコールバック由来の node:http2 の use-after-free、UDPSocket.sendMany の範囲外書き込み、tls.connect ごとのメモリリーク、CSS パーサの double-free。まだ続くし、どれも技術的な話だ。

Sumner は、これは Zig のせいではないとはっきり言っている。「Zig がなければここまで来られなかったし、それにはいつまでも感謝している」。問題は構造的なものだ。GC 境界をまたいだライフタイム管理は、それを前提に設計していない言語ならどれでも難しく、Zig の答えである「すべての呼び出し箇所での明示的な defer」は、めったに通らないエラー経路では間違えやすい。

彼は C++ も検討した。Bun の約 20% はすでに C++ で、さらに移せばコンストラクタとデストラクタが手に入る。だがそれでもコードレビューで守るスタイルガイドに頼ることになり、メモリ破壊はなくならない。

Rust の売りは違う。safe Rust なら、use-after-free も double-free も「解放し忘れ」もコンパイルエラーになる。borrow checker と Drop が、スタイルガイドの問題を型システムの問題に変えるのだ。

戦略:再設計ではなくトランスパイル

書き直し(rewrite)は悪評が高い。理由はある。535K 行を一から書き直せば、機能追加もバグ修正も一年凍結される。Bun が選んだ形は機械的な移植だった。アーキテクチャも性能目標も機能もテストスイートも同じ —— ただ Rust で。

この取り組みを全体で動かしたのは二つの決断だ。

  1. 一度にやり切る。段階的ではない。 Sumner が esbuild を Zig に移植した経験から、段階的な書き直しは一時的な足場を残し、役に立つより邪魔になると分かっていた。
  2. トランスパイルされたように見せる。 狙いは、元の Zig から来たと分かるような Rust を書き、v1.4 のリリース後に慣習的な Rust へ向けてリファクタリングすることだ。

決定的だったのは、Bun のテストスイートが TypeScript で書かれていることだ。つまりランタイムが何語で実装されていても気にしない。だから「すべてのテストが green」を完了の定義にできた。

準備は小さかったが意図的だった。コードを書く前に、Sumner は Claude と約 3 時間かけて Zig のパターンを Rust に対応させた。その対話が PORTING.md になった(後に Hacker News に載る)。二度目のパスでは、コードベース中の全構造体フィールドの正しい Rust ライフタイムを分析し、それを LIFETIMES.tsv に書き出した。どちらの文書も、一行も移植する前に敵対的レビューを通った。

そのまま残ったもの:JavaScriptCore と埋め込み済みの C/C++ ライブラリ。書き直したのはネイティブの糊付け部分と Bun 固有のコードで、JS エンジンではない。

実行:64 Claude、11 日

移植は Claude Code 上で約 50 個の「動的ワークフロー」として、11 日間、休みなく動いた。各ワークフローはループだ。タスクを受け取り、コードを出し、レビューを受け、フィードバックを反映する。

面白いのはレビュー体制だ。実装者一人につき、二人以上の敵対的レビュアー —— 「このコードは間違っている」と仮定し、失敗する理由をすべて見つけろと命じられた別の Claude セッション —— がいた。実装者はレビューせず、レビュアーは実装しない。アナウンスはこれを人間のレビューになぞらえている。作者はマージしたいので、別の人がチェックするのだ。

いくつかの仕組みは抜き出す価値がある。

  • まず試し走り。 全 1,448 ファイルに踏み切る前に、3 ファイルを移植した。
  • worktree での分割。 当初の実行では Claude 同士が git stashgit reset で踏みつけ合っていた。解決策は 4 つの worktree、各 16 Claude、タスクの最中は git も cargo も絶対に実行しないというルールだ。
  • コンパイラエラーを作業キューに。 cargo check が約 16,000 件のエラーをファイルに吐き、crate ごとにまとめた。64 の Claude が少しずつ削り、4 つの worktree で 16 の fix/review/apply ループを回した。
  • 循環依存。 Zig は事実上ひとつのコンパイル単位だったが、Rust には約 100 の crate が必要だった。サイクルの解体で、その 16,000 件のエラーの大半が浮き彫りになった。
  • 隔離。 TCP ソケットを使い切ったり約 1 万プロセスを fork したりするストレステストは systemd-run の cgroup 下で実行した。それでもマシンはディスク不足で何度かクラッシュした。
timeline
    title Bun の 11 日間の Zig → Rust 移植(2026年5月)
    2026-05-03 : 移植ブランチをオープン(PR #30412)
    2026-05-04 : 最初の100ファイル草稿バッチ
    2026-05-06 : 約16,000件のコンパイラエラー、64 Claudes
    2026-05-08 : 最初のCI実行(失敗972ファイル)
    2026-05-09 : Linux x64 がグリーンに
    2026-05-11 : Windows がグリーン(最後のプラットフォーム)
    2026-05-14 : 全6プラットフォームがグリーン、マージ済み

数字で見ると:ピーク時は毎分 1,300 行、一番忙しい時間(5 月 6 日)に 695 コミット、最も忙しい一分間で 58 コミット。すべてのコミットは着地前に二人の敵対的レビュアーを通った。最終的な diff は +1,009,272 行。スキップも削除もされたテストはゼロ。

本番さながらになったのは CI だ。最初の実行から二日で、失敗するテストファイルは 972 から 23 へ減った。あと一日半で Linux が完全に green に。Windows は 5 月 11 日に最後に終わり、全六プラットフォームが green になったのは build #54202、5 月 14 日だ。

Rust が実際に手に入れたもの

掲げた目標は安定性で、ポストは硬い数字でそれを裏付けている。

バグ。 Bun v1.4.0 は、v1.3.14 で再現する 128 のバグを直している。

メモリ。 Drop が呼び出し箇所ごとの defer を置き換えた。ポストの例:同じ 60 モジュールのプロジェクトを一つのプロセスで 2,000 回バンドルする。v1.3.14 ではビルドごとに約 3 MB が永久に漏れ続ける。v1.4.0 ではメモリが横ばいになる。

ビルド回数 v1.3.14 v1.4.0
500 1,914 MB 526 MB
1,000 3,506 MB 586 MB
1,500 5,097 MB 608 MB
2,000 6,745 MB 609 MB

Zig でこれを直す以前の試みは、Sumner によればマージされなかった。等価な Drop がないために、自信を持てなかったからだ。

バイナリサイズ。 Rust への書き直し単体で 3.8 MB(Windows)、5.5 MB(macOS)、6.8 MB(Linux)を削った —— 主に過剰な Zig の comptime を捨てたおかげだ。さらにリンカの手入れ(同一コードの畳み込み、ICU データの削り込み)で、Linux と Windows で合計約 20% の縮小になった。

バージョン プラットフォーム サイズ
v1.4.0 Windows 76 MB
v1.3.14 Windows 94 MB
v1.4.0 Linux 70 MB
v1.3.14 Linux 88 MB

速度。 C/C++ と Rust の言語をまたぐ LTO により、コンパイラが言語の境界を越えてインライン展開できる。Bun が測った改善は 2–5%。

  • Bun.serve: 169.6k → 177.7k req/s (+4.8%)
  • express: 64.5k → 66.6k (+3.2%)
  • next build: 13.62s → 13.03s (+4.5%)
  • tsc -b --force: 0.94s → 0.89s (+4.7%)

Prisma は Compute のパブリックベータを Rust 版の移植に乗せた。Alexey Orlenko の言葉:「メモリリークと、VM が一時停止して再開したあとに回復できないコネクションプールに遭遇した。Rust 版の移植が出てきたとき、同じ失敗モードで試した。完璧に処理できた」

Claude Code v2.1.181(6 月 17 日)以降は Rust 版を使っている。起動は Linux で 10% 速い。Sumner の、ユーザーに見える変化への評価はこれだ。「Boring is good.」(退屈こそが良い)

移植が滑ったところ

535K 行の忠実なトランスパイルはタダではない。ポストには 19 の既知の regression が記録されているが、すべて直っている。教訓的なのは、言語間の小さな意味論の穴だ。

  • debug_assert! の副作用。 Zig の assert は関数なので、その引数はすべてのビルドで実行される。Rust の debug_assert! はリリースで消えるマクロだ —— HMR の状態を変更していた insert_stale の呼び出しが、音もなく実行されなくなった。(#30678)
  • 奇数長のスライス。 Zig のヘルパーは末尾の奇数バイトを無視していた。bytemuck::cast_slice はそれでパニックする。UTF-16 の BOM に奇数バイトが続く Blob.text() がクラッシュし始めた。(#31188)
  • 境界チェック。 Zig の ReleaseFast は境界チェックを剥ぐ。Rust は残す。プレースホルダ定数のせいで、ファイル名のインターン上限が 8.4M から 270K に下がり、実際のプロジェクトがそれにぶつかった。(#31503)
  • comptime の書式文字列。 Zig は書式文字列をコンパイル時に評価するので、色マーカーは引数が代入される前に消えている。Rust に comptime はないから、マーカーパーサが OSC 8 ハイパーリンク内のリテラルなバックスラッシュを食ってしまった。(#30693)

二つの言語は見た目が同じでも、そうではないゆえに表面化するバグたちだ。

これの意味

LLM の話を脇に置けば、そこにあるのは本物のエンジニアリングの物語だ。ひとつのプロジェクトが、手動メモリ管理が規模において与えられるものの天井にぶつかり、ひとつのバグの階級を「単に慎ませる」ものから「不可能にする」ツールとして Rust の borrow checker を選んだ。

LLM の話こそが、人々が議論する部分だ。Sumner は率直に、これはコードベース全体を把握した三人のエンジニアに約一年かかったはずで、彼らならやらなかっただろう —— 現実的な代替案は「何もせず、バグを直し続ける」ことだった —— と言う。代わりに、64 の Claude を監督する一人のエンジニアが、11 日で、約 $165K でやってのけた。

Bun の Rust コードの約 4% が unsafe ブロックにある(約 78 万行の全体のうち、約 2.7 万行にわたり約 13,000 個の unsafe キーワード)。そのブロックの 78% は一行だ。マージ以降、セキュリティレビューを 11 回実施し、すべてのパーサを横断する 24/7 のカバレッジガイド付きファジングを追加した —— これまでに 1,000 億回の実行、約 15 件の PR。

私が見張るのは保守性の問いだ。Sumner は Rust が Zig のように読めると言い、その主張のために canMergeSymbols の並べて比較を示している。賭けは、忠実な移植なら port ごとにレビュー可能で、それが一人の人間が 100 万行の diff にサインオフすることを可能にしたというものだ。

彼の結びの言葉は、面白く年を取るものだろう。「One engineer can do a lot more today than a year ago.」(今の一人のエンジニアは、一年前よりずっと多くのことができる)

参考

このページをシェア