Tauri を拡張する:プラグインアーキテクチャと拡張モデル
前提知識
- ›第 1〜4 回:アーキテクチャ、ライフサイクル、IPC、セキュリティ
- ›Rust のトレイトオブジェクトとダイナミックディスパッチ
- ›build.rs スクリプトとコンパイル時コード生成の基礎知識
Tauri を拡張する:プラグインアーキテクチャと拡張モデル
Tauri のプラグインシステムは、サードパーティ向けの拡張機能ではありません — フレームワーク自身のコア機能を実装するための仕組みでもあります。イベント、ウィンドウ管理、webview 管理、トレイアイコン — これらはすべてプラグインとして実装されています。この設計の選択は、アーキテクチャの根本にある考え方を示しています。フレームワーク自身の機能をプラグインとして表現できるということは、プラグイン API がどんな用途にも対応できるほど強力でなければならないということです。
Plugin トレイト:ライフサイクルフック
Plugin<R> トレイト は、プラグインが実装できる完全なインターフェースを定義しています。
| フック | 呼び出しタイミング | 用途 |
|---|---|---|
name() |
常時 | プラグインの文字列識別子を返す |
initialize() |
App::build() 時 |
アプリハンドルとプラグイン設定を受け取る |
initialization_script() |
webview 作成時 | ページ読み込み前に注入する JS |
window_created() |
ウィンドウ作成後 | 新しいウィンドウへの反応処理 |
webview_created() |
webview 作成後 | 新しい webview への反応処理 |
on_navigation() |
ナビゲーション前 | false を返すとナビゲーションをキャンセル |
on_page_load() |
ページ読み込み後 | ページロードイベントへの反応処理 |
on_event() |
イベントループの各ティック | RunEvent のディスパッチを受け取る |
extend_api() |
IPC ディスパッチ時 | プラグインコマンドを処理する |
sequenceDiagram
participant App as App::build()
participant Plugin as Plugin
participant WV as New Webview
App->>Plugin: initialize(app_handle, config)
Note over Plugin: Setup state, start services
App->>Plugin: initialization_script()
Note over Plugin: Return JS to inject
WV->>Plugin: window_created(window)
WV->>Plugin: webview_created(webview)
WV->>Plugin: on_navigation(webview, url)
WV->>Plugin: on_page_load(webview, payload)
loop Event Loop
App->>Plugin: on_event(app_handle, event)
end
Note over WV: IPC call arrives
WV->>Plugin: extend_api(invoke)
すべてのフックには何もしないデフォルト実装が用意されているため、プラグインは必要なフックだけを実装すればよい設計になっています。このトレイトは Send を要求します — プラグインは Mutex の内部に保持され、どのスレッドからもアクセスできるようになっています。
Plugin Builder:流れるような構築 API
Plugin<R> を直接実装することもできますが、Builder を使うとより人間工学的に書けます。
pub struct Builder<R: Runtime, C: DeserializeOwned = ()> {
name: &'static str,
invoke_handler: Box<InvokeHandler<R>>,
setup: Option<Box<SetupHook<R, C>>>,
js_init_script: Option<InitializationScript>,
on_navigation: Box<OnNavigation<R>>,
on_page_load: Box<OnPageLoad<R>>,
on_window_ready: Box<OnWindowReady<R>>,
on_webview_ready: Box<OnWebviewReady<R>>,
on_event: Box<OnEvent<R>>,
on_drop: Option<Box<OnDrop<R>>>,
uri_scheme_protocols: HashMap<String, Arc<UriSchemeProtocol<R>>>,
}
ジェネリックパラメータ C: DeserializeOwned はプラグインの設定型です。initialize() が呼ばれると、フレームワークは tauri.conf.json からプラグイン名をキーとして設定セクションを取り出し、C にデシリアライズして PluginApi 経由で利用できるようにします。
classDiagram
class Builder~R, C~ {
+new(name) Self
+invoke_handler(handler) Self
+setup(hook) Self
+js_init_script(script) Self
+on_navigation(handler) Self
+on_page_load(handler) Self
+on_event(handler) Self
+register_uri_scheme_protocol(name, handler) Self
+build() TauriPlugin~R, C~
}
class TauriPlugin~R, C~ {
-name: &'static str
-app: Option~AppHandle~
-invoke_handler
-setup
// ... all fields from Builder
}
class Plugin~R~ {
<<trait>>
+name() &str
+initialize() Result
+extend_api(Invoke) bool
}
Builder --> TauriPlugin : build()
TauriPlugin ..|> Plugin : implements
慣例として、プラグインを構築して返す init() 関数をエクスポートします。
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("my-plugin")
.invoke_handler(tauri::generate_handler![my_command])
.setup(|app, api| {
// initialization logic
Ok(())
})
.build()
}
PluginStore とコマンドの名前空間
crates/tauri/src/app/plugin.rs の PluginStore は、登録されたすべてのプラグインを管理します。プラグインをトレイトオブジェクト(Box<dyn Plugin<R>>)として Vec に格納し、初期化・イベントディスパッチ・コマンドルーティングのためのメソッドを提供します。
名前の衝突は予約済み名チェックによって防がれています。
const RESERVED_PLUGIN_NAMES: &[&str] = &["core", "tauri"];
Builder::try_build() メソッドはこのリストと照合し、プラグインが "core" や "tauri" という名前を使おうとすると BuilderError::ReservedName を返します。これにより、Tauri 内部のプラグイン名前空間との衝突を防いでいます。
第 3 回で見たように、プラグインコマンドは plugin:{name}|{command} という形式で名前空間化されています。このプレフィックスを持つ IPC リクエストが届くと、フレームワークは登録済みプラグインを順に調べ、一致する名前のプラグインを見つけて extend_api() を呼び出します。extend_api の戻り値は bool で、コマンドを処理した場合は true、次に渡す場合は false を返します。
コア内部プラグイン
Tauri は自身のプラグインシステムを使ってコア機能を実装しています。event/plugin.rs を見ると、イベントシステムが "core:event" として登録され、listen・unlisten・emit・emit_to のコマンドを持っていることがわかります。
同様に、app/plugin.rs は version・name・tauri_version・identifier といったアプリメタデータコマンドと、macOS 固有の app_show・app_hide を公開しています。
App::register_core_plugins() で登録されるコアプラグインの全一覧は以下のとおりです。
| プラグイン | 用途 | コマンド |
|---|---|---|
core:event |
イベントシステム | listen, unlisten, emit, emit_to |
core:app |
アプリメタデータ | version, name, tauri_version, identifier |
core:window |
ウィンドウ管理 | create, close, set_title など |
core:webview |
webview 管理 | create, navigate, eval など |
core:resources |
リソーステーブル | close(リソースの破棄) |
core:menu |
メニュー操作 | new, set_text など |
core:tray |
トレイアイコン | new, set_icon, set_tooltip など |
flowchart TB
subgraph "Core Plugins"
EVENT["core:event"]
APP["core:app"]
WINDOW["core:window"]
WEBVIEW["core:webview"]
RES["core:resources"]
MENU["core:menu"]
TRAY["core:tray"]
end
subgraph "User Plugins"
FS["plugin:fs"]
HTTP["plugin:http"]
CUSTOM["plugin:my-plugin"]
end
FRAMEWORK["Tauri Framework"] --> EVENT
FRAMEWORK --> APP
FRAMEWORK --> WINDOW
FRAMEWORK --> WEBVIEW
JS["Frontend JS"] --> |"invoke('plugin:core:event|listen')"| EVENT
JS --> |"invoke('plugin:fs|read_file')"| FS
この「自分で使う」アプローチは、アーキテクチャ上の強いシグナルです — プラグイン API が実際のフレームワーク機能を表現できるほど十分であることを証明し、API が適切に保守され続けることを保証しています。
tauri-plugin ビルドヘルパー
外部プラグインは build.rs の中で tauri-plugin クレートを使い、ACL システムと統合します。このクレートは(ランタイムプラグインの Builder とは別の)Builder を提供し、以下の処理を行います。
- プラグインのパーミッション定義ファイルを読み込む
- 利用可能なパーミッションとデフォルトケイパビリティを記述した ACL マニフェストを生成する
- パーミッション定数の Rust コードを自動生成する
- プラグイン設定の JSON スキーマを生成する
このビルド時の処理により、Tauri アプリがプラグインに依存する際、アプリ自身のビルドプロセスがすべての利用可能なパーミッションを検出し、解決済み ACL に含められるようになります。
ヒント: プラグイン作者は、粗い粒度のパーミッションではなく、細かい粒度のパーミッション(例:
fs:allow-read-file、fs:allow-write-file)を定義しましょう。これにより、アプリ開発者が付与するケイパビリティをきめ細かく制御できます。よく使う組み合わせをまとめる場合は、パーミッションセット(fs:defaultなど)を活用してください。
モバイルプラグインブリッジ
プラグインは crates/tauri/src/plugin/mobile.rs のモバイルブリッジを通じて Android および iOS にも拡張できます。PluginHandle 型が提供する run_mobile_plugin_method() 関数は、リクエストをシリアライズし、ネイティブメソッド(Android では JNI、iOS では Swift)を呼び出し、レスポンスをデシリアライズします。
Android では android_binding! マクロが JNI のグルーコードを生成します。具体的には 2 つの JNI 関数が作られます。
handlePluginResponse— Kotlin プラグインメソッドからのレスポンスを受け取るsendChannelData— Kotlin からのストリーミングチャネルデータを受け取る
グローバルな PENDING_PLUGIN_CALLS マップが進行中のモバイルプラグイン呼び出しを管理しており、単調増加する ID をキーとして使います。ネイティブ側の処理が完了すると、JNI を通じて結果がコールバックされ、待機中の呼び出しの oneshot センダーが Rust の future を解決します。
プラグイン実装のウォークスルー
リポジトリには examples/api/src-tauri/tauri-plugin-sample/ にサンプルプラグインが含まれており、全体の構造を確認できます。このサンプルには以下が含まれます。
tauri-pluginのビルドヘルパーを使ったbuild.rs- パーミッション定義ファイル
init()→Builder::new("sample")→.build()の流れを持つ Rust ランタイム実装.invoke_handler(tauri::generate_handler![...])によるコマンド登録
このサンプルは、ビルド時の ACL 生成・ランタイムプラグイン登録・コマンドハンドリング・セキュリティ統合といったすべてのパーツがどう組み合わさるかを示す、公式のテンプレートとして機能します。
flowchart TB
BUILD["build.rs<br/>tauri-plugin::Builder"] --> MANIFEST["ACL manifest<br/>(permissions.json)"]
INIT["init() function"] --> BUILDER["plugin::Builder::new('sample')"]
BUILDER --> HANDLER[".invoke_handler(...)"]
HANDLER --> SETUP[".setup(|app, api| {...})"]
SETUP --> PLUGIN[".build() → TauriPlugin"]
PLUGIN --> APP["app.plugin(sample::init())"]
APP --> STORE["PluginStore"]
次回はフレームワーク層からツールチェーン層へと移り、CLI がビルドをどのようにオーケストレートするか、設定がどのように解決されるか、バンドラーがどのようにプラットフォーム固有のパッケージを生成するかを見ていきます。