非同期(async)コマンドを定義する

Recipe ID: rust-005

ネットワークリクエストやファイルI/Oなど、時間のかかる処理をブロッキングせずに行うために async コマンドを使用します。
Tauri コマンドは async fn をそのままサポートしています。

なぜ async が必要なのか?

Rust の通常の関数(async なし)は、処理が完了するまでスレッドを占有します。
もしUIスレッド(メインスレッド)で数秒かかる重い処理を実行してしまうと、その間アプリの画面がフリーズしてしまいます。

async 関数を使うと、処理の待ち時間にスレッドを開放できるため、裏で重い処理をしつつも、アプリの操作性を損なわないようにできます。

前提条件

1. クレート (Tokio) の追加

Tauri は内部で tokio ランタイムを使用していますが、自分で非同期関数(sleepなど)を使うには、プロジェクトにも明示的に tokio を追加する必要があります。
src-tauri ディレクトリで以下のコマンドを実行してください。

cd src-tauri
cargo add tokio --features full

2. Permissions (権限) の設定

src-tauri/capabilities/default.json に以下の権限を追加します。

{
  "permissions": [
    ...
  ]
}

実装例

std::thread::sleep ではなく、非同期対応の tokio::time::sleep を使うのがポイントです。

use std::time::Duration;
use tokio::time::sleep;

// async fn をつけるだけで非同期コマンドになります
#[tauri::command]
async fn fetch_remote_data(url: String) -> Result<String, String> {
    // ログ出力など
    println!("Fetching: {}", url);
    
    // ここで重い処理(待機)のシミュレーション
    // await をつけることで、この待機時間は他の処理(UI描画など)にスレッドを譲ります
    sleep(Duration::from_secs(2)).await;
    
    Ok(format!("Data from {}", url))
}

並列実行上の注意

Tauri の非同期コマンドは、Rust の代表的な非同期ランタイムである Tokio 上で実行されます。

NG な例: async 内でのブロッキング処理

async コマンドの中で、ブロッキング処理(スレッドを占有して止まる処理)を行うのは避けてください。
例えば、std::thread::sleep や、大量の計算ループなどを async 関数内にそのまま書くと、Tokio の実行スレッド全体が止まってしまい、他の非同期タスクも遅延します。

// 悪い例
#[tauri::command]
async fn bad_command() {
    // これは UI はフリーズしませんが(別スレッドプールのため)、
    // 他の非同期コマンドの実行を阻害する可能性があります。
    std::thread::sleep(Duration::from_secs(5)); 
}

解決策: spawn_blocking

CPUを大量に使う計算や、非同期に対応していない古いライブラリを使う場合は、spawn_blocking でラップして実行します。

#[tauri::command]
async fn heavy_computation() -> Result<String, String> {
    let result = tokio::task::spawn_blocking(|| {
        // ここでの処理は専用のスレッドプールで実行されます
        // 重い計算や std::thread::sleep もOK
        std::thread::sleep(Duration::from_secs(2));
        "Done"
    }).await.map_err(|e| e.to_string())?;
    
    Ok(result.to_string())
}

フロントエンドからの呼び出し

フロントエンド(JavaScript/TypeScript)からは、通常のコマンドと同じように呼び出せます。
Rust 側が async かどうかに関わらず、invoke は常に Promise を返します。

import { invoke } from '@tauri-apps/api/core';

async function loadData() {
  try {
    console.log('Fetching data...');
    // invoke は非同期関数なので await で待機します
    // Rust 側の処理が終わるまでここで止まります(UIは止まりません)
    const result = await invoke('fetch_remote_data', { url: 'https://example.com' });
    
    console.log(result); // "Data from https://example.com"
  } catch (error) {
    console.error('Failed to fetch:', error);
  }
}