プログラミング
RustでのBunの書き直し
Rewriting Bun in Rust (bun.com)
要約
Bunは当初Zigで書かれていましたが、安定性の課題に直面し、Rustでの書き直しが進められています。ZigとJavaScriptのメモリ管理の複雑な相互作用が、メモリリークやクラッシュの主な原因でした。Rustの所有権システムとメモリ安全機能は、これらの問題を根本的に解決し、Bunの長期的な安定性と信頼性を向上させることを目指しています。
全文翻訳
開示: Bunは2025年12月にAnthropicに買収されました。私とBunチームの他のメンバーはAnthropicで働いています。Rustへの書き直しには、Claude Fable 5のプレリリース版を多く使用しました。
Bunは、esbuildのJavaScript & TypeScriptトランスパイラをGoからZigへ1行ずつポートすることから始まりました。私は2021年4月16日に最初のZigの行を書きました。Hacker Newsで見た単一ページのZig Language Referenceを見て、低レベルの制御とパフォーマンスへの配慮に非常に興奮し、Zigに賭けました。
当初から、Bunのスコープは巨大でした:
JavaScript、TypeScript、CSSのトランスパイラ、ミニファイア、バンドラ
npm互換のパッケージマネージャー
Jestライクなテストランナー
Node.js & TypeScript互換のモジュール解決
HTTP/1.1 & WebSocketクライアント
fs、net、tlsなどのNode.js API実装、およびその他多数のモジュール
Bunの初期バージョンは、LLMが登場する前の、狭いオークランドのアパートで、私が1年かけてZigで書いたものです。Bunのような野心的なスコープのプロジェクトのデフォルトの結果は、GitHubプロフィールのページにある死んだサイドプロジェクトの墓場に加わることです。ZigがBunを可能にしました。Zigがなければ、1年でこれほど多くのものを構築することはできなかったでしょう。現在、BunのCLIは月間2200万ダウンロードを超えています。Claude CodeやOpenCodeのような人気ツールは、Bunをランタイムとして採用しています。Vercel、Railway、DigitalOceanなどがBunのファーストパーティサポートを提供しています。
Bunのスコープは安定性の課題でもありました。以下は、Bun v1.3.14で修正したバグの小さなサンプルです:
スレッドプールでasync .write()が進行中に.reset()を呼び出した際のnode:zlibでのヒープ使用後解放クラッシュ
onerrorコールバックが再入可能なwrite()を発行し、その後ネイティブハンドルでclose()を発行した際のnode:zlibでの使用後解放クラッシュ
再入可能なJSコールバック(例:タイムアウトリスナー内のsession.request()、オプションゲッター、または書き込みコールバック)がハッシュマップのリハッシュを引き起こし、内部ストリームポインタを無効にした際のnode:http2での使用後解放クラッシュ
valueOf()またはtoString()コールバックで、ペイロードキャプチャと実際の送信の間にユーザーコードがArrayBufferをデタッチできた場合のUDPSocket.send()およびsendMany()での使用後解放クラッシュ
valueOfコールバックが引数変換中に基になるArrayBufferをデタッチまたはリサイズした場合のBuffer#copyおよびBuffer#fillでのクラッシュと境界外読み取り
ユーザーJSコールバックによってソケットの接続状態がイテレーション中に変更された場合のUDPSocket.sendMany()でのヒープ境界外書き込み
出力バッファの割り当てが失敗した場合にコールバックと保護されたパスワード/ソルトバッファが決して解放されなかったcrypto.scryptでのメモリリーク
エラーパスでstrdupされたパスフレーズをSSLWrapper.initがリーク
SSL_SESSION_freeをd2i_SSL_SESSIONの後に行わなかったため、各呼び出しでSSL_SESSION(呼び出しあたり約6.5KB)がリークしたtlsSocket.setSession()でのメモリリーク
.close()後にfs.watch()ウォッチャーが決してガベージコレクションされなかったメモリリーク。これは参照カウントのアンダーフローが原因で、各ウォッチャーがGCルートとして永久にピン留めされていました。
背景クリップにベンダープレフィックスとマルチレイヤー背景があった場合のCSSパーサーでの二重解放クラッシュ
DuplexUpgradeContextが決して解放されなかった — tls.connect({ socket: duplex })ごとの完全なリーク
GCマーカー スレッドがBroadcastChannelまたはMessagePortからの同時アクセス中にm_dataの破損したバリアントを観測できた場合のMessageEventでの競合状態クラッシュ
私たちはこれらの種類のバグを永続的に一つずつ修正し続けることもできましたが、ユーザーに対してより良いことをする、そしてこれらの種類のバグが再発しないように体系的に防止するという義務があります。
私たちがすでにやっていたこと
Address Sanitizerサポートを追加するためにZigコンパイラをパッチしました。コミットごとにASANでテストスイートを実行しています。
WindowsではZigの安全チェック付きReleaseSafeビルドを配布しています。
V8 & JavaScriptCoreによって使用されているJavaScriptエンジンのファザーであるFuzzilliを使用して、BunのランタイムAPIを24時間年中無休でファズしています。
メモリリークの検出のためのエンドツーエンドテストを多数用意しています。
これは多くのプロジェクトが行っている以上のことです。
ただ本当に賢く振る舞い、間違いを犯さない?
私たちのバグ修正リストは悪く感じられ、Bunでのクラッシュを心配しながら眠りにつくのにうんざりしていました。私はその原因をZigのせいにはしません。Zigの他のユーザーは私たちが抱えていたようなバグを抱えていませんし、GCと手動管理メモリの混合は、ソフトウェアが設計を必要とするほど一般的ではないため、どの言語も本当に設計していません。Zigなしではここまで来られなかったでしょうし、常に感謝しています。ごく最近まで、Bunのようなプロジェクトにとって、プログラミング言語の選択は一方通行の決定でした。
JavaScriptはガベージコレクションされる言語であり、JavaScriptCore(およびV8)のような最新のJavaScriptエンジンは、例外処理とガベージコレクタに関して厳格なルールを持っています。ZigはCのようにメモリを管理しません。これは多くのプロジェクトにとってZigを使用する大きな理由となるトレードオフです。Zigにはコンストラクタ/デストラクタがなく、ほとんどのクリーンアップは各呼び出しサイトで明示的にdeferを使用して書き出すことが期待されます。
Bunにとって、ガベージコレクションされる値と手動管理される値のライフタイムを正しく処理することは、安定性の問題の主な原因となってきました。最も頻繁には小さなメモリリーク、そして時折クラッシュです。すべてのメモリ割り当ては細心の注意を払ってレビューする必要があります。これらのバイトはどこで解放されますか?一度だけ解放されることをどうやって保証しますか?JavaScriptの例外を正しくチェックしましたか?これはガベージコレクションされたポインタですか、それとも手動管理されたメモリですか?
安定性の問題については、可能な限り早く知ることが最善です。ファジングはコードがマージされた後に行われます。CIはコードがプッシュされたときに行われます。ランタイムの安全チェックとアドレスサニタイザーはコードが実行されるとき(できれば開発中に、CIの前)に行われます。
このクラスの問題を減らす一般的な方法の1つは、クリーンアップコードがそれを必要とするコードに対して常に正確に1回実行されることを保証することです。Zigは隠れた制御フローのないシンプルな言語として設計されており、C++の暗黙的な~DestructorやRustの暗黙的なDropよりも、スコープの終わりにコードを実行するために明示的なdeferキーワードを好みます。
言語
クリーンアップ
Zig
defer, errdefer
C++
~Destructor, &&Move
Rust
Drop
Zigコードの場合、クリーンアップコードを実行すべき正確なタイミングはいつですか?同じ*Tを多くの異なる関数に渡している場合、それがもはやアクセスできなくなり、クリーンアップできるのはいつかどうやってわかりますか?関数が呼び出された後もメモリへの参照を継続する必要がある場合、それはどのように機能しますか?現在の私たちの方法は次のものの組み合わせです:
アリーナのライフタイム。アクセス可能なスコープが明確です(パーサーステートは呼び出し関数からエスケープしないため、ASTノードはそこで良い選択肢です)。
参照カウント
非常に注意を払う
多くのプロジェクトは、これらの質問にスタイルガイドを通じて答えることを選択します。TigerBeetleのTigerStyleはZigの例であり、Googleの31,000語のC++スタイルガイドは別の例です。スタイルガイドの課題は強制です。スタイルガイドが守られていることをどうやって保証しますか?歴史的には、コードレビューが、リンターと静的アナライザによるベストエフォートの強制を伴う答えでした。
明確な所有権の期待を型システムに明示的に記述した厳格なスタイルガイドは、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();
// ...
}
これは、私たちが期待するZigよりもエルゴノミクスが劣ります:
fn foo(a: *TCPSocket) !void {
const b = try do_something_with_a(a);
// ...
}
C/C++はどうですか?
Bunのコードの約20%はC++で書かれており、BunはいくつかのC/C++ライブラリを埋め込んでいます:
JavaScriptCore、Safariを支えるJavaScriptエンジン
uWebSockets & usockets — 私たちのHTTP/WebSocketサーバー、およびイベントループ
lshpack & lsquic — HPACKおよびHTTP/3ライブラリ
BoringSSL、GoogleのOpenSSLフォーク
SQLite
Zigの代わりにC++は、Bunにとって合理的な選択肢となるでしょう。コンストラクタとデストラクタを得ることができます。多くのextern "C"ラッパーコードを削除できます。
しかし、私たちは依然としてコードレビューを通じて強制されるスタイルガイドに依存することになり、ASANを使用しても、メモリ破損やメモリリークは依然として発生するでしょう。
なぜRustなのか?
大規模