Rust コマンドに引数を渡す

invoke の第 2 引数で Rust コマンドに値を渡す。引数名が camelCase に変換される規則、構造体での受け取り、型が合わないときの invalid args エラーの読み方を示す。

フロントエンド 対象: Tauri 2.x 更新日: 読了目安: 約9分 front-002
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. キーは Rust の引数名を camelCase にしたもの
  4. JS の値が Rust でどうなるか
  5. 大きなバイナリは第 2 引数にそのまま渡す
  6. 2. バックエンドから実装する (Rust)
  7. 動作確認
  8. よくあるエラーと対処法
  9. OS ごとの違いと注意点
  10. 関連レシピ

invoke の第 2 引数にオブジェクトを渡すと、そのキーと同じ名前の Rust の引数に値が入ります。値はいったん JSON になり、Rust 側で引数の型へ変換(デシリアライズ)されます。迷いやすいのは、キーが Rust の引数名を camelCase にした名前 になることと、型が合わないときは Rust の関数が呼ばれないまま invoke が reject されることです。コマンドの定義と登録の基本は Rust のコマンドを JS から呼び出す (invoke) を参照してください。

前提条件

プラグインは不要です。アプリ自身が定義したコマンドは、既定では capabilities に権限を書かなくても呼べます。構造体で受け取るには serde の derive 機能が必要ですが、テンプレートの src-tauri/Cargo.toml には最初から入っています。

1. フロントエンドから実装する (TypeScript)

キーは Rust の引数名を camelCase にしたもの

#[tauri::command] は既定で、Rust の引数 is_human を JS のキー isHuman として探します。JS も Rust もそれぞれの慣習どおりに書けるようにするための変換です。

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

// Rust: fn greet(name: String, age: u8, is_human: bool) -> String
const message = await invoke<string>('greet', { name: 'Tauri', age: 25, isHuman: true });
console.log(message);

// Rust: fn save_user(user: NewUser) -> String
// 構造体は「引数名のキー」の中に入れる。フィールドを直接並べると missing required key になる
await invoke<string>('save_user', {
  user: { displayName: 'Alice', isAdmin: false, tags: ['beta'] },
});

// Rust: #[tauri::command(rename_all = "snake_case")] fn search_items(search_text: String, max_count: Option<u32>)
// max_count は Option なので省略できる
const items = await invoke<string[]>('search_items', { search_text: 'tauri' });
console.log(items);
Rust の引数JS のキー(既定)rename_all = "snake_case" のとき
namenamename
is_humanisHumanis_human
max_countmaxCountmax_count
r#typetypetype

