Read OSS

内部構造を探る:Tauriのランタイム抽象化とプラットフォーム統合

上級

前提知識

  • 第1〜3回:アーキテクチャ、ライフサイクル、IPC
  • Rust の深い知識:関連型、GAT、Send/Sync 境界
  • イベントループとウィンドウシステムの概念への理解
  • 条件付きコンパイル(#[cfg(...)])への習熟

内部構造を探る:Tauriのランタイム抽象化とプラットフォーム統合

このシリーズを通じて「ランタイム」層について繰り返し触れてきました。これは Tauri フレームワークと実際の webview・ウィンドウライブラリの間に位置する抽象化レイヤーです。最終回となる本稿では、そのレイヤーを徹底的に掘り下げます。インターフェースを定義するトレイト階層、それを実装する WRY、スレッド安全性を実現するディスパッチャーパターン、そして Android・iOS への抽象化の延長について詳しく見ていきましょう。

Runtime トレイト:抽象インターフェース

Runtime トレイトは、この設計の基盤となる抽象化です。アプリケーションのライフサイクルを生成・管理するためのメソッド群と、4つの関連型を定義しています。

pub trait Runtime<T: UserEvent>: Debug + Sized + 'static {
    type WindowDispatcher: WindowDispatch<T, Runtime = Self>;
    type WebviewDispatcher: WebviewDispatch<T, Runtime = Self>;
    type Handle: RuntimeHandle<T, Runtime = Self>;
    type EventLoopProxy: EventLoopProxy<T>;

    fn new(args: RuntimeInitArgs) -> Result<Self>;
    fn create_proxy(&self) -> Self::EventLoopProxy;
    fn handle(&self) -> Self::Handle;
    fn create_window<F>(&self, pending: PendingWindow<T, Self>, ...) -> Result<DetachedWindow<T, Self>>;
    fn create_webview(&self, window_id: WindowId, pending: PendingWebview<T, Self>) -> Result<DetachedWebview<T, Self>>;
    // ... モニタークエリ、テーマ設定、プラットフォーム固有メソッド
}
classDiagram
    class Runtime~T~ {
        <<trait>>
        +type WindowDispatcher
        +type WebviewDispatcher
        +type Handle
        +type EventLoopProxy
        +new(args) Result~Self~
        +handle() Handle
        +create_window(pending) Result~DetachedWindow~
        +create_webview(window_id, pending) Result~DetachedWebview~
        +run(callback)
    }
    class RuntimeHandle~T~ {
        <<trait>>
        +type Runtime
        +create_proxy() EventLoopProxy
        +create_window(pending) Result~DetachedWindow~
        +create_webview(window_id, pending) Result~DetachedWebview~
        +run_on_main_thread(f)
    }
    class EventLoopProxy~T~ {
        <<trait>>
        +send_event(event) Result
    }

    Runtime --> RuntimeHandle : handle()
    Runtime --> EventLoopProxy : create_proxy()
    RuntimeHandle --> EventLoopProxy : create_proxy()

これらの関連型は閉じた体系を成しています。RuntimeHandle を生成し、Handle がディスパッチャーを生成し、ディスパッチャーは関連型として Runtime 型を持ち返す——この循環構造により、単一のランタイム実装の中で一貫性が保たれます。

RuntimeHandle トレイトは、Runtime に対応する Send + Sync + Clone な存在です。Runtime 自体は run() によってイベントループに入ると消費されますが、RuntimeHandle は自由にクローンしてバックグラウンドスレッドに渡せます。ウィンドウや webview の生成メソッドに加え、run_on_main_thread() という脱出口も備えており、メインスレッドへの処理のスケジューリングに使えます。

ウィンドウと Webview のディスパッチャー

GUI フレームワークでは一般に、UI 操作はメインスレッドで行う必要があります。Tauri はこの制約をディスパッチャーパターンで解決しています。WindowDispatchWebviewDispatch トレイトはそれぞれ crates/tauri-runtime/src/window.rscrates/tauri-runtime/src/webview.rs で定義されています。タイトルやサイズ、位置、表示状態の変更、JavaScript の実行など、あらゆる UI 操作のメソッドを提供します。

これらのディスパッチャーは Send を実装しているため、どのスレッドからでも保持できます。メソッドを呼び出すと、ディスパッチャーはメインスレッドのイベントループにメッセージを送り、そこで同期的に処理されます。呼び出し側から見ると set_title() などが失敗しないように見えるのはこのためで、実際の操作は即座に実行されるのではなく、キューに積まれます。

