Read OSS

Tauriのアーキテクチャ:15クレートからなるRustモノレポを読み解く

中級

前提知識

  • Rustの基本知識(トレイト、ジェネリクス、モジュール)
  • Cargoワークスペースとクレート依存関係への理解
  • デスクトップWebviewフレームワークの概要把握

Tauriのアーキテクチャ:15クレートからなるRustモノレポを読み解く

Tauriは、オープンソースのRustプロジェクトの中でも特に野心的な一作です。WebフロントエンドとRustバックエンドを組み合わせ、デスクトップ・モバイルアプリを1つのコードベースから届けられるポリグロットなアプリケーションフレームワークです。ところが、リポジトリを初めてクローンした瞬間、15個のクレート、複数のTypeScriptパッケージ、コンパイル時からランタイムまで広がるビルドパイプラインが一気に目に飛び込んできます。この記事では、そのすべてをスムーズに把握するためのメンタルモデルを紹介します。

モノレポの全体像

リポジトリのトップレベルはシンプルな構造になっています。

ディレクトリ 役割
crates/ すべてのRustクレート — コアフレームワーク、ランタイム、CLI、バンドラー、マクロ、ヘルパー
packages/ TypeScript/JavaScriptパッケージ — @tauri-apps/api JS APIとNAPIベースのCLIラッパー
examples/ APIの全機能を網羅したサンプルアプリなど
bench/ ベンチマーク用ハーネス

ワークスペースルートの Cargo.toml がすべてをまとめ、15のメンバークレートに加えてテスト・サンプルプロジェクトを宣言しています。

graph TB
    subgraph "Workspace Root"
        CT[Cargo.toml]
    end
    subgraph "crates/"
        tauri[tauri]
        runtime[tauri-runtime]
        wry_rt[tauri-runtime-wry]
        macros[tauri-macros]
        utils[tauri-utils]
        build[tauri-build]
        codegen[tauri-codegen]
        plugin[tauri-plugin]
        cli[tauri-cli]
        bundler[tauri-bundler]
        sign[tauri-macos-sign]
        schema[tauri-schema-generator]
        schema_w[tauri-schema-worker]
        driver[tauri-driver]
    end
    subgraph "packages/"
        api["@tauri-apps/api"]
        cli_napi["@tauri-apps/cli (NAPI)"]
    end
    CT --> tauri
    CT --> cli
    CT --> api

ワークスペースでは resolver = "2" が設定されており、Rustの最小バージョンとして1.77.2が要求されます。リリースプロファイルはバイナリサイズの最小化を徹底的に追求しており(opt-level = "s"、LTO有効、コード生成単位を1つに統合)、これはTauriが掲げる「小さなバイナリ」という核心的な価値観を反映しています。

ヒント: Cargo.toml 末尾の [patch.crates-io] セクションでは、tauritauri-plugintauri-utils をローカルパスに固定しています。つまりワークスペース内のすべてのクレートは、crates.ioで公開されたバージョンではなく、常にローカルのソースを参照します。依存バージョンを読む際は、この点を念頭に置いておきましょう。

コアクレート:tauri

tauri クレートはこのリポジトリの主役です。アプリ開発者が依存するのはこのクレートであり、必要なものはほぼすべてここから再エクスポートされています。crates/tauri/src/lib.rs のモジュール宣言を見ると、内部の構成が見えてきます。

モジュール 公開範囲 役割
app pub(crate) Builderパターン、App、AppHandle、RunEvent
ipc pub IPCコマンド、チャンネル、authority
plugin pub Pluginトレイトとビルダー
webview pub WebviewおよびWebviewWindowの型
window pub ウィンドウ管理
event private イベントシステム
manager private AppManager — 中央オーケストレーター
protocol pub(crate) カスタムURIプロトコルハンドラー
state private TypeIdをキーとした状態管理
menu pub(デスクトップのみ) メニューAPI
tray pub(デスクトップ + feature) システムトレイAPI

このクレートは ManagerListenerEmitter という3つの基盤トレイトを定義しており、これらが公開APIの骨格を形成しています。AppAppHandleWindowWebviewWebviewWindow がこれらのトレイトを実装することで、状態へのアクセス、イベントの発行、リソース管理に一貫したインターフェースが提供されます。

classDiagram
    class Manager {
        +app_handle() AppHandle
        +config() Config
        +get_webview_window(label) Option~WebviewWindow~
        +state~T~() State~T~
    }
    class Listener {
        +listen(event, handler) EventId
        +once(event, handler) EventId
        +unlisten(id)
    }
    class Emitter {
        +emit(event, payload) Result
        +emit_to(target, event, payload) Result
    }
    class ManagerBase {
        <<sealed>>
        +manager() AppManager
        +runtime() RuntimeOrDispatch
    }

    Manager --|> ManagerBase : depends on
    Listener --|> ManagerBase : depends on
    Emitter --|> ManagerBase : depends on
    App ..|> Manager
    AppHandle ..|> Manager
    Window ..|> Manager
    Webview ..|> Manager
    WebviewWindow ..|> Manager

