概要

Tauriは、RustバックエンドとWebフロントエンドを組み合わせてデスクトップアプリケーションを開発するためのフレームワークである。
フロントエンドにはReact、Vue、Svelte等の任意のフレームワークを使用でき、バックエンドはRustで記述する。

Tauriプロジェクトは、フロントエンド用のディレクトリとRustバックエンド用の src-tauri/ ディレクトリで構成される。
この2層構造により、Web技術の柔軟性とRustのパフォーマンスを両立できる。

フロントエンドとバックエンドの通信には、Tauriが提供する >invoke API を使用する。
フロントエンドからRustのコマンドを呼び出し、その結果を受け取ることができる。

Tauri v2では、セキュリティ強化のために capabilities/ ディレクトリで権限管理を行う。
アプリケーションが必要とする機能のみを明示的に許可することにより、セキュリティリスクを最小限に抑える。

主な特徴は以下の通りである。

  • Rustバックエンドによる高速で安全な処理
  • Webフロントエンドによる柔軟なUI開発
  • クロスプラットフォーム対応 (Windows、MacOS、Linux)
  • 小さなバイナリサイズと低いメモリ消費
  • Tauri v2による強化された権限管理システム



ディレクトリ構成

Tauriプロジェクトの標準的なディレクトリ構成を以下に示す。

.
├── package.json
├── index.html
├── vite.config.ts
├── node_modules/
├── dist/
├── src/
│   ├── main.tsx
│   ├── App.tsx
│   └── components/
└── src-tauri/
    ├── Cargo.toml
    ├── build.rs
    ├── tauri.conf.json
    ├── src/
    │   ├── main.rs
    │   ├── lib.rs
    │   └── commands.rs
    ├── icons/
    └── capabilities/
        └── default.json


ディレクトリの役割

  • src/
    フロントエンドのソースコードを配置するディレクトリ
  • src-tauri/
    Rustバックエンドのプロジェクトディレクトリ (Cargoパッケージとして管理)
  • src-tauri/capabilities/
    Tauri v2の権限設定ファイルを配置するディレクトリ
  • src-tauri/icons/
    アプリケーションアイコンを配置するディレクトリ


自動生成ディレクトリ

  • node_modules/
    npmパッケージのインストール先 (npm install コマンドで自動生成)
  • dist/
    フロントエンドのビルド成果物 (npm run build コマンドで生成)
  • src-tauri/target/
    Rustのビルド成果物
    (デバッグ: target/debug/、リリース: target/release/)



主要ファイルの役割 (フロントエンド)

main.tsx

main.tsx は、Reactアプリケーションのエントリーポイントである。

 import React from "react";
 import ReactDOM from "react-dom/client";
 import App from "./App";

 ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
   <React.StrictMode><App /></React.StrictMode>,
 );


App.tsx

App.tsx は、アプリケーションのメインコンポーネントである。
invoke 関数を使用して、Rustバックエンドのコマンドを呼び出す。

 import { useState } from "react";
 import { invoke } from "@tauri-apps/api/core";

 function App() {
   const [greeting, setGreeting] = useState("");
   async function greet(name: string) {
     try {
       const result = await invoke<string>("greet", { name });
       setGreeting(result);
     } catch (err) { console.error("Error:", err); }
   }
   return (
     <div>
       <button onClick={() => greet("World")}>Greet</button>
       <p>{greeting}</p>
     </div>
   );
 }
 export default App;


invoke関数

invoke 関数は、フロントエンドからRustバックエンドのコマンドを呼び出すためのAPIである。
@tauri-apps/api/core からインポートして使用する。

  • 第1引数: Rust側で定義したコマンド名 (文字列)
  • 第2引数: コマンドに渡す引数 (オブジェクト形式、省略可能)
  • 戻り値: Rustコマンドの実行結果を含むPromise


エラーハンドリング例:

 async function callRustCommand() {
   try {
     const result = await invoke<string>("get_version");
     console.log("Version:", result);
   } catch (error) { console.error("Command failed:", error); }
 }


index.html

index.html は、アプリケーションのHTMLエントリーポイントである。

 <!DOCTYPE html>
 <html lang="ja">
   <head><meta charset="UTF-8" /><title>Tauri App</title></head>
   <body>
     <div id="root"></div>
     <script type="module" src="/src/main.tsx"></script>
   </body>
 </html>


vite.config.ts

vite.config.ts は、Viteの設定ファイルである。

 import { defineConfig } from "vite";
 import react from "@vitejs/plugin-react";

 export default defineConfig({
   plugins: [react()],
   clearScreen: false,
   server: { port: 1420, strictPort: true },
 });



