Tauriの基礎 - Commands

2026年3月4日 (水) 00:37時点におけるWiki (トーク | 投稿記録)による版 (ページの作成:「== 概要 == Tauriの <u>Commands</u> は、フロントエンド (JavaScript / TypeScript) からバックエンド (Rust) の関数を呼び出すための通信メカニズムである。<br> <br> この機能により、ReactやVue等のフロントエンドフレームワークから、Rustで実装された高速で安全な処理を直接実行できる。<br> <br> TauriのCommandsはIPC (Inter-Process Communication) を基盤としており、WebViewプロ…」)
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)

概要

Tauriの Commands は、フロントエンド (JavaScript / TypeScript) からバックエンド (Rust) の関数を呼び出すための通信メカニズムである。

この機能により、ReactやVue等のフロントエンドフレームワークから、Rustで実装された高速で安全な処理を直接実行できる。

TauriのCommandsはIPC (Inter-Process Communication) を基盤としており、WebViewプロセスとRustプロセス間でのデータの送受信を行う。

通信はJSONベースのシリアライズを通じて行われ、型安全性を確保しながらデータのやり取りが可能である。

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

  • 型安全な通信
    Rustの型システムとTypeScriptの型定義により、コンパイル時に型チェックが行われる。
  • JSONシリアライズ
    Serdeクレートを使用して、自動的にJSONへの変換が行われる。
  • 非同期サポート
    非同期関数を定義することで、ブロッキングを回避できる。
  • エラーハンドリング
    Result型を使用して、構造化されたエラー処理が可能である。



前提条件

Commandsを使用するには、以下に示す前提条件を満たしている必要がある。

必要な依存関係

Cargo.tomlファイルに以下に示す依存関係を追加する。

 [dependencies]
 tauri = { version = "2", features = ["unstable"] }
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"


プロジェクト構成

Tauriプロジェクトの基本的な構成を以下に示す。

my-tauri-app/
├── src/                    # フロントエンド (React / TypeScript)
│   ├── App.tsx
│   └── main.tsx
├── src-tauri/              # バックエンド (Rust)
│   ├── src/
│   │   ├── main.rs         # エントリーポイント
│   │   └── lib.rs          # Commands定義
│   └── Cargo.toml
└── package.json


TypeScriptの型定義

フロントエンドで型安全性を確保するために、以下に示すパッケージをインストールする。

npm install @tauri-apps/api



#[tauri::command]マクロ

#[tauri::command] マクロは、Rust関数をフロントエンドから呼び出し可能にするための属性マクロである。

基本的な使い方

マクロを関数に付与することにより、その関数がCommandとして登録される。

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


関数シグネチャの要件

Commandとして定義する関数は、以下に示す要件を満たす必要がある。

  • 引数の型
    引数は、serde::Deserialize を実装している必要がある。
    基本的な型 (String、i32、bool等) や カスタム構造体を使用できる。
  • 戻り値の型
    戻り値は、serde::Serialize を実装している必要がある。
    Result<T, E> 型も使用可能である。(Eは、Serializeを実装している必要がある)
  • ライフタイム
    引数に参照を使用する場合は、ライフタイムの考慮が必要である。


複数の引数を受け取る関数

複数の引数を受け取る関数の例を以下に示す。

 #[command]
 fn calculate(a: i32, b: i32, operation: String) -> Result<i32, String> {
    match operation.as_str() {
       "add" => Ok(a + b),
       "subtract" => Ok(a - b),
       "multiply" => Ok(a * b),
       "divide" => {
          if b == 0 {
             Err("Division by zero".to_string())
          }
          else {
             Ok(a / b)
          }
       }
       _ => Err(format!("Unknown operation: {}", operation)),
    }
 }



invoke関数による呼び出し

フロントエンドからCommandを呼び出すには、invoke関数を使用する。

JavaScript / TypeScript側の実装