このパターンは、メインの tauri クレートがディスパッチャーを使う箇所によく表れています。sealed メソッド ManagerBase::runtime() が返す RuntimeOrDispatch 列挙型を見てみましょう。

pub enum RuntimeOrDispatch<'r, R: Runtime> {
    Runtime(&'r R),
    RuntimeHandle(R::Handle),
    Dispatch(R::WindowDispatcher),
}

App は実際の Runtime を保持しているので Runtime(&R) を返し、AppHandleRuntimeHandle を保持しているので RuntimeHandle を返します。WindowWebview はディスパッチャーを保持しています。ウィンドウや webview を生成するフレームワークのコードはこの列挙型をマッチして、呼び出すべき API を決定します。

WRY による実装

tauri-runtime-wry は、2つのライブラリを使って抽象トレイトをすべて実装しています。

  • TAO(Tauri チーム製)— クロスプラットフォームのウィンドウライブラリ。winit のフォークで、システムトレイ、メニューバー、グローバルキーボードショートカットなどの機能を追加しています
  • WRY(Tauri チーム製)— クロスプラットフォームの webview レンダリングライブラリ。プラットフォームネイティブの webview(macOS/iOS/Linux では WebKit、Windows では WebView2、Android では Android WebView)をラップしています
flowchart TB
    subgraph "tauri-runtime-wry"
        WRY_IMPL["Wry struct<br/>implements Runtime"]
        HANDLE["WryHandle<br/>implements RuntimeHandle"]
        WIN_D["WryWindowDispatcher<br/>implements WindowDispatch"]
        WV_D["WryWebviewDispatcher<br/>implements WebviewDispatch"]
    end
    subgraph "TAO"
        EL["EventLoop"]
        WIN["Window"]
    end
    subgraph "WRY"
        WV["WebView"]
    end

    WRY_IMPL --> EL
    WIN_D --> WIN
    WV_D --> WV
    EL --> WIN
    WIN --> WV

