フロントエンドから Rust を呼び出す
この文書には、アプリケーションのフロントエンドから Rust コードと通信する方法についての手引が書かれています。 Rust コードからフロントエンドと通信する方法については、前節のRust からフロントエンドを呼び出す を参照してください。
Tauri は、より動的な イベント・システム と共に、型安全性を備えた Rust 関数にアクセスするための コマンド・プリミティブを提供します。
Tauri は、Web アプリから Rust 関数を呼び出すためのシンプルでありながら強力な「コマンド command
」システムを提供しています。
「コマンド」は引数を受け入れ、値を返すことができます。また、エラーを返したり、async
(非同期)にしたりもできます。
コマンドは src-tauri/src/lib.rs
ファイルで定義できます。
コマンドを作成するには、関数を追加し、#[tauri::command]
で注釈を付けるだけです:
#[tauri::command]fn my_custom_command() { println!("I was invoked from JavaScript!");}
コマンドのリストは、次のように、「ビルダー」関数に提供する必要があります。
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![my_custom_command]) .run(tauri::generate_context!()) .expect("error while running tauri application");}
これで、JavaScript コードからコマンドを呼び出すことができます。
// Tauri API の npm パッケージを用いる場合:import { invoke } from '@tauri-apps/api/core';
// Tauri global script を用いる場合(npm パッケージを使わない時)// `tauri.conf.json` の `app.withGlobalTauri` を必ず true に設定してください。const invoke = window.__TAURI__.core.invoke;
// コマンドを呼び出すinvoke('my_custom_command');
アプリケーションで多数のコンポーネントを定義している場合、あるいはコンポーネントがグループ化できる場合、lib.rs
ファイルにコマンド定義を詰め込むのではなく、別のモジュールにコマンドを定義できます。
例として、src-tauri/src/commands.rs
ファイルにコマンドを定義してみましょう。
#[tauri::command]pub fn my_custom_command() { println!("I was invoked from JavaScript!");}
lib.rs
ファイルでは、モジュールを定義し、それに応じたコマンドのリストを提供します:
mod commands;
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![commands::my_custom_command]) .run(tauri::generate_context!()) .expect("error while running tauri application");}
コマンド・リスト内の「プレフィックス commands::
」に注意してください。これは、このコマンド関数への完全なパスを意味しています。
この例にあるコマンド名は my_custom_command
なので、フロントエンド内で invoke("my_custom_command")
を実行して呼び出すことができ、プレフィックスの commands::
は無視されます。
Rust フロントエンドを使用して、引数なしで invoke()
を呼び出す場合は、フロントエンド・コードを以下のように変更する必要があります。
その理由は、Rust がオプションの引数をサポートしていないためです。
#[wasm_bindgen]extern "C" { // 引数なして呼び出す #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)] async fn invoke_without_args(cmd: &str) -> JsValue;
// 引数ありで呼び出す(デフォルト) #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] async fn invoke(cmd: &str, args: JsValue) -> JsValue;
// この二つは別の名前にする必要があります!}
コマンド・ハンドラーは引数を取ることができます:
#[tauri::command]fn my_custom_command(invoke_message: String) { println!("I was invoked from JavaScript, with this message: {}", invoke_message);}
引数は、camelCase キーを持つ JSON オブジェクトとして渡す必要があります。
《訳注》 英語複合語の表記法について: ・camelCase 「キャメルケース」 繋げた単語の初めの文字を大文字にする記法 ・snake_case 「スネークケース」 繋げる単語の間にアンダースコア(_)を入れて全て小文字で書く記法
invoke('my_custom_command', { invokeMessage: 'Hello!' });
引数は、serde::Deserialize
を実装している限り、どのような型でも可能です。
対応する JavaScript:
invoke('my_custom_command', { invoke_message: 'Hello!' });
コマンド・ハンドラーもデータを返します:
#[tauri::command]fn my_custom_command() -> String { "Hello from Rust!".into()}
invoke
関数(「呼び出し」関数)は、戻り値によって処理内容が決まる「promise」を返します:
invoke('my_custom_command').then((message) => console.log(message));
返されるデータは、serde::Serialize
を実装している限り、どの型でも構いません。
serde::Serialize
を実装する戻り値は、応答がフロントエンドに送信されると JSON 形式にシリアル化されます。
この方法では、もしファイルやダウンロード HTTP 応答などの大きなデータを返そうとすると、アプリケーションの速度が低下する可能性があります。
配列バッファを最適化された方法で返すには、tauri::ipc::Response
を使用します。
《訳注》 シリアル化 serialize: 複数の並列データを「直列化」(一列に並べる操作)して、ファイル保存やネットワーク送受信できるように変換する処理。
use tauri::ipc::Response;#[tauri::command]fn read_file() -> Response { let data = std::fs::read("/path/to/file").unwrap(); tauri::ipc::Response::new(data)}
もしハンドラーが処理に失敗しエラーを返す必要がある場合は、コマンド関数に Result
を返させます。
#[tauri::command]fn login(user: String, password: String) -> Result<String, String> { if user == "tauri" && password == "tauri" { // 処理成功 Ok("logged_in".to_string()) } else { // 処理失敗 Err("invalid credentials".to_string()) }}
コマンドがエラーを返す場合、「promise」は「処理失敗」、そうでない場合には「処理成功」となります:
invoke('login', { user: 'tauri', password: '0j4rijw8=' }) .then((message) => console.log(message)) .catch((error) => console.error(error));
上述のように、エラーも含めて、コマンドから返されるものはすべて serde::Serialize
を実装していなければなりません。
ただし、もし Rust の 標準ライブラリまたは外部クレートからのエラー・タイプを使用している場合、そのほとんどのエラー・タイプは serde::Serialize
を実装していないため、問題が発生する可能性があります。
この問題を解決する単純なシナリオは、map_err
を使用してこれらのエラーを String
に変換することです。
#[tauri::command]fn my_custom_command() -> Result<(), String> { std::fs::File::open("path/to/file").map_err(|err| err.to_string())?; // 成功すると `null` を返します Ok(())}
この解決法はあまり一般的ではないので、serde::Serialize
を実装する独自のエラー・タイプを作成することをお勧めします。
次の例では、thiserror
クレートを使用してエラー・タイプを作成しています。
これにより、thiserror::Error
トレイトを派生させることで、列挙型(enum)をエラー型に変換できます。
詳細については、関連文書(英語版)を参照してください。
// プログラムで起こり得るすべてのエラーを表すエラー・タイプを作成する#[derive(Debug, thiserror::Error)]enum Error { #[error(transparent)] Io(#[from] std::io::Error)}
// 「serde::Serialize」を手動で実装する必要がありますimpl serde::Serialize for Error { 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 my_custom_command() -> Result<(), Error> { // これでエラーが返されます std::fs::File::open("path/that/does/not/exist")?; // 成功すると `null` を返します Ok(())}
カスタムのエラー・タイプでは、起こり得るすべてのエラーを明示的に示すという利点があり、どのようなエラーが起こる可能性があるのかをすぐに識別できます。
これにより、後でコードのレビューやリファクタリングを行なうときに、他の人(および自分自身)の時間を大幅に節約できます。
また、エラー・タイプがシリアル化される方法を完全に管理することもできます。
上記の例では、エラー・メッセージを文字列として返すだけでしたが、それぞれのエラーにコードを割り当てて、よく似た見た目の「TypeScript エラー列挙型」に簡単にマップすることができます。たとえば、次のようになります:
#[derive(Debug, thiserror::Error)]enum Error { #[error(transparent)] Io(#[from] std::io::Error), #[error("failed to parse as string: {0}")] Utf8(#[from] std::str::Utf8Error),}
#[derive(serde::Serialize)]#[serde(tag = "kind", content = "message")]#[serde(rename_all = "camelCase")]enum ErrorKind { Io(String), Utf8(String),}
impl serde::Serialize for Error { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::ser::Serializer, { let error_message = self.to_string(); let error_kind = match self { Self::Io(_) => ErrorKind::Io(error_message), Self::Utf8(_) => ErrorKind::Utf8(error_message), }; error_kind.serialize(serializer) }}
#[tauri::command]fn read() -> Result<Vec<u8>, Error> { let data = std::fs::read("/path/to/file")?; Ok(data)}
あなたのフロントエンドは、これで、 { kind: 'io' | 'utf8', message: string }
エラー・オブジェクトが表示されるはずです:
type ErrorKind = { kind: 'io' | 'utf8'; message: string;};
invoke('read').catch((e: ErrorKind) => {});
Tauri では、UI のフリーズや速度の低下を起こさせないために、負荷の高い作業の実行には、「非同期コマンド」が推奨されています。
コマンドを非同期で実行する必要がある場合は、そのコマンドを async
とだけ宣言すれば大丈夫です。
「借用した型」を使用する場合は、追加の変更を加える必要があります。主なオプションは次の二つです:
オプション 1: &str
などの型を、String
などの借用されていない類似の型に変換します。
この方法はすべての型で機能するとは限りません、たとえば State<'_, Data>
など、では。
例:
// 「&str」の代わりに「String」を使用して非同期関数を宣言します(「&str」は借用されておりサポートされていません)#[tauri::command]async fn my_custom_command(value: String) -> String { // 別の非同期関数を呼び出し、その処理が完了するまで待機します some_async_function().await; value}
オプション 2: 戻り値の型を Result
でラップします。こちらの方法は実装が少し難しいですが、すべての型で機能します。
戻り値の型 Result<a, b>
を使用し、a
を「返したい型」で置き換えます。null
を返したい場合は ()
に置き換えます。また、b
を、何か問題が発生した場合に返す「エラー型」に置き換えます。オプションのエラーを返さないようにしたい場合は ()
に置き換えます。たとえば:
Result<String, ()>
「String 型」を返し、エラーは返しません。Result<(), ()>
「null
」を返します。Result<bool, Error>
上記の エラー処理 の項にあるように、「ブール値」または「エラー」を返します。
例:
// 型借用の問題を回避するために、「Result<String, ()>」を返します#[tauri::command]async fn my_custom_command(value: &str) -> Result<String, ()> { // 別の非同期関数を呼び出し、その処理が完了するまで待機します some_async_function().await; // 戻り値は「`Ok()`」でラップする必要があることに注意してください Ok(format!(value))}
JavaScript からのコマンド呼び出しでは「promise」が返されていますので、他のコマンドと同じように動作します:
invoke('my_custom_command', { value: 'Hello, Async!' }).then(() => console.log('Completed!'));
「Tauri チャネル」は、フロントエンドへのストリーミング HTTP 応答のようなストリーミング・データに対する推奨メカニズムです。 次の例では、ファイルを読み取り、フロントエンドに 4096 バイトのチャンク(データ分割単位)で進行状況を通知します:
use tokio::io::AsyncReadExt;
#[tauri::command]async fn load_image(path: std::path::PathBuf, reader: tauri::ipc::Channel<&[u8]>) { // 説明簡潔化のため、この例ではエラー処理は含まれていません let mut file = tokio::fs::File::open(path).await.unwrap();
let mut chunk = vec![0; 4096];
loop { let len = file.read(&mut chunk).await.unwrap(); if len == 0 { // 「長さ=ゼロ」とは、「ファイルの終わり」を意味します break; } reader.send(&chunk).unwrap(); }}
詳細については、「Rust からフロントエンドを呼び出す」の チャネル の説明を参照してください。
コマンドは、メッセージを呼び出した WebviewWindow
のインスタンスにアクセスできます:
#[tauri::command]async fn my_custom_command(webview_window: tauri::WebviewWindow) { println!("WebviewWindow: {}", webview_window.label());}
コマンドは、AppHandle
のインスタンスにアクセスできます:
#[tauri::command]async fn my_custom_command(app_handle: tauri::AppHandle) { let app_dir = app_handle.path_resolver().app_dir(); use tauri::GlobalShortcutManager; app_handle.global_shortcut_manager().register("CTRL + U", move || {});}
Tauri は、tauri::Builder
の manage
関数を使用して「状態 state」を管理できます。
「状態 state」には、tauri::State
を使用したコマンドでアクセスできます:
struct MyState(String);
#[tauri::command]fn my_custom_command(state: tauri::State<MyState>) { assert_eq!(state.0 == "some state value", true);}
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() { tauri::Builder::default() .manage(MyState("some state value".into())) .invoke_handler(tauri::generate_handler![my_custom_command]) .run(tauri::generate_context!()) .expect("error while running tauri application");}
Tauri コマンドでは、ボディ・ペイロード(ヘッダー部を除いた本体部分)の生データとリクエスト・ヘッダーを含むtauri::ipc::Request
オブジェクト全体にもアクセスできます。
#[derive(Debug, thiserror::Error)]enum Error { #[error("unexpected request body")] RequestBodyMustBeRaw, #[error("missing `{0}` header")] MissingHeader(&'static str),}
impl serde::Serialize for Error { 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 upload(request: tauri::ipc::Request) -> Result<(), Error> { let tauri::ipc::InvokeBody::Raw(upload_data) = request.body() else { return Err(Error::RequestBodyMustBeRaw); }; let Some(authorization_header) = request.headers().get("Authorization") else { return Err(Error::MissingHeader("Authorization")); };
// アップロード中 ...
Ok(())}
フロントエンドでは、ペイロード引数に ArrayBuffer または Uint8Array を指定して Raw Request 本文を送信する「invoke()」を呼び出せます。三番目の引数にリクエスト・ヘッダーを含めることも可能です:
const data = new Uint8Array([1, 2, 3]);await __TAURI__.core.invoke('upload', data, { headers: { Authorization: 'apikey', },});
「tauri::generate_handler!
マクロ」はコマンドの配列を受け取ります。複数のコマンドを登録する場合、invoke_handler を複数回呼び出すことはできません。最後の呼び出しのみが使用されます。それぞれのコマンドを一つずつ tauri::generate_handler!
の呼び出しに渡す必要があります。
#[tauri::command]fn cmd_a() -> String { "Command a"}#[tauri::command]fn cmd_b() -> String { "Command b"}
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![cmd_a, cmd_b]) .run(tauri::generate_context!()) .expect("error while running tauri application");}
上記の機能の一部または全部を組み合わせてることができます:
struct Database;
#[derive(serde::Serialize)]struct CustomResponse { message: String, other_val: usize,}
async fn some_other_function() -> Option<String> { Some("response".into())}
#[tauri::command]async fn my_custom_command( window: tauri::Window, number: usize, database: tauri::State<'_, Database>,) -> Result<CustomResponse, String> { println!("Called from {}", window.label()); let result: Option<String> = some_other_function().await; if let Some(message) = result { Ok(CustomResponse { message, other_val: 42 + number, }) } else { Err("No result".into()) }}
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() { tauri::Builder::default() .manage(Database {}) .invoke_handler(tauri::generate_handler![my_custom_command]) .run(tauri::generate_context!()) .expect("error while running tauri application");}
import { invoke } from '@tauri-apps/api/core';
// Invocation from JavaScriptinvoke('my_custom_command', { number: 42,}) .then((res) => console.log(`Message: ${res.message}, Other Val: ${res.other_val}`) ) .catch((e) => console.error(e));
「イベント・システム」は、フロントエンドと Rust 間のより簡潔な通信メカニズムです。 コマンドとは異なり、イベントは型安全ではなく、常に非同期であり、値を返すことができず、JSON ペイロードのみをサポートします。
グローバル・イベントをトリガーするには、event.emit または WebviewWindow#emit 関数が使用できます:
import { emit } from '@tauri-apps/api/event';import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
// emit(eventName, payload)emit('file-selected', '/path/to/file');
const appWebview = getCurrentWebviewWindow();appWebview.emit('route-changed', { url: window.location.href });
個別の Webview によって登録されたリスナーに対してイベントをトリガーするには、event.emitTo または WebviewWindow#emitTo 関数が使用できます:
import { emitTo } from '@tauri-apps/api/event';import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
// emitTo(webviewLabel, eventName, payload)emitTo('settings', 'settings-update-requested', { key: 'notification', value: 'all',});
const appWebview = getCurrentWebviewWindow();appWebview.emitTo('editor', 'file-changed', { path: '/path/to/file', contents: 'file contents',});
@tauri-apps/api
NPM パッケージは、グローバル・イベントと Webview 固有のイベントの両方を検知(リッスン)するための API を提供しています。
-
グローバル・イベントの検知
import { listen } from '@tauri-apps/api/event';type DownloadStarted = {url: string;downloadId: number;contentLength: number;};listen<DownloadStarted>('download-started', (event) => {console.log(`downloading ${event.payload.contentLength} bytes from ${event.payload.url}`);}); -
Webview 固有イベントの検知
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';const appWebview = getCurrentWebviewWindow();appWebview.listen<string>('logged-in', (event) => {localStorage.setItem('session-token', event.payload);});
The listen
関数は、アプリケーションの全「ライフタイム」期間中、イベント・リスナーの登録は維持されたままです。
イベントの検知(リッスン)を停止するには、listen
関数によって返される unlisten
関数を使用できます:
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('download-started', (event) => {});unlisten();
グローバル・イベントも Webview 固有のイベントも、Rust に登録されたリスナーに配信されます。
-
グローバル・イベントの検知
src-tauri/src/lib.rs use tauri::Listener;#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() {tauri::Builder::default().setup(|app| {app.listen("download-started", |event| {if let Ok(payload) = serde_json::from_str::<DownloadStarted>(&event.payload()) {println!("downloading {}", payload.url);}});Ok(())}).run(tauri::generate_context!()).expect("error while running tauri application");} -
Webview 固有イベントの検知
src-tauri/src/lib.rs use tauri::{Listener, Manager};#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() {tauri::Builder::default().setup(|app| {let webview = app.get_webview_window("main").unwrap();webview.listen("logged-in", |event| {let session_token = event.data;// save token..});Ok(())}).run(tauri::generate_context!()).expect("error while running tauri application");}
The listen
関数は、アプリケーションの全「ライフタイム」期間中、イベント・リスナーの登録は維持されたままです。
イベントの検知(リッスン)を停止するには、listen
関数によって返される unlisten
関数を使用できます:
// unlisten outside of the event handler scope:let event_id = app.listen("download-started", |event| {});app.unlisten(event_id);
// unlisten when some event criteria is matchedlet handle = app.handle().clone();app.listen("status-changed", |event| { if event.data == "ready" { handle.unlisten(event.id); }});
さらに、Tauri はイベントを一度だけ検知(リッスン)するためのユーティリティ関数を提供しています:
app.once("ready", |event| { println!("app is ready");});
この場合、イベント・リスナーは最初のトリガー後にすぐに登録が解除されます。
Rust コードからイベントを検知(リッスン)したりイベントを発行したりする方法については、前章「Rust からフロントエンドを呼び出す」の イベント・システムの説明 を参照してください。
【※ この日本語版は、「Feb 22, 2025 英語版」に基づいています】
© 2025 Tauri Contributors. CC-BY / MIT