@tauri-apps/api パッケージから invoke 関数をインポートして使用する。

 import { invoke } from '@tauri-apps/api/core'
 
 // 基本的な呼び出し
 const result = await invoke<string>('greet', { name: 'Tauri' })
 console.log(result) // "Hello, Tauri!"
 
 // エラーハンドリング付き
 try {
    const result = await invoke<number>('calculate', {
       a: 10,
       b: 5,
       operation: 'add'
    })
    console.log(result)  // 15
 }
 catch (error) {
    console.error('Error:', error)
 }


Reactでの使用パターン

ReactコンポーネントでCommandを呼び出す例を以下に示す。

 import { useState } from 'react'
 import { invoke } from '@tauri-apps/api/core'
 
 interface GreetingProps {
   initialName?: string
 }
 
 function Greeting({ initialName = '' }: GreetingProps) {
   const [name, setName] = useState(initialName)
   const [greeting, setGreeting] = useState('')
   const [loading, setLoading] = useState(false)
   const [error, setError] = useState<string | null>(null)
 
   const handleGreet = async () => {
     setLoading(true)
     setError(null)
     try {
       const result = await invoke<string>('greet', { name })
       setGreeting(result)
     }
     catch (err) {
       setError(err as string)
     }
     finally {
       setLoading(false)
     }
   }
 
   return (
     <div>
       <input
         type="text"
         value={name}
         onChange={(e) => setName(e.target.value)}
         placeholder="名前を入力"
       />
       <button onClick={handleGreet} disabled={loading}>
         {loading ? '処理中...' : '挨拶'}
       </button>
       {greeting && <p>{greeting}</p>}
       {error && <p style={{ color: 'red' }}>{error}</p>}
     </div>
   )
 }
 
 export default Greeting


型定義の自動生成

Tauri v2では、tauri-codegen クレートを使用して、TypeScriptの型定義を自動生成できる。

Cargo.tomlファイルに以下に示す設定を追加する。

 [build-dependencies]
 tauri-build = { version = "2", features = ["codegen"] }


build.rsファイルを作成する。

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


これにより、ビルド時にTypeScriptの型定義ファイルが生成される。


引数の受け渡し (JSONシリアライズ)

Commandへの引数は、JSONシリアライズを通じて渡される。

基本的な引数

基本的な型は自動的にシリアライズされる。

サポートされる基本的な型
Rust型 TypeScript型 説明
String string 文字列
i32, i64 number 整数
f64 number 浮動小数点数
bool boolean 真偽値
Vec<T> T[] 配列
Option<T> null NULL許容
HashMap<K, V> Record<K, V> マップ


複雑なオブジェクトの受け渡し

カスタム構造体を使用して、複雑なオブジェクトを渡すことができる。

  • Rust側の定義
     use serde::Deserialize;
     
     #[derive(Deserialize)]
     struct User {
        id: u32,
        name: String,
        email: String,
        active: bool,
     }
     
     #[command]
     fn create_user(user: User) -> Result<String, String> {
        if user.name.is_empty() {
           return Err("Name is required".to_string())
        }
        Ok(format!("Created user: {} (ID: {})", user.name, user.id))
     }
    

  • TypeScript側の呼び出し
     interface User {
       id: number
       name: string
       email: string
       active: boolean
     }
     
     const createUser = async (user: User) => {
       try {
         const result = await invoke<string>('create_user', { user })
         console.log(result)
       }
       catch (error) {
         console.error('Failed to create user:', error)
       }
     }
     
     // 使用例
     createUser({
       id: 1,
       name: 'John Doe',
       email: 'john@example.com',
       active: true
     })
    


Serdeによるシリアライズ