Wry 構造体は TAO の EventLoop を所有し、run() が呼ばれるとそれを起動します。ウィンドウの生成は TAO の WindowBuilder を通じて、webview の生成は WRY の WebViewBuilder を通じて行われます。カスタムプロトコルハンドラ(tauri://ipc://asset://isolation://)は、webview の初期化時に WRY へ登録されます。

#[default_runtime] マクロと Wry 型エイリアス

多くの Tauri ユーザーはコードの中で <R: Runtime> を書く必要がありません。これを実現しているのは2つの仕組みです。

まず Wry 型エイリアスです。

#[cfg(feature = "wry")]
pub type Wry = tauri_runtime_wry::Wry<EventLoopMessage>;

そして #[default_runtime] proc macro です。このマクロは wry フィーチャーが有効な場合に、構造体と impl の定義において最後のジェネリックパラメーターのデフォルトを Wry にします。

flowchart LR
    INPUT["#[default_runtime(crate::Wry, wry)]<br/>pub struct Builder&lt;R: Runtime&gt;"] --> MACRO["default_runtime<br/>proc macro"]
    MACRO --> OUTPUT["#[cfg(feature = 'wry')]<br/>pub struct Builder&lt;R: Runtime = crate::Wry&gt;<br/><br/>#[cfg(not(feature = 'wry'))]<br/>pub struct Builder&lt;R: Runtime&gt;"]

このマクロは ContextBuilderAppAppHandleWindowWebviewWebviewWindow など、ほぼすべての公開型に適用されています。その結果、デフォルトの WRY フィーチャーを使っている場合は Builder::<Wry>::default() と書く代わりに、単に Builder::default() と書くだけで済みます。

カスタムプロトコルハンドラ

Tauri は webview ランタイムに4つのカスタム URI スキームを登録しています。

スキーム 用途 主要ファイル
tauri:// 埋め込みフロントエンドアセットの配信(または dev サーバーへのプロキシ) protocol/tauri.rs
ipc:// IPC 呼び出しの処理 ipc/protocol.rs
asset:// ローカルファイルシステム上のファイルの配信(スコープチェックあり) protocol/asset.rs
isolation:// isolation パターン用 iframe の配信 protocol/isolation.rs

tauri:// ハンドラは最も複雑です。本番環境では、コンパイル時に圧縮・埋め込まれた EmbeddedAssets からアセットを配信します。パス解決、MIME タイプ検出、CSP ヘッダーの注入、設定ファイルで指定されたカスタムヘッダーの付与なども担当します。モバイル上の dev モード(PROXY_DEV_SERVER = true)では、リクエストをフロントエンドの dev サーバーにプロキシします。

プラットフォーム固有コードと条件付きコンパイル

Tauri のコードベースでは #[cfg(...)] アトリビュートが随所で使われています。主なパターンは次のとおりです。

  • #[cfg(desktop)] / #[cfg(mobile)]tauri-build が設定するカスタム cfg フラグ。デスクトップ(macOS、Windows、Linux)とモバイル(Android、iOS)を区別します
  • #[cfg(target_os = "macos")] — アクティベーションポリシー、Dock の表示制御、アプリメニューの管理など、macOS 固有の API
  • #[cfg(windows)] — HWND アクセス、WebView2 の設定、メッセージフックなど、Windows 固有の API
  • #[cfg(feature = "tray-icon")] — システムトレイのサポートなど、フィーチャーフラグで制御される機能

プラットフォーム固有コードの最も顕著な例が android_binding! マクロです。このマクロは Kotlin/Java の Android ランタイムと Rust バックエンドをつなぐ JNI エントリーポイントを生成します。

macro_rules! android_binding {
    ($domain:ident, $app_name:ident, $main:ident, $wry:path) => {
        ::tauri::wry::android_binding!($domain, $app_name, $wry);
        ::tauri::tao::android_binding!($domain, $app_name, Rust, android_setup, $main, ::tauri::tao);
        // JNI functions for plugin response handling and channel data
    };
}

このマクロが生成するネイティブ関数は、Android の PluginManager.kt からプラグインのレスポンスやチャンネルデータを Rust 側に届けるために呼び出されます。

モバイル統合:Android・iOS ブリッジ

第5回で見たように、モバイルの統合はプラグインシステムを経由して行われます。mobile.rs モジュールは、保留中のプラグイン呼び出しを追跡するためのグローバル状態を管理しています。

flowchart TB
    subgraph "Rust"
        PLUGIN["Plugin code"]
        HANDLE["PluginHandle"]
        PENDING["PENDING_PLUGIN_CALLS<br/>(OnceLock + Mutex + HashMap)"]
    end
    subgraph "Android (JNI)"
        KOTLIN["PluginManager.kt"]
    end
    subgraph "iOS (Swift)"
        SWIFT["Swift Plugin"]
    end

    PLUGIN --> HANDLE
    HANDLE -->|"run_mobile_plugin_method()"| PENDING
    PENDING -->|"JNI call"| KOTLIN
    KOTLIN -->|"handlePluginResponse"| PENDING
    HANDLE -->|"Swift FFI"| SWIFT
    SWIFT -->|"callback"| PENDING

Android では、PluginHandle::run_mobile_plugin_method() がリクエストを JSON にシリアライズし、一意の ID を割り当てます。PENDING_PLUGIN_CALLS に oneshot チャンネルの送信側を格納してから、JNI を通じて Kotlin のプラグインメソッドを呼び出します。Kotlin 側の処理が完了すると handlePluginResponseandroid_binding! が生成した JNI 関数)が呼ばれます。ID で保留中の呼び出しを検索して oneshot チャンネル越しにレスポンスを送信し、Rust の Future が解決されます。

iOS では同様のパターンを swift_rs クレートによる Swift FFI で実現しています。ios_plugin_binding! マクロがブリッジ関数の宣言を生成します。

どちらのプラットフォームでも、同じ Runtime 抽象化を使っています。TAO と WRY は Android・iOS バックエンドを持ち、プラットフォームネイティブのコンポーネントを使ってウィンドウと webview のトレイトを実装しています。そのため、Tauri の上位層(Manager、IPC、プラグイン、セキュリティ)はすべてのプラットフォームで同一の動作をします。

ヒント: モバイルプラグインの問題をデバッグする際は、PENDING_PLUGIN_CALLS のフローを確認しましょう。最も多い障害パターンは、ネイティブ側がレスポンスハンドラを呼び忘れることで Rust の Future が永遠に待ち続けるケースです。トレーシングを有効にすると、呼び出し ID の生成と解決の様子を追うことができます。


以上で Tauri コードベースへの深い探索を終えます。このシリーズでは、スタック全体を上から下まで追ってきました。15クレートで構成されるワークスペースのレイアウト、ビルダーパターンとイベントループ、セキュリティを強制する IPC ブリッジ、拡張性を構造化するプラグインシステム、配布可能なアプリケーションを生成する CLI とビルドパイプライン、そして複数の OS をまたいで全体を動かすランタイム抽象化。設計は野心的でありながら、筋が通っています。各レイヤーには明確な責務があり、抽象化はプラットフォーム行列全体において確かにその価値を発揮しています。