mcpField
C
calling-rust-from-tauri-frontend

calling-rust-from-tauri-frontend

github.com/dchuk開発36350 回インストール40 週間前に更新
リポジトリ

calling-rust-from-tauri-frontend について

Tauri v2 の開発者向けに、JavaScript や TypeScript から Rust のコマンドを呼び出す方法を解説するスキルです。引数、戻り値、エラー、非同期処理、ストリーミングを扱います。

主な機能

  1. 01コマンドの登録と呼び出し
  2. 02Rust と JavaScript の引数対応
  3. 03シリアライズ結果とエラー処理
  4. 04非同期コマンドとストリーミングチャネル

活用シーン

  1. 01Tauri フロントエンドから Rust 関数を呼び出す
  2. 02構造化データやバイナリ応答を返す
  3. 03フロントエンドでバックエンドのエラーを処理する

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dchuk/claude-code-tauri-skills --skill calling-rust-from-tauri-frontend

インストール後、Claude Code で/calling-rust-from-tauri-frontendを入力して読み込まれたか確認します。

詳細

Tauri フロントエンドから Rust を呼び出す

このスキルでは、コマンドシステムと invoke 関数を使って、Tauri v2 のフロントエンドから Rust バックエンド関数を呼び出す方法を説明します。

概要

Tauri には 2 つの IPC メカニズムがあります。

  • コマンド(推奨):引数と戻り値をシリアライズする、型安全な関数呼び出しです
  • イベント:動的な一方向通信です(ここでは扱いません)

基本的なコマンド

Rust でコマンドを定義する

#[tauri::command] 属性マクロを使用します。

// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: String) -> String {
    format!("Hello, {}!", name)
}
コマンドを登録する

コマンドは invoke ハンドラーに登録する必要があります。

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet, login, fetch_data])
        .run(tauri::generate_context!())
        .expect("error while running tauri application")
}
JavaScript/TypeScript から呼び出す
import { invoke } from '@tauri-apps/api/core';

const greeting = await invoke('greet', { name: 'World' });
console.log(greeting); // "Hello, World!"

グローバル Tauri オブジェクトを使うこともできます(app.withGlobalTauri が有効な場合)。

const { invoke } = window.__TAURI__.core;
const greeting = await invoke('greet', { name: 'World' });

引数を渡す

引数の命名規則

デフォルトでは、Rust の snake_case 引数は JavaScript の camelCase に対応します。

#[tauri::command]
fn create_user(user_name: String, user_age: u32) -> String {
    format!("{} is {} years old", user_name, user_age)
}
await invoke('create_user', { userName: 'Alice', userAge: 30 });

rename_all を使うと、命名規則を変更できます。

#[tauri::command(rename_all = "snake_case")]
fn create_user(user_name: String, user_age: u32) -> String {
    format!("{} is {} years old", user_name, user_age)
}
複雑な引数

引数は serde::Deserialize を実装する必要があります。

use serde::Deserialize;

#[derive(Deserialize)]
struct UserData {
    name: String,
    email: String,
    age: u32,
}

#[tauri::command]
fn register_user(user: UserData) -> String {
    format!("Registered {} ({}) age {}", user.name, user.email, user.age)
}
await invoke('register_user', {
    user: { name: 'Alice', email: '[email protected]', age: 30 }
});

値を返す

単純な戻り値の型

戻り値の型は serde::Serialize を実装する必要があります。

#[tauri::command]
fn get_count() -> i32 { 42 }

#[tauri::command]
fn get_message() -> String { "Hello from Rust!".into() }
const count: number = await invoke('get_count');
const message: string = await invoke('get_message');
複雑な型を返す
use serde::Serialize;

#[derive(Serialize)]
struct AppConfig {
    theme: String,
    language: String,
    notifications_enabled: bool,
}

#[tauri::command]
fn get_config() -> AppConfig {
    AppConfig {
        theme: "dark".into(),
        language: "en".into(),
        notifications_enabled: true,
    }
}
interface AppConfig {
    theme: string;
    language: string;
    notificationsEnabled: boolean;
}
const config: AppConfig = await invoke('get_config');
バイナリデータを返す

大きなバイナリデータには、JSON シリアライズを回避するために tauri::ipc::Response を使用します。

use tauri::ipc::Response;

#[tauri::command]
fn read_file(path: String) -> Response {
    let data = std::fs::read(&path).unwrap();
    Response::new(data)
}
const data: ArrayBuffer = await invoke('read_file', { path: '/path/to/file' });

エラー処理

Result 型を使用する

E が Serialize を実装するか、String である Result<T, E> を返します。

#[tauri::command]
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Cannot divide by zero".into())
    } else {
        Ok(a / b)
    }
}
try {
    const result = await invoke('divide', { a: 10, b: 0 });
} catch (error) {
    console.error('Error:', error); // "Cannot divide by zero"
}
thiserror を使ったカスタムエラー型
use serde::Serialize;
use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("File not found: {0}")]
    FileNotFound(String),
    #[error("Permission denied")]
    PermissionDenied,
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

impl Serialize for AppError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where S: serde::ser::Serializer {
        serializer.serialize_str(self.to_string().as_ref())
    }
}

#[tauri::command]
fn open_file(path: String) -> Result<String, AppError> {
    if !std::path::Path::new(&path).exists() {
        return Err(AppError::FileNotFound(path));
    }
    let content = std::fs::read_to_string(&path)?;
    Ok(content)
}
構造化されたエラーレスポンス
use serde::Serialize;

#[derive(Debug, Serialize)]
struct ErrorResponse { code: String, message: String }

