内部構造を探る: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()
これらの関連型は閉じた体系を成しています。Runtime が Handle を生成し、Handle がディスパッチャーを生成し、ディスパッチャーは関連型として Runtime 型を持ち返す——この循環構造により、単一のランタイム実装の中で一貫性が保たれます。
RuntimeHandle トレイトは、Runtime に対応する Send + Sync + Clone な存在です。Runtime 自体は run() によってイベントループに入ると消費されますが、RuntimeHandle は自由にクローンしてバックグラウンドスレッドに渡せます。ウィンドウや webview の生成メソッドに加え、run_on_main_thread() という脱出口も備えており、メインスレッドへの処理のスケジューリングに使えます。
ウィンドウと Webview のディスパッチャー
GUI フレームワークでは一般に、UI 操作はメインスレッドで行う必要があります。Tauri はこの制約をディスパッチャーパターンで解決しています。WindowDispatch と WebviewDispatch トレイトはそれぞれ crates/tauri-runtime/src/window.rs と crates/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) を返し、AppHandle は RuntimeHandle を保持しているので RuntimeHandle を返します。Window と Webview はディスパッチャーを保持しています。ウィンドウや 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<R: Runtime>"] --> MACRO["default_runtime<br/>proc macro"]
MACRO --> OUTPUT["#[cfg(feature = 'wry')]<br/>pub struct Builder<R: Runtime = crate::Wry><br/><br/>#[cfg(not(feature = 'wry'))]<br/>pub struct Builder<R: Runtime>"]
このマクロは Context、Builder、App、AppHandle、Window、Webview、WebviewWindow など、ほぼすべての公開型に適用されています。その結果、デフォルトの 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 側の処理が完了すると handlePluginResponse(android_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 をまたいで全体を動かすランタイム抽象化。設計は野心的でありながら、筋が通っています。各レイヤーには明確な責務があり、抽象化はプラットフォーム行列全体において確かにその価値を発揮しています。