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" のとき |
|---|---|---|
name | name | name |
is_human | isHuman | is_human |
max_count | maxCount | max_count |
r#type | type | type |
この変換が掛かるのは 引数名だけ です。構造体の中のフィールド名は変換されず、構造体に付けた 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: integer300, 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 コマンドからの戻り値を受け取る を参照してください。