この変換が掛かるのは 引数名だけ です。構造体の中のフィールド名は変換されず、構造体に付けた serde の属性(#[serde(rename_all = "camelCase")] など)で決まります。「引数は camelCase で通るのに、構造体のフィールドだけ届かない」はこの違いが原因です。rename_all に指定できるのは "camelCase" と "snake_case" の 2 つです。

JS の値が Rust でどうなるか

値は JSON を経由するため、JSON にない値は形が変わります。

  • undefined のキーは送られません。Rust 側が Option<T> なら None、それ以外は「missing required key」のエラーです。null も Option<T> なら None になります。
  • Date は ISO 形式の文字列になるので String で受けます。Map はオブジェクトになり HashMap<String, T> で受けられます。
  • 整数型の引数に小数や範囲外の値(u8 に 300 など)を渡すとエラーです。2^53 を超える ID は JS の number で正確に表せないので、文字列で送って Rust 側で変換します。
  • ファイルパスは PathBuf で受けると、Windows の \ 区切りも他の OS の / 区切りもそのまま扱えます。

大きなバイナリは第 2 引数にそのまま渡す

{ data: bytes } のようにオブジェクトに入れた Uint8Array は数値の配列(JSON)になり、数 MB では変換に時間がかかります。Uint8Array / ArrayBuffer を第 2 引数にそのまま渡すと JSON を経由せずに届きます。ファイル名などの付随情報は第 3 引数の headers で送り、Rust 側は tauri::ipc::Request で受けます。

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

export async function upload(file: File) {
  const bytes = new Uint8Array(await file.arrayBuffer());
  return invoke<number>('upload_bytes', bytes, {
    headers: { 'x-file-name': encodeURIComponent(file.name) }, // ヘッダーは ASCII のみ
  });
}

2. バックエンドから実装する (Rust)

引数の型は serde::Deserialize を実装していれば何でもよく、String・数値・bool・Vec<T>・Option<T>・HashMap はそのまま使えます。自作の構造体には #[derive(Deserialize)] と、JS に合わせる #[serde(rename_all = "camelCase")] を付けます。JS 側が省略しうるフィールドを Option<T> か #[serde(default)] にしておくと、フロントの変更で壊れにくくなります。

tauri::WebviewWindow・tauri::AppHandle・tauri::State を引数に書くと Tauri が値を差し込みます。これらは JS のオブジェクトには入れません。

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

#[tauri::command]
fn greet(name: String, age: u8, is_human: bool) -> String {
    format!("{name} ({age}) human={is_human}")
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] // JS の displayName / isAdmin を受ける
struct NewUser {
    display_name: String,
    is_admin: bool,
    #[serde(default)] // 省略されたら空の Vec
    tags: Vec<String>,
    email: Option<String>, // 省略・null なら None
}

#[tauri::command]
fn save_user(user: NewUser) -> String {
    format!("saved {:?}", user)
}

#[tauri::command(rename_all = "snake_case")] // JS 側も search_text / max_count で渡す
fn search_items(search_text: String, max_count: Option<u32>) -> Vec<String> {
    ["tauri", "tauri-cli", "tauri-plugin-fs"]
        .iter()
        .filter(|s| s.contains(search_text.as_str()))
        .take(max_count.unwrap_or(10) as usize)
        .map(|s| s.to_string())
        .collect()
}

// window は Tauri が差し込む。JS からは { note } だけを渡す
#[tauri::command]
fn whoami(window: tauri::WebviewWindow, note: String) -> String {
    format!("{} から: {}", window.label(), note)
}

#[tauri::command]
fn upload_bytes(request: Request<'_>) -> Result<usize, String> {
    let InvokeBody::Raw(bytes) = request.body() else {
        return Err("バイト列で送ってください".into());
    };
    let name = request
        .headers()
        .get("x-file-name")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("unknown");
    println!("{name}: {} bytes", bytes.len());
    Ok(bytes.len())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            greet, save_user, search_items, whoami, upload_bytes
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

動作確認

npm run tauri dev で起動し、次の関数をボタンなどから呼びます。

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

export async function tryArgs() {
  console.log(await invoke<string>('greet', { name: 'Tauri', age: 25, isHuman: true }));
  console.log(await invoke<string>('save_user', { user: { displayName: 'Alice', isAdmin: false } }));
  // 型の違い・キー名の違いをわざと起こす
  for (const args of [{ name: 'Tauri', age: '25', isHuman: true }, { name: 'Tauri', age: 25, is_human: true }]) {
    try {
      await invoke('greet', args);
    } catch (e) {
      console.error(e); // 引数のエラーは文字列で届く
    }
  }
}

開発者ツールのコンソールに次のように出ます。エラーの文には、どの引数で失敗したかが入ります。

Tauri (25) human=true
saved NewUser { display_name: "Alice", is_admin: false, tags: [], email: None }
invalid args `age` for command `greet`: invalid type: string "25", expected u8
invalid args `isHuman` for command `greet`: command greet missing required key isHuman

よくあるエラーと対処法

  • 「command greet missing required key isHuman」で終わるエラー: キーが見つかりません。JS で is_human と書いた、構造体のフィールドを引数名のキーで包まずに並べた、値が undefined だった、のいずれかです。エラーに出ている名前が Rust 側の期待するキーです。
  • 「invalid type: string "25", expected u8」を含むエラー: 型の不一致です。<input> の value は常に文字列なので、Number() で変換してから渡します。範囲外なら「invalid value: integer 300, expected u8」、null なら「invalid type: null」で始まる文言になります。
  • 「missing field displayName」を含むエラー: 構造体のフィールドが足りないか、名前が違います。rename_all = "camelCase" を付けたなら JS は displayName、付けていないなら display_name です。
  • 「async commands that contain references as inputs must return a Result」: async fn の引数に &str などの参照を使ったときのコンパイルエラーです。String で受けるか、戻り値を Result にします(非同期(async)コマンドを定義する)。
  • 引数のエラーは文字列として reject されます。catch での受け方と独自エラー型は Rust 側で起きたエラーを JS でキャッチする を参照してください。

OS ごとの違いと注意点

  • 引数の変換に OS による違いはありません。
  • tauri::ipc::Request で生のバイト列を受け取る方法は Android では使えません。Android も対象にするなら、通常の引数で受ける経路も用意します。
  • 引数は呼ぶたびに JSON にされるので、大きな配列を短い間隔で何度も送る設計は避けます。Rust 側に置ける状態は Rust 側で持ち、JS からは ID だけを渡します。
  • 構造体と JSON の変換規則(rename・default・enum の形など)は Serde で JSON のシリアライズを行う にまとめています。戻り値の形は Rust コマンドからの戻り値を受け取る を参照してください。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する