1059行目で定義されているsealed化された ManagerBase トレイトは巧みな設計です。内部の AppManager へのアクセスを必須としながら、pub(crate) モジュール内に置くことで外部コードからの実装を防いでいます。つまり ManagerListenerEmitter は事実上sealedであり、利用することはできても、自分の型に実装することはできません。

ランタイム抽象化レイヤー

Tauriは「フレームワークがWebviewランタイムに求めるもの」と「WRY/TAOによる実際の実装」を明確に分離しています。この分離は2つのクレートにまたがります。

tauri-runtime は抽象トレイトを定義します。Runtime トレイトはdispatcher、handle、イベントループproxyに対応する関連型を規定し、ウィンドウ・Webviewの生成やモニター情報の取得などのメソッドを宣言しています。対となる RuntimeHandle トレイトSend + Sync + Clone を満たすhandleを提供し、どのスレッドからでも安全に利用できます。

tauri-runtime-wry はWRY(Webviewレンダリング)とTAO(ウィンドウ管理)を用いた具体的な実装を提供します。両ライブラリを再エクスポートし、抽象トレイトへの橋渡しを行います。

flowchart TB
    subgraph "Application Code"
        App["Your App"]
    end
    subgraph "tauri crate"
        Manager["Manager / Builder"]
    end
    subgraph "tauri-runtime"
        RT["Runtime trait"]
        RTH["RuntimeHandle trait"]
        WD["WindowDispatch trait"]
        WVD["WebviewDispatch trait"]
    end
    subgraph "tauri-runtime-wry"
        Wry["Wry struct"]
    end
    subgraph "External"
        WRY["WRY (webview)"]
        TAO["TAO (windowing)"]
    end

    App --> Manager
    Manager --> RT
    RT -.->|"impl"| Wry
    Wry --> WRY
    Wry --> TAO

メインの tauri クレートは Wry という型エイリアスを定義し、tauri_runtime_wry::Wry<EventLoopMessage> をラップしています。#[default_runtime] proc macroとの組み合わせにより、wry featureが有効な場合はジェネリクスパラメータ R: Runtime が自動で補完されるため、エンドユーザーがこのパラメータを意識する場面はほとんどありません。

コンパイル時クレート:utils、codegen、macros、build

Tauriの処理の多くはコンパイル時に行われます。そのパイプラインを担う4つのクレートを見ていきましょう。

flowchart LR
    CONFIG["tauri.conf.json"] --> UTILS["tauri-utils<br/>Config parsing, ACL types"]
    UTILS --> CODEGEN["tauri-codegen<br/>Asset embedding, context generation"]
    CODEGEN --> MACROS["tauri-macros<br/>#[command], generate_context!, generate_handler!"]
    UTILS --> BUILD["tauri-build<br/>build.rs helpers, ACL resolution"]
    BUILD --> CODEGEN
    MACROS --> APP["Compiled application"]
    BUILD --> APP

tauri-utils はこのパイプラインの土台です。tauri.conf.json のRust表現である Config 構造体、すべてのACL型(capabilities、permissions、scopes)、そして共有ユーティリティを定義しています。ランタイム層への依存を一切持たないため、ビルドスクリプトやproc macroから安全に利用できます。

tauri-codegen はconfigを読み込み、フロントエンドのアセットを探索・圧縮し、Context 構造体をトークンストリームとして生成します。get_config 関数は TAURI_CONFIG 環境変数によるオーバーライドにも対応しており、これによってCLIがマージ済みの設定をコンパイルステップへ受け渡せる仕組みになっています。

tauri-macros は開発者が直接使うproc macroを提供します。コマンド関数に付与する #[command] やdispatchテーブルを構築する generate_handler! が代表例です。コンパイル時のcontextを埋め込む generate_context! やジェネリクスのruntimeパラメータを自動補完する #[default_runtime] も含まれます。

tauri-build はユーザーの build.rs から呼び出すことを想定したクレートです。ACLパーミッションの解決、リソースのコピー、Windowsマニフェストの生成、そして必要に応じて tauri-codegen へのアセット埋め込み処理の委譲を担います。

ツールチェーンクレート:CLI、バンドラー、署名

開発者向けツールチェーンは3つのクレートで構成されています。