#[tauri::command]
fn validate_input(input: String) -> Result<String, ErrorResponse> {
    if input.is_empty() {
        return Err(ErrorResponse {
            code: "EMPTY_INPUT".into(),
            message: "Input cannot be empty".into(),
        });
    }
    Ok(input.to_uppercase())
}
interface ErrorResponse { code: string; message: string; }

try {
    const result = await invoke('validate_input', { input: '' });
} catch (error) {
    const err = error as ErrorResponse;
    console.error(`Error ${err.code}: ${err.message}`);
}

非同期コマンド

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

async キーワードを使用します。

#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
    let response = reqwest::get(&url).await.map_err(|e| e.to_string())?;
    let body = response.text().await.map_err(|e| e.to_string())?;
    Ok(body)
}
借用型を使う場合の非同期処理の制限

非同期コマンドでは、&str のような借用型を直接使用できません。

// Will NOT compile:
// async fn bad_command(value: &str) -> String { ... }

// Use owned types instead:
#[tauri::command]
async fn good_command(value: String) -> String {
    some_async_operation(&value).await;
    value
}

// Or wrap in Result as workaround:
#[tauri::command]
async fn with_borrowed(value: &str) -> Result<String, ()> {
    some_async_operation(value).await;
    Ok(value.to_string())
}
フロントエンドからの呼び出し

invoke は Promise を返すため、非同期コマンドも同期コマンドと同じように動作します。

const result = await invoke('fetch_data', { url: 'https://api.example.com/data' });

Tauri の内部機能へのアクセス

WebviewWindow、AppHandle、State
use std::sync::Mutex;

struct AppState { counter: Mutex<i32> }

#[tauri::command]
async fn get_window_label(window: tauri::WebviewWindow) -> String {
    window.label().to_string()
}

#[tauri::command]
async fn get_app_version(app: tauri::AppHandle) -> String {
    app.package_info().version.to_string()
}

#[tauri::command]
fn increment_counter(state: tauri::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_window_label, get_app_version, increment_counter
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application")
}

高度な機能

生のリクエストへのアクセス

ヘッダーと生の本文にアクセスします。

use tauri::ipc::{Request, InvokeBody};

#[tauri::command]
fn upload(request: Request) -> Result<String, String> {
    let InvokeBody::Raw(data) = request.body() else {
        return Err("Expected raw body".into());
    };
    let auth = request.headers()
        .get("Authorization")
        .and_then(|v| v.to_str().ok())
        .ok_or("Missing Authorization header")?;
    Ok(format!("Received {} bytes", data.len()))
}
const data = new Uint8Array([1, 2, 3, 4, 5]);
await invoke('upload', data, { headers: { Authorization: 'Bearer token123' } });
ストリーミング用チャネル
use tauri::ipc::Channel;
use tokio::io::AsyncReadExt;

#[tauri::command]
async fn stream_file(path: String, channel: Channel<Vec<u8>>) -> Result<(), String> {
    let mut file = tokio::fs::File::open(&path).await.map_err(|e| e.to_string())?;
    let mut buffer = vec![0u8; 4096];
    loop {
        let len = file.read(&mut buffer).await.map_err(|e| e.to_string())?;
        if len == 0 { break; }
        channel.send(buffer[..len].to_vec()).map_err(|e| e.to_string())?;
    }
    Ok(())
}
import { Channel } from '@tauri-apps/api/core';

const channel = new Channel<Uint8Array>();
channel.onmessage = (chunk) => console.log('Received:', chunk.length, 'bytes');
await invoke('stream_file', { path: '/path/to/file', channel });

モジュール内でコマンドを整理する

// src-tauri/src/commands/user.rs
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
pub struct CreateUserRequest { pub name: String, pub email: String }

#[derive(Serialize)]
pub struct User { pub id: u32, pub name: String, pub email: String }

#[tauri::command]
pub fn create_user(request: CreateUserRequest) -> User {
    User { id: 1, name: request.name, email: request.email }
}
// src-tauri/src/commands/mod.rs
pub mod user;
// src-tauri/src/lib.rs
mod commands;

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

TypeScript の型安全性

型付きラッパーを作成します。

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

export interface User { id: number; name: string; email: string; }
export interface CreateUserRequest { name: string; email: string; }

export const commands = {
    createUser: (request: CreateUserRequest): Promise<User> =>
        invoke('create_user', { request }),
    greet: (name: string): Promise<string> =>
        invoke('greet', { name }),
};

// Usage
const user = await commands.createUser({ name: 'Bob', email: '[email protected]' });

クイックリファレンス

タスクRustJavaScript
コマンドを定義する#[tauri::command] fn name() {}-
コマンドを登録するtauri::generate_handler![name]-
コマンドを呼び出す-await invoke('name', { args })
値を返す-> T(T: Serialize)const result = await invoke(...)
エラーを返す-> Result<T, E>try/catch
非同期コマンドasync fn name()同期コマンドと同じ
ウィンドウにアクセスするwindow: tauri::WebviewWindow-
アプリにアクセスするapp: tauri::AppHandle-
状態にアクセスするstate: tauri::State<T>-

主な制約

  1. コマンド名はアプリケーション全体で一意である必要があります
  2. lib.rs 内のコマンドは pub にできません(整理するにはモジュールを使用します)
  3. すべてのコマンドは、1 つの generate_handler! 呼び出しに登録する必要があります
  4. 非同期コマンドでは、&str のような借用型を直接使用できません
  5. 引数は Deserialize を実装し、戻り値の型は Serialize を実装する必要があります

コメント