コンテンツにスキップ
Tauri

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

この文書には、アプリケーションのフロントエンドから Rust コードと通信する方法についての手引が書かれています。 Rust コードからフロントエンドと通信する方法については、前節のRust からフロントエンドを呼び出す を参照してください。

Tauri は、より動的な イベント・システム と共に、型安全性を備えた Rust 関数にアクセスするための コマンド・プリミティブを提供します。

Tauri は、Web アプリから Rust 関数を呼び出すためのシンプルでありながら強力な「コマンド command」システムを提供しています。 「コマンド」は引数を受け入れ、値を返すことができます。また、エラーを返したり、async(非同期)にしたりもできます。

コマンドは src-tauri/src/lib.rs ファイルで定義できます。 コマンドを作成するには、関数を追加し、#[tauri::command] で注釈を付けるだけです:

src-tauri/src/lib.rs
#[tauri::command]
fn my_custom_command() {
println!("I was invoked from JavaScript!");
}

コマンドのリストは、次のように、「ビルダー」関数に提供する必要があります。

src-tauri/src/lib.rs
#[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 ファイルにコマンドを定義してみましょう。

src-tauri/src/commands.rs
#[tauri::command]
pub fn my_custom_command() {
println!("I was invoked from JavaScript!");
}

lib.rs ファイルでは、モジュールを定義し、それに応じたコマンドのリストを提供します:

src-tauri/src/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 のインスタンスにアクセスできます:

src-tauri/src/lib.rs
#[tauri::command]
async fn my_custom_command(webview_window: tauri::WebviewWindow) {
println!("WebviewWindow: {}", webview_window.label());
}

コマンドは、AppHandle のインスタンスにアクセスできます:

src-tauri/src/lib.rs
#[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::Buildermanage 関数を使用して「状態 state」を管理できます。

「状態 state」には、tauri::State を使用したコマンドでアクセスできます:

src-tauri/src/lib.rs
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! の呼び出しに渡す必要があります。

src-tauri/src/lib.rs
#[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");
}

上記の機能の一部または全部を組み合わせてることができます:

src-tauri/src/lib.rs
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 JavaScript
invoke('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 matched
let 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