tauri-clicargo tauri コマンドの実装本体です。main.rs では cargo-tauri(Cargoサブコマンド)として呼ばれたのか直接実行されたのかを判定し、余分な tauri 引数を除去してから tauri_cli::run() に処理を委ねます。lib.rs のClapベースのコマンド構造には devbuildbundleinitaddandroidiospluginiconsigner など豊富なサブコマンドが並びます。

tauri-bundler はコンパイル済みバイナリを受け取り、各プラットフォーム向けのパッケージを生成します。macOS向けのDMGと .app バンドル、Windows向けのMSI/NSSインストーラー、Linux向けのDEB/RPM/AppImageなどに対応しています。

tauri-macos-sign はmacOSのコード署名と公証(notarization)を担当します。

CLIは packages/cli/ でNAPIモジュールとしてもラップされており、@tauri-apps/cli としてnpm経由で配布されます。JavaScriptフレームワークを使うユーザーの多くは、このnpmパッケージを通じてTauriを操作しています。

JS APIパッケージ

packages/api/ にある @tauri-apps/api パッケージは、フロントエンドコードがRustバックエンドと通信するためのTypeScriptインターフェースを提供します。コアモジュール(packages/api/src/core.ts)がエクスポートする invoke() 関数は、その中心的な存在です。

async function invoke<T>(
  cmd: string,
  args: InvokeArgs = {},
  options?: InvokeOptions
): Promise<T> {
  return window.__TAURI_INTERNALS__.invoke(cmd, args, options)
}

この関数は window.__TAURI_INTERNALS__ に処理を委ねており、このオブジェクトはTauriがすべてのWebviewに注入する初期化スクリプトによって設定されます。Channel クラス(77〜154行目)はRustからJavaScriptへのストリーミングデータ転送をメッセージの順序保証付きで実現しており、プラグインの進捗報告やリアルタイム更新で広く活用されるパターンです。

sequenceDiagram
    participant Frontend as JS Frontend
    participant Internals as __TAURI_INTERNALS__
    participant Protocol as ipc:// Protocol
    participant Rust as Rust Backend

    Frontend->>Internals: invoke("greet", {name: "World"})
    Internals->>Protocol: HTTP POST with headers
    Protocol->>Rust: parse_invoke_request()
    Rust->>Rust: ACL check + command dispatch
    Rust-->>Protocol: Response
    Protocol-->>Frontend: Promise resolves

クレートの依存関係グラフ

各クレートの実際の依存関係を示します。コンパイル時クレート(左側)とランタイムクレート(右側)がはっきりと分かれているのが見て取れます。

graph TD
    tauri_utils["tauri-utils"]
    tauri_runtime["tauri-runtime"]
    tauri_runtime_wry["tauri-runtime-wry"]
    tauri_codegen["tauri-codegen"]
    tauri_macros["tauri-macros"]
    tauri_build["tauri-build"]
    tauri_plugin["tauri-plugin"]
    tauri_main["tauri"]
    tauri_cli["tauri-cli"]
    tauri_bundler["tauri-bundler"]

    tauri_utils --> tauri_runtime
    tauri_utils --> tauri_codegen
    tauri_utils --> tauri_build
    tauri_utils --> tauri_plugin
    tauri_utils --> tauri_cli
    tauri_utils --> tauri_bundler
    tauri_runtime --> tauri_runtime_wry
    tauri_runtime --> tauri_main
    tauri_runtime_wry --> tauri_main
    tauri_codegen --> tauri_macros
    tauri_codegen --> tauri_build
    tauri_macros --> tauri_main
    tauri_build -.->|"build.rs"| tauri_main
    tauri_plugin -.->|"build.rs"| tauri_main
    tauri_bundler --> tauri_cli

この構造はまさに層状のケーキです。最下層に tauri-utils(Tauri固有の依存なし)、その上に抽象インターフェースを定義する tauri-runtime、具体的な実装を提供する tauri-runtime-wry、そしてすべてを統合する tauri が積み重なります。コンパイル時クレート(codegenmacrosbuild)は並行したトラックで動作し、tauri-utils の型を参照しながらも、ランタイム層には一切依存しません。

ヒント: 特定の機能がどのクレートにあるかを調べるときは、次のルールから始めましょう。型や設定に関するものであれば tauri-utils、ビルド時の処理であれば tauri-build または tauri-codegen、ランタイムの動作であれば tauri クレートの内部モジュール、WebviewやWindowの生成に関わるものであれば tauri-runtime または tauri-runtime-wry を確認してみてください。

次の記事では、Tauriアプリケーションのライフサイクルを端から端まで追っていきます。コンパイル時にアセットを埋め込む generate_context! マクロを起点に、Builderパターンを経て、アプリを動かし続けるイベントループに至るまでの全体の流れを解説します。