Serdeクレートを使用して、シリアライズの動作をカスタマイズできる。

 use serde::{Deserialize, Serialize};
 
 // フィールド名のリネーム
 #[derive(Deserialize)]
 #[serde(rename_all = "camelCase")]
 struct UserProfile {
    user_id: u32,
    first_name: String,
    last_name: String,
    created_at: String,
 }
 
 // オプショナルフィールド
 #[derive(Deserialize)]
 struct UpdateRequest {
    #[serde(default)]
    name: Option<String>,
    #[serde(default)]
    email: Option<String>,
 }
 
 // カスタムデシリアライザ
 #[derive(Deserialize)]
 struct Config {
    #[serde(deserialize_with = "parse_version")]
    version: u32,
 }



戻り値の受け取り

Commandからの戻り値は、JSONシリアライズを通じてフロントエンドに返される。

基本的な戻り値

基本的な型を返す例を以下に示す。

 #[command]
 fn get_timestamp() -> u64 {
    std::time::SystemTime::now()
       .duration_since(std::time::UNIX_EPOCH)
       .unwrap()
       .as_secs()
 }
 
 #[command]
 fn get_app_info() -> (String, String) {
    ("MyApp".to_string(), "1.0.0".to_string())
 }


複雑なオブジェクトの返却

構造体を返すことにより、複雑なデータを渡すことができる。

 use serde::Serialize;
 
 #[derive(Serialize)]
 struct FileInfo {
    name: String,
    size: u64,
    is_directory: bool,
    modified: String,
 }
 
 #[command]
 fn get_file_info(path: String) -> Result<FileInfo, String> {
    let metadata = std::fs::metadata(&path)
       .map_err(|e| format!("Failed to read file: {}", e))?;
    
    Ok(FileInfo {
       name: path.split('/').last().unwrap_or(&path).to_string(),
       size: metadata.len(),
       is_directory: metadata.is_dir(),
       modified: metadata.modified()
          .map(|t| format!("{:?}", t))
          .unwrap_or_else(|_| "Unknown".to_string()),
    })
 }


 // TypeScript側での受信
 
 interface FileInfo {
   name: string
   size: number
   is_directory: boolean
   modified: string
 }
 
 const getFileInfo = async (path: string): Promise<FileInfo | null> => {
   try {
     const info = await invoke<FileInfo>('get_file_info', { path })
     return info
   }
   catch (error) {
     console.error('Failed to get file info:', error)
     return null
   }
 }


型安全な受信

TypeScriptのジェネリクスを使用して、型安全に受信する。

 import { invoke } from '@tauri-apps/api/core'
 
 // 型定義
 interface ApiResponse<T> {
   success: boolean
   data: T | null
   error: string | null
 }
 
 interface UserData {
   id: number
   name: string
   email: string
 }
 
 // 型安全なinvokeラッパー
 async function typedInvoke<T>(
   command: string,
   args?: Record<string, unknown>
 ): Promise<T> {
   return invoke<T>(command, args)
 }
 
 // 使用例
 const fetchUser = async (userId: number) => {
   try {
     const response = await typedInvoke<ApiResponse<UserData>>(
       'fetch_user',
       { userId }
     )
 
     if (response.success && response.data) {
       console.log('User:', response.data.name)
     }
     else {
       console.error('Error:', response.error)
     }
   }
   catch (error) {
     console.error('Command failed:', error)
   }
 }



generate_handler!マクロによるコマンド登録

定義したCommandを使用するには、generate_handler! マクロで登録する必要がある。

単一コマンドの登録

基本的な登録方法を以下に示す。

 // src-tauri/src/lib.rs
 use tauri::command;
 
 #[command]
 fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
 }
 
 // エクスポート
 #[tauri::command]
 fn get_version() -> String {
    env!("CARGO_PKG_VERSION").to_string()
 }


 // main.rsでの登録
 // src-tauri/src/main.rs
 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
 fn main() {
    tauri::Builder::default()
       .invoke_handler(tauri::generate_handler![greet, get_version])
       .run(tauri::generate_context!())
       .expect("error while running tauri application");
 }


複数コマンドの登録