主要ファイルの役割 (バックエンド)

main.rs

main.rs は、Rustアプリケーションのエントリーポイントである。
実際の処理は lib.rs に委譲する。

 #![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]

 fn main() { app_lib::run() }


Windows環境では、リリースビルド時にコンソールウィンドウを表示しないように設定している。

lib.rs

lib.rs は、Tauriアプリケーションのメインロジックを記述するファイルである。

 #![cfg_attr(mobile, tauri::mobile_entry_point)]

 pub fn run() {
   tauri::Builder::default()
     .invoke_handler(tauri::generate_handler![greet, get_version])
     .run(tauri::generate_context!())
     .expect("error while running tauri application");
 }

 #[tauri::command]
 fn greet(name: &str) -> String { format!("Hello, {}!", name) }


コマンド定義

Tauriでフロントエンドから呼び出し可能なコマンドを定義するには、#[tauri::command] アトリビュートを使用する。

基本的なコマンドと戻り値
 #[tauri::command]
 fn simple_command() { println!("Command called"); }

 #[tauri::command]
 fn greet(name: &str) -> String { format!("Hello, {}!", name) }


非同期コマンド (async)
 #[tauri::command]
 async fn fetch_data(url: String) -> Result<String, String> {
   let response = reqwest::get(&url).await.map_err(|e| e.to_string())?;
   response.text().await.map_err(|e| e.to_string())
 }


引数の命名規則 (rename_all)

Rustのsnake_caseとJavaScriptのcamelCaseの違いを吸収する。

 #[tauri::command]
 fn process_data(#[serde(rename = "userName")] user_name: String) -> String {
   format!("User: {}", user_name)
 }


エラーハンドリング (Result型)
 #[tauri::command]
 fn divide(a: i32, b: i32) -> Result<i32, String> {
   if b == 0 { return Err("Division by zero".to_string()); }
   Ok(a / b)
 }


build.rs

build.rs は、ビルド時に実行されるスクリプトである。

 fn main() { tauri_build::build() }



別モジュールへのコマンド定義

コマンドが増えてくると、lib.rs が肥大化する。
コマンドを別のモジュールに分割して管理する方法を説明する。

commands.rsの作成

 // src-tauri/src/commands.rs

 #[tauri::command]
 pub fn greet(name: &str) -> String { format!("Hello, {}!", name) }

 #[tauri::command]
 pub fn get_version() -> String { env!("CARGO_PKG_VERSION").to_string() }


lib.rsでのインポート

 // src-tauri/src/lib.rs
 mod commands;

 #![cfg_attr(mobile, tauri::mobile_entry_point)]

 pub fn run() {
   tauri::Builder::default()
     .invoke_handler(tauri::generate_handler![commands::greet, commands::get_version])
     .run(tauri::generate_context!())
     .expect("error while running tauri application");
 }



状態管理

Tauriでは、アプリケーション全体で共有される状態を管理できる。
tauri::State を使用して、複数のコマンド間でデータを共有する。

 use std::sync::Mutex;
 use tauri::State;

 struct AppState { counter: Mutex<i32> }

 #[tauri::command]
 fn get_counter(state: State<AppState>) -> i32 {
   *state.counter.lock().unwrap()
 }

 #[tauri::command]
 fn increment_counter(state: State<AppState>) -> i32 {
   let mut counter = state.counter.lock().unwrap();
   *counter += 1;
   *counter
 }

 pub fn run() {
   tauri::Builder::default()
     .manage(AppState { counter: Mutex::new(0) })
     .invoke_handler(tauri::generate_handler![get_counter, increment_counter])
     .run(tauri::generate_context!())
     .expect("error while running tauri application");
 }



設定ファイル

Cargo.toml

Cargo.toml ファイルは、Rustプロジェクトの設定ファイルである。

 [package]
 name = "my-tauri-app"
 version = "0.1.0"
 edition = "2021"
 
 [build-dependencies]
 tauri-build = { version = "2.0.0" }
 
 [dependencies]
 serde = { version = "1.0", features = ["derive"] }
 tauri = { version = "2.0.0", features = [] }
 
 [profile.release]
 panic = "abort"
 lto = true
 opt-level = "s"
 strip = true


Cargo.tomlファイルの主要な設定項目
設定項目 説明
[package] パッケージの基本情報
[build-dependencies] ビルド時に必要な依存クレート
[dependencies] 実行時に必要な依存クレート
[profile.release] リリースビルドの最適化設定


package.json

