Read OSS

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.rsPluginStore は、登録されたすべてのプラグインを管理します。プラグインをトレイトオブジェクト(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" として登録され、listenunlistenemitemit_to のコマンドを持っていることがわかります。

同様に、app/plugin.rsversionnametauri_versionidentifier といったアプリメタデータコマンドと、macOS 固有の app_showapp_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 を提供し、以下の処理を行います。

  1. プラグインのパーミッション定義ファイルを読み込む
  2. 利用可能なパーミッションとデフォルトケイパビリティを記述した ACL マニフェストを生成する
  3. パーミッション定数の Rust コードを自動生成する
  4. プラグイン設定の JSON スキーマを生成する

このビルド時の処理により、Tauri アプリがプラグインに依存する際、アプリ自身のビルドプロセスがすべての利用可能なパーミッションを検出し、解決済み ACL に含められるようになります。

ヒント: プラグイン作者は、粗い粒度のパーミッションではなく、細かい粒度のパーミッション(例:fs:allow-read-filefs: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 がビルドをどのようにオーケストレートするか、設定がどのように解決されるか、バンドラーがどのようにプラットフォーム固有のパッケージを生成するかを見ていきます。