複数のCommandを一括で登録する。

 // src-tauri/src/lib.rs
 mod commands;
 
 pub use commands::*;
 
 // src-tauri/src/commands/mod.rs
 pub mod user;
 pub mod file;
 pub mod system;
 
 pub use user::*;
 pub use file::*;
 pub use system::*;


 // src-tauri/src/main.rs
 use my_app::*;
 
 fn main() {
    tauri::Builder::default()
       .invoke_handler(tauri::generate_handler![
          // User commands
          user::create_user,
          user::get_user,
          user::update_user,
          user::delete_user,
          // File commands
          file::read_file,
          file::write_file,
          file::delete_file,
          // System commands
          system::get_system_info,
          system::get_env_var,
       ])
       .run(tauri::generate_context!())
       .expect("error while running tauri application");
 }


main.rsでの設定

Tauri Builderの設定例を以下に示す。

 use tauri::Manager;
 
 fn main() {
    tauri::Builder::default()
       // Commandsの登録
       .invoke_handler(tauri::generate_handler![
          greet,
          get_version,
          calculate,
          create_user,
          get_file_info,
       ])
       // ウィンドウ設定
       .setup(|app| {
          #[cfg(debug_assertions)]
          {
             let window = app.get_webview_window("main").unwrap();
             window.open_devtools();
          }
          Ok(())
       })
       .run(tauri::generate_context!())
       .expect("error while running tauri application");
 }



サンプルコード

Rust側の実装

 // src-tauri/src/lib.rs
 use serde::{Deserialize, Serialize};
 use tauri::command;
 
 // データ構造
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct Todo {
    id: u32,
    title: String,
    completed: bool,
 }
 
 // グローバル状態 (実際のアプリではデータベースを使用)
 static mut TODOS: Vec<Todo> = Vec::new();
 
 // Todo一覧取得
 #[command]
 fn get_todos() -> Vec<Todo> {
    unsafe { TODOS.clone() }
 }
 
 // Todo追加
 #[command]
 fn add_todo(title: String) -> Result<Todo, String> {
    if title.trim().is_empty() {
       return Err("Title cannot be empty".to_string())
    }
 
    unsafe {
       let id = TODOS.len() as u32 + 1;
       let todo = Todo {
          id,
          title,
          completed: false,
       };
       TODOS.push(todo.clone());
       Ok(todo)
    }
 }
 
 // Todo更新
 #[command]
 fn update_todo(id: u32, completed: bool) -> Result<Todo, String> {
    unsafe {
       if let Some(todo) = TODOS.iter_mut().find(|t| t.id == id) {
          todo.completed = completed;
          Ok(todo.clone())
       }
       else {
          Err(format!("Todo with id {} not found", id))
       }
    }
 }
 
 // Todo削除
 #[command]
 fn delete_todo(id: u32) -> Result<(), String> {
    unsafe {
       let initial_len = TODOS.len();
       TODOS.retain(|t| t.id != id);
       if TODOS.len() == initial_len {
          Err(format!("Todo with id {} not found", id))
       }
       else {
          Ok(())
       }
    }
 }