package.json ファイルは、Node.jsプロジェクトの設定ファイルである。

 {
   "scripts": {
     "dev": "vite",
     "build": "tsc && vite build",
     "tauri": "tauri"
   },
   "dependencies": {
     "@tauri-apps/api": "^2.0.0",
     "react": "^18.2.0"
   },
   "devDependencies": {
     "@tauri-apps/cli": "^2.0.0",
     "typescript": "^5.0.0",
     "vite": "^5.0.0"
   }
 }


tauri.conf.json

tauri.conf.json ファイルは、Tauriアプリケーションの設定ファイルである。

 {
   "productName": "My Tauri App",
   "version": "0.1.0",
   "identifier": "com.example.myapp",
   "build": {
     "beforeDevCommand": "npm run dev",
     "devUrl": "http://localhost:1420",
     "frontendDist": "../dist"
   },
   "app": { "windows": [{ "title": "My Tauri App", "width": 800, "height": 600 }] }
 }



capabilities/ディレクトリ

capabilities/ ディレクトリは、Tauri v2で導入された権限管理システムの設定ファイルを配置する。

Tauri v2権限システム

Tauri v2では、セキュリティ強化のためにcapabilities (権限) システムが導入された。
アプリケーションが必要とする機能のみを明示的に許可することにより、セキュリティリスクを最小限に抑える。

  • プリンシパル (主体)
    権限を付与する対象 (ウィンドウ、Webビュー等)
  • 権限 (Permission)
    特定の機能へのアクセス許可
  • ケイパビリティ (Capability)
    プリンシパルと権限の組み合わせ


設定例

 {
   "$schema": "../gen/schemas/desktop-schema.json",
   "identifier": "main-capability",
   "windows": ["main"],
   "permissions": [
     "core:path:default",
     "core:event:default",
     "core:window:default",
     "core:app:default",
     "shell:allow-open"
   ]
 }


主な権限の一覧
権限 説明
core:path:default ファイルパス操作の基本権限
core:event:default イベントシステムの基本権限
core:window:default ウィンドウ操作の基本権限
core:app:default アプリケーション情報へのアクセス権限
shell:allow-open 外部プログラムの実行許可
dialog:default ファイルダイアログの基本権限
fs:default ファイルシステムの基本権限


プラットフォーム固有のcapability

 {
   "identifier": "platform-capability",
   "windows": ["main"],
   "permissions": [
     { "identifier": "shell:allow-open", "platforms": ["macOS", "windows"] }
   ]
 }



icons/ディレクトリ

icons/ ディレクトリには、アプリケーションのアイコンファイルを配置する。

アイコンファイルの種類
ファイル名 用途 対応プラットフォーム
32x32.png 小サイズアイコン 全プラットフォーム
128x128.png 中サイズアイコン 全プラットフォーム
icon.icns MacOS用アイコン MacOS
icon.ico Windows用アイコン Windows


アイコン生成コマンド

Tauri CLIを使用して、ソース画像から各プラットフォーム用のアイコンを一括生成できる。

npm run tauri icon /path/to/source-icon.png



実用例

フロントエンドからRustコマンドを呼び出す例を示す。

コンポーネントからのRustコマンド呼び出し

 import { useState, useEffect } from "react";
 import { invoke } from "@tauri-apps/api/core";
 
 interface FileInfo { name: string; size: number; }
 
 function FileList() {
   const [files, setFiles] = useState<FileInfo[]>([]);
   const [loading, setLoading] = useState(false);
   async function loadFiles(directory: string) {
     setLoading(true);
     try {
       const result = await invoke<FileInfo[]>("list_files", { directory });
       setFiles(result);
     } finally { setLoading(false); }
   }
   useEffect(() => { loadFiles("."); }, []);
   return (
     <div>
       <h2>File List</h2>
       {loading && <p>Loading...</p>}
       <ul>
         {files.map((file) => (
           <li key={file.name}>{file.name} ({file.size} bytes)</li>
         ))}
       </ul>
     </div>
   );
 }
 export default FileList;


状態管理の例

Rust側でアプリケーション状態を管理する例を示す。

 use std::sync::Mutex;
 use tauri::State;
 
 #[derive(Clone, serde::Serialize)]
 pub struct Todo { id: u32, text: String, completed: bool }
 
 pub struct TodoStore { todos: Mutex<Vec<Todo>> }
 
 #[tauri::command]
 pub fn get_todos(store: State<TodoStore>) -> Vec<Todo> {
    store.todos.lock().unwrap().clone()
 }
 
 #[tauri::command]
 pub fn add_todo(text: String, store: State<TodoStore>) -> Todo {
    let mut todos = store.todos.lock().unwrap();
    let id = todos.len() as u32 + 1;
    let todo = Todo { id, text, completed: false };
    todos.push(todo.clone());
    todo
 }