世界を動かす技術を、日本語で。

Subsecond: Rustのホットリロード用ランタイムホットパッチエンジン

概要

Subsecond はRustアプリケーション向けの ホットパッチ ライブラリであり、アプリを再起動せずにコード変更を反映可能。 開発時の ThinLinking によるビルド高速化も特徴。 ゲームエンジンやサーバーなどの 長時間稼働アプリ に特に有用。 Dioxusチームが開発・保守しており、 Dioxus CLI との連携が推奨される。 一部制限や注意点もあるため、詳細を確認の上で利用推奨。

Subsecond: Rust向けホットパッチライブラリの概要

  • Subsecond はRustアプリケーションの実行中に コードを変更・反映 できるホットパッチ機能を提供
  • ゲームエンジンサーバー など、再起動コストが高い長時間稼働アプリケーション向け
  • ThinLinking 技術により、開発モードでのビルド速度を大幅短縮
  • Dioxusチーム が開発・保守、公式CLIツールとの親和性
  • 安全性重視 の設計で、プロセスメモリを直接書き換えず外部ツールによるジャンプテーブル方式を採用

使い方

  • アプリケーション・ライブラリ開発者向けに シンプルなAPI 設計
  • 既存の関数呼び出しを subsecond::call でラップするだけの実装例
    • 例:
      for x in 0..5 {
          subsecond::call(|| {
              println!("Hello, world! {}", x);
          });
      }
      
  • 実際のパッチ適用には、 Subsecondコンパイラとプロトコル を実装したサードパーティツールが必要
  • 公式推奨は Dioxus CLIdx serve --hotpatchでホットリロード対応)
  • cargo binstall でCLIインストール推奨

仕組み

  • ジャンプテーブル による関数コールの迂回実装
  • ジャンプテーブルが最新の関数ポインタを保持し、呼び出し時に参照
  • プロセスメモリの直接書き換えは非採用 で安全性確保
  • 外部ツールが変更部分のみを再コンパイルし、パッチとしてジャンプテーブルを送信
  • 実行中アプリが パッチを受信・適用 し、即座に新コードで動作継続
  • ランタイム統合が必須、未統合時は安全なパニック発生・自動リトライ機構

ワークスペース対応

  • 現状は “tip”クレート(main.rs所在クレート) のみホットパッチ対象
  • 他クレートの変更は無視されるため、複数クレート構成時は注意
  • 将来的に フルワークスペース対応 予定

グローバル・static・スレッドローカル変数

  • グローバル・static・スレッドローカル のホットリロードを一部サポート
    • 新規グローバル追加は可能だが、デストラクタは呼ばれない
    • staticイニシャライザ変更は反映されない
    • “tip”クレート内のスレッドローカルはパッチ適用時に初期値へリセットされる制限
  • 複雑なセットアップではクラッシュやセグフォの可能性あり、将来的な改善予定

構造体のレイアウト・アラインメント

  • 構造体のホットリロードは未対応
    • レイアウトやアラインメント変更時、古い構造体参照でクラッシュするリスク
  • フレームワーク側で re-instancing (再インスタンス化)対応推奨
    • 例:Dioxusは古い状態を破棄し再構築

ポインタバージョニング

  • 現状、 関数ポインタのバージョン管理なし
    • 将来的にメタデータ追加予定
  • DioxusやBevyはTypeIDやHotFnのptr_addressを利用し、変更検知を実装

ネスト呼び出し

  • Subsecond::call はネスト可能
    • パッチ適用範囲を細かく制御でき、状態消失を最小化
    • 例:main関数内でforループやprintln!を個別にラップ

パッチ適用方法

  • Dioxus CLIdx serveコマンド利用時、自動でパッチ適用
  • Dioxus DevtoolsのWebSocketプロトコル経由でパッチ配信
  • 独自アプリ統合時は apply_patch 関数でジャンプテーブル適用
  • Dioxus Devtoolsプロトコル対応には dioxus-devtools クレートのconnectメソッド利用
  • ASLR 対応のため、main関数アドレス通知が必要

ThinLinkについて

  • ThinLink はSubsecond用のRust向けプログラムリンカ
    • 既存リンカのラッパーで、動的リンクやジャンプテーブル生成、差分ビルドに対応
    • 開発プロファイルで 500ms未満のビルド速度 を実現可能
  • Dioxus CLIに自動統合、単体提供は現状未定

制限事項

  • 構造体のホットリロードにはre-instancingやアンワインドが必要
  • static変数は追跡されるがデストラクタは呼ばれない
  • スレッドローカルの初期化・リセット問題

プラットフォーム対応

  • 主要プラットフォーム 全対応
    • Android(arm64-v8a, armeabi-v7a)
    • iOS(arm64, シミュレータのみ、実機は未対応)
    • Linux(x86_64, aarch64)
    • macOS(x86_64, aarch64)
    • Windows(x86_64, arm64)
    • WebAssembly(wasm32)
  • 新規プラットフォーム追加希望は Subsecondリポジトリ でIssue提出推奨

Subsecondバッジの追加

  • フレームワーク作者向けにREADME用 Subsecondバッジ 提供
    • 例: [![Subsecond](https://img.shields.io/badge/Subsecond-Enabled-orange)](https://crates.io/crates/subsecond)

ライセンス

  • MITライセンス 採用
  • 詳細はLICENSEファイル参照

サポート・支援

  • Dioxusチーム による開発
  • GitHubスポンサーやDioxus Deploy(準備中)による支援歓迎

主な型・関数

  • HotFn :ホットリロード可能な関数型
  • HotFnPanic :call時に発生するパニック型(リトライ処理用)
  • HotFnPtr :ホットパッチ対象関数へのポインタ
  • JumpTable :ジャンプテーブル型
  • PatchError :パッチ適用時のエラー型
  • HotFunction :ホットパッチ可能型を実現するトレイト
  • apply_patch :ジャンプテーブルを用いたパッチ適用関数
  • call :ホットリロード対応関数呼び出し。上位コード変更時はパニック発生・アンワインド
  • register_handler :ハンドラ登録用関数

Hackerたちの意見

Rustサーバーの作業でこれを試してみる必要がありそう。progscrape.comは、編集-コンパイル-実行サイクルが遅いから、すぐに起動できるようにいろんなトリックを使ってるんだよね(主にインデックスの遅延読み込みとか)。今の仕事先のGelでは、結構複雑なコードのRustソケットフロントエンドに取り組んでて、これも面白そうだな。コードベースの中でいい「カットオーバー」ポイントを選ぶ必要があるみたいだけど、正直言ってそれほど難しくはなさそう。ウェブサーバーのHTTPサービスハンドラーとか、非ウェブサーバーのソケットハンドラーとかね。メインクレートだけがホットパッチ可能っていう制限があるみたいだけど、それはちょっと残念だね。でも、その便利さがあれば、コード構造を変える価値はあるかも。

クリエイターです!まだブログ記事を書く時間がなかったんだけど、楽しみにしててね。要するに、Rustのリンクフェーズをインターセプトして、手動でrustcを動かしてるってこと。コンパイル間のアセンブリを比較するための差分ロジックがあって、実行中のプロセスに対してシンボルをパッチするリンクフェーズがあるんだ。macOS、Windows、Linux、iOS、Android、WASMで動くよ。m4では130msのコンパイル-パッチ時間が出せて、かなりすごい。従来のdylibリロードが扱えないような難しい部分(TLS、スタティック、コンストラクタなど)も対応してる。デモはTwitterページに投稿してるよ(Twitterごめんね…)。 - Bevy: https://x.com/dioxuslabs/status/1924762773734511035 - iOSで: https://x.com/dioxuslabs/status/1920184030173278608 - フロントエンド + バックエンド(axum): https://x.com/dioxuslabs/status/1913353712552251860 - Ratatui(TUIアプリ): https://x.com/dioxuslabs/status/1899539430173786505 未完成のリリースノートはこちら: https://github.com/DioxusLabs/dioxus/releases/tag/v0.7.0-alp... 詳細は後でお知らせするね!

xitterの投稿にアクセスできない…axumの部分はDioxus全体を使ってるの?それともbare axum + コードリロード?

axum の例は驚くほど便利そう!すごくクールなプロジェクトとアイデアだね。

[どうやって] パッチを当てたコードの古いバージョンを削除するのが安全かを追跡するの? 編集:まあ、これが本番で使うことを想定していないみたいだから、必要ないかもね。ランタイムプロセスを無限に膨らませることができるし。LinuxカーネルのライブパッチがRCUに関連する何かでこれを扱っていると理解しているから、ちょっと興味があったんだ。

いいね。長い間、ホットパッチングを使う人がいるのか疑問だったけど、大きなJavaアプリケーションを扱うことで、その可能性を理解できたよ。100%信頼できるわけじゃないけどね(Javaみたいに)。ドキュメントを見た感じ、Subsecondはほぼ完璧に見える。ただ、見つけた唯一の欠点は(もし理解が合っていれば)、ホットパッチしたい関数のソースコード内で関数呼び出しを修正しなきゃいけないこと。リリースビルドではその変更がコストをかけないから少し軽減されるけど、それでも大きな問題だよね。デバッグセッションでパッチを当てるかもしれない関数ごとに呼び出しを散りばめたくないな。

クリエイターです!ランタイムにフックするためにはsubsecond::callを一つだけ使えばいいし、それが自分のコードに入ってる必要もないよ。依存関係の中にあっても大丈夫。現在、DioxusとBevyはsubsecondの統合がされていて、エンドユーザーの設定なしで自動的にホットパッチングができるんだ。axum、ratatui、eguiなどの一般的なアダプターもリリース予定だよ。

ずっと誰がホットパッチを使うのか不思議だった。たくさんホットパッチを使っていたけど、今はできない... これは本番向けじゃないけど… ホットパッチはプログラムの状態を失わずにコードを更新するために不可欠なんだ。多くの人がHTTPリクエスト/レスポンスの仕事をしていて、プログラムの状態はリクエスト/レスポンスの間だけ持続するから、通常はそれにはホットパッチは必要ない。レスポンスがすごく長い場合を除いてはね。リクエストがしばらく経ってから新しいコードに当たるように新しいコードを入れ替える方法はいろいろあるし、古いコードで始まったリクエストが終わるのが普通だから、これが通常のやり方だよ。長いリクエストがあって、その間に何かを変更したい場合はホットパッチが必要だね。長時間接続が続くものや他の複雑なセッションがある場合、ホットパッチは切断/再接続の手間を大幅に減らして、新しいバージョンでスムーズに動かせるようにしてくれる。ホットパッチの手間を受け入れる限りはね。

面白いけど、リロード可能なコード部分には単にdylibを使いたいな。

以前作ってたおもちゃのゲームでこれを使ったことがあるんだけど、しばらくはうまくいった。でも問題は、時々OSが dlclose をノーオペレーションにしちゃうこと。OSに強制的にアンロードさせる方法は見つけたことがないし、時々そのまま残っちゃうんだよね。[1] https://github.com/andreivasiliu/demimud/tree/master/netcore

面白いけど、ドキュメントを読むと、変更したいコードを特別なラッパー「call」関数で事前にラップしなきゃいけないみたいに聞こえる。もしそうなら、特別なアノテーションなしでどんな関数でも変更できる他の言語の既存のソリューションよりも魅力が減っちゃうね。

基本的にプログラムのtick()関数をラップする必要があるよ。そうしないと、mallocの途中でホットパッチを当てて、構造体のレイアウトやアライメントが変わって、プログラムが未定義の動作でクラッシュしちゃうかも。目標は、フレームワークがtick()関数にsubsecond::currentを組み込んで、エンドユーザーがホットパッチングを無料で利用できるようにすることなんだ。

ここに強く同意する。プログラムのアドレス空間を変更しない「純粋性」の BS は、プログラマーにとってかなりのストレスを伴うように思える。ライブラリを手動でサポートして間接テーブルを維持するのは、私にとっては絶対に無理だ。リンクを作ったことがあるなら、そのメンテナンス負担をメタプログラミングで解決するのは比較的簡単なはずだと思う。

思っているほど悪くないよ。ホットリロードが必要な部分は全体のコードベースの5%未満だから。たいていデバッグできないもの(APIのレスポンスとか)だし。だから、何度もコンパイルし直してる。もしそれにホットリロードができれば、95%の時間短縮になる。残りのコードの再コンパイルには耐えられるよ。

Rust の dylibs が現実になることを本当に望んでる。ライブラリが特定のバージョンのトレイトを実装して、その実装を動的に読み込んで挙動を変えられるプラグインシステムがあればいいのに。今のところ、そんなことを実装するにはかなりの unsafe コードが必要で、それにはちょっと抵抗がある。

stabbyやabi_stableはあなたのユースケースには合わないの?

他のウェブフレームワーク、例えば Leptos がサブセカンドを採用するか、自分たちのを作るか気になるな。

構造体について: https://docs.rs/subsecond/0.7.0-alpha.1/subsecond/index.html... 「実際には、サブセカンドパッチングを適切に実装したフレームワークは古い状態を捨てるので、ミスアライメントやサイズ変更によるセグフォルトを目撃することはないはずです。フレームワークは、サイズやアライメントの変更を引き起こす古い状態を積極的に処分することが推奨されています。」古い状態を捨てるのが理にかなった推奨だとは思えない。「再インスタンス化」は OTP では「内部状態の更新/アップグレード/ダウングレード」と呼ばれている: https://www.erlang.org/docs/24/man/gen_server#Module:code_ch... もしかしたら何か見落としてるかもしれないけど、サブセカンドはおもちゃアプリや開発者のワークフローにはいいツールかもしれない。でも、真面目な用途には構造体のレイアウト管理が一番の懸念だと思う。

これは Rust のイテレーションを早めるための開発者ツールで、Erlang のようなプロダクションツールではない。それはさておき、Bevy は bevy-reflect を使ってホットパッチ間で適切な構造体のホットリロードを実装してるよ。

Subsecondはデバッグアサーションが有効な時だけ使えるから、パフォーマンスのオーバーヘッドを気にせずにアプリを出荷できるよ。デバッグアサーションは通常、開発段階でしか有効にならない(リリースビルドはデフォルトでdebug_assertions=falseだから)、だからこれは本番環境での使用を想定してないね。

Juliaがどのようにコードのリロードを扱っているか、組み込みのインフラやRevise.jlパッケージを調べてみることをおすすめするよ。基本的に、関数の各メソッドや、v1.12(現在ベータ版)以降は、すべてのバインディングや構造体定義には「ワールドエイジ」が関連付けられているんだ。つまり、Juliaは動的型付けだけど、関数内では固定されたワールドエイジの中で静的型付け言語のように振る舞う。だから、関数への入力タイプに基づいて、コンパイラはメソッドテーブルが変更されないこと、const-globalであること、型も変更できないことを知ることができるんだ。でも、ワールドエイジが変わると、何でもあり。メソッドを再定義したり、構造体を再定義したりできる。特にいいのは、古いデータが無効にならず、古いワールドに存在しているだけで、Foo構造体を取得すればどのワールドから来たかがわかること。関数内でワールドエイジを進めるためのinvokelatestやinvoke_in_world関数があって、ユーザーがイベントループのようなものを作ると、イベントループの反復処理をinvokelatestでラップするだけで、エディタでコードを変更するたびに自動的にホットリロードされるんだ。

言語設計の観点からは面白いけど、Rustでこんなデザインを使うのは無理だよね。もし私が仕組みを誤解してなければだけど。

これいいね、Erlang/OTPのホットコードリロードからインスパイアされたのかな。