TypeScript / React側の実装

 // src/App.tsx
 import { useState, useEffect } from 'react'
 import { invoke } from '@tauri-apps/api/core'
 
 interface Todo {
   id: number
   title: string
   completed: boolean
 }
 
 function App() {
   const [todos, setTodos] = useState<Todo[]>([])
   const [newTodo, setNewTodo] = useState('')
   const [loading, setLoading] = useState(false)
   const [error, setError] = useState<string | null>(null)
 
   // Todo一覧取得
   const fetchTodos = async () => {
     try {
       const result = await invoke<Todo[]>('get_todos')
       setTodos(result)
     }
     catch (err) {
       setError(err as string)
     }
   }
 
   useEffect(() => {
     fetchTodos()
   }, [])
 
   // Todo追加
   const handleAddTodo = async () => {
     if (!newTodo.trim()) return
     
     setLoading(true)
     setError(null)
     try {
       await invoke<Todo>('add_todo', { title: newTodo })
       setNewTodo('')
       await fetchTodos()
     }
     catch (err) {
       setError(err as string)
     }
     finally {
       setLoading(false)
     }
   }
 
   // Todo更新
   const handleToggleTodo = async (id: number, completed: boolean) => {
     try {
       await invoke<Todo>('update_todo', { id, completed })
       await fetchTodos()
     }
     catch (err) {
       setError(err as string)
     }
   }
 
   // Todo削除
   const handleDeleteTodo = async (id: number) => {
     try {
       await invoke('delete_todo', { id })
       await fetchTodos()
     }
     catch (err) {
       setError(err as string)
     }
   }
 
   return (
     <div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
       <h1>Todo App</h1>
 
       {/* エラー表示 */}
       {error && (
         <div style={{ color: 'red', marginBottom: '10px' }}>
           {error}
           <button onClick={() => setError(null)}>×</button>
         </div>
       )}
 
       {/* Todo追加フォーム */}
       <div style={{ marginBottom: '20px' }}>
         <input
           type="text"
           value={newTodo}
           onChange={(e) => setNewTodo(e.target.value)}
           placeholder="新しいTodoを入力"
           style={{ padding: '8px', marginRight: '10px', width: '300px' }}
           onKeyDown={(e) => e.key === 'Enter' && handleAddTodo()}
         />
         <button 
           onClick={handleAddTodo} 
           disabled={loading}
           style={{ padding: '8px 16px' }}
         >
           {loading ? '追加中...' : '追加'}
         </button>
       </div>
 
       {/* Todo一覧 */}
       <ul style={{ listStyle: 'none', padding: 0 }}>
         {todos.map((todo) => (
           <li
             key={todo.id}
             style={{
               display: 'flex',
               alignItems: 'center',
               padding: '10px',
               borderBottom: '1px solid #ccc'
             }}
           >
             <input
               type="checkbox"
               checked={todo.completed}
               onChange={(e) => handleToggleTodo(todo.id, e.target.checked)}
               style={{ marginRight: '10px' }}
             />
             <span
               style={{
                 flex: 1,
                 textDecoration: todo.completed ? 'line-through' : 'none'
               }}
             >
               {todo.title}
             </span>
             <button
               onClick={() => handleDeleteTodo(todo.id)}
               style={{ color: 'red' }}
             >
               削除
             </button>
           </li>
         ))}
       </ul>
 
       {todos.length === 0 && (
         <p style={{ textAlign: 'center', color: '#666' }}>
           Todoがありません
         </p>
       )}
     </div>
   )
 }
 
 export default App



推奨される事柄

引数の命名規則

Rust側ではスネークケース (snake_case) を使用して、TypeScript側ではキャメルケース (camelCase) を使用する。

 // Rust (snake_case)
 
 #[command]
 fn get_user_by_id(user_id: u32) -> Result<User, String> {
    // ...
 }


 // TypeScript (camelCase)
 
 await invoke<User>('get_user_by_id', { userId: 1 })


大きなデータの取り扱い

大きなデータを渡す場合は、パフォーマンスに注意する。

  • チャンク化
    大きなデータは小さなチャンクに分割して処理する。
  • ストリーミング
    Channel APIを使用して、ストリーミング処理を行う。
  • 圧縮
    必要に応じてデータを圧縮する。


セキュリティ

CommandsはIPCを通じて通信するため、セキュリティに注意する。

  • 入力検証
    全ての入力を検証し、無効なデータを拒否する。
  • 権限管理
    ファイルシステムアクセス等の危険な操作は、必要最小限の権限で実行する。
  • エラーメッセージ
    内部実装の詳細を漏らさないエラーメッセージを返す。


デバッグのヒント

開発時のデバッグに役立つテクニックを以下に示す。

 #[command]
 fn debug_command(input: String) -> String {
    #[cfg(debug_assertions)]
    println!("Debug: input = {}", input);
 
    format!("Processed: {}", input)
 }



関連情報