前回はTauriプロジェクトの作成方法から紹介しました。
Tauriとは、Electronアプリのような、メインプロセス側の処理と、
ブラウザー側のレンダラープロセスとに分離したデスクトップアプリが作れるフレームワークです。
メインプロセス側は、バックエンドともいえますが、Rustで記述します。
レンダラープロセス側はブラウザ表示なので、WEBのフロントエンド技術を使って記述します。
もし、レンダラープロセス側だけでは足りない機能や、
メインプロセス側に処理を依頼したい場合には、フロント側から処理をリクエストするような機能もTauriには備わっています。
今回の記事では、フロントエンドとバックエンドの処理を繋ぐためのコマンド関数(command)について、紹介したいと思います。
コマンド関数でメインプロセスに処理を依頼するといったことができるようになります。
コマンド関数とは
Commandは、Tauriに用意されているフロントエンドから、バックエンド側に処理リクエストを送る為の機能です。
メインプロセスで処理した結果を、レスポンスとしてフロントエンドに返却することもできます。
フロントエンド側からは、tauriAPIを介してリクエストします。
コマンド関数の結果は、Promiseで得られるので、then/catchで取得するか、async/awaitの形で取得することになります。
次に、コマンド関数の簡単な実装例を見てみましょう。
実装サンプル
バックエンド側の実装 (main.rs)
// コマンド関数には、tauri::command属性をつける。
#[tauri::command]
fn my_command() -> Result<String, String> {
println!("このコマンドはバックエンド側のプロセスで動作する");
Ok("hello my command".into())
}
fn main() {
// tauriの起動ビルダー
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![my_command]) // コマンド関数をここで指定する。
.run(tauri::generate_context!())
.expect("failed to run app");
}
#[tauri::command]
属性をつけた関数が、コマンド関数として機能します。
また、作成した関数は、incoke_handlerに登録する必要があります。
tauri::generate_handler!
マクロには複数の指定が可能です。
フロントエンド側にレスポンスを返す場合、コマンド関数の返り値は、Result<T, E>
の形で定義します。
返せるものはプリミティブ型かString、または、jsonに変換できるものを使います。
(構造体を返す場合は、serde_jsonを使ってSerializeできるようにしておく)
エラーで返す場合も同様です。
上記の例は、リザルトもエラー時も結果をStringで返すように定義しています。
フロントエンド側の実装 (App.tsx)
// tauriのAPIを使ってアクセスする。
import {tauri} from '@tauri-apps/api';
/**
* Backend側のコマンド関数、invokeする処理をラップしているだけ。
*/
const myCommand = async (): Promise<string> => {
// invokeメソッドでRust側で実装したコマンド関数をコールできる。
// 結果はPromiseで返るため、async/awaitを使うか、then/catch などで扱えばよい。
return await tauri.invoke("my_command");
}
const App: FC = () => {
const handleMyCommand = useCallback(async () => {
let ret = await myCommand();
console.log("my_command result:", ret);
}, []);
return (
<div className="App">
<button onClick={handleMyCommand}>Send Command</button>
</div>
);
}
フロントエンドの実装は、まず tauriAPIが使えるようにインポートします。
import {tauri} from '@tauri-apps/api';
バックエンド側で定義したコマンド関数をコールするのは、invokeメソッドにコマンド名(Rust側の関数名)を指定します。
invokeメソッドは、Promise<T>
を返すので、コマンド関数で定義した返り値の型と合わせておきましょう。
TypeScriptの場合、型情報を有効に使いたいので、上記の様に関数でラップして定義しておくと、型チェックの恩恵を受けられます。
完成したら、npx tauri dev
などで実行してみましょう。
実行結果は、下図のとおり。
ボタンが一つだけあるUIで、ボタンをクリックすると、バックエンド側のプロセスに処理が移り、my_command関数のprint文が出力されているのがわかります。
コマンド関数が返した結果は、フロントエンド側にPromiseで取得ができています。
ポイントは、
- コマンド関数には、
#[tauri::command]
属性をつける。 - ビルダーの invoke_handlerで、実装した関数を登録する。
- フロントエンド側はTauriAPIを使って、invokeメソッドでコールする。
引数と返り値
コマンド関数を実行する際に、フロンドエンド側から引数を渡して処理させたいことがあります。
その場合、invokeメソッドには、第二引数にargsを取るようになっていて、{key: value, ...}
形式でパラメーターを渡すことが可能です。
指定したキーに応じて、Rust側のコマンド関数の引数名に自動バインドされるので、関数の引数名は合わせて定義する必要があります。
コマンド関数の返り値や引数は構造体であっても、
serde::Serialize
とserde::Deserialize
などでJSON化できればやり取りすることができます。
この例では、指定の人数分だけ、
Personオブジェクトをリストにして返すような処理を書いてみます。
バックエンド側の実装 (main.rs)
/// フロンドエンド側に返すための型
/// serde::Serialize でJSON化できる構造体にしておく。
#[derive(Debug, Serialize)]
struct Person {
name: String,
age: usize,
}
/// 指定の人数だけ、名前と年齢をランダムで生成して返すコマンド関数
///
/// フロントエンド側で指定されたマップのキー名が、引数の変数にバインドされる。
#[tauri::command]
fn get_person(
count: usize
) -> Result<Vec<Person>, String> {
let mut rng = rand::thread_rng();
let list = (0..count)
.map(|_| {
// NAMESは、&str配列
let i = rng.gen_range(0..NAMES.len());
let name = NAMES[i].to_owned();
let age: usize = rng.gen_range(0..=100);
Person { name, age }
})
.collect();
Ok(list)
}
fn main() {
// tauriの起動ビルダー
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
my_command,
get_person, // <- ここに登録するの忘れずに
]) // コマンド関数をここで指定する。
.run(tauri::generate_context!())
.expect("failed to run app");
}
get_personコマンド関数は、Vec<Person>
を返したいので、Person構造体には、#[derive(Serialize)]
属性をつけてJSON化できるように定義しています。
引数には、countという人数を表す変数をとるようにしています。
invokeメソッドのargsでキーバリュー形式で指定されるのを想定したものです。
最後に、追加したコマンド関数はビルダーのinvoke_handlerに登録するのを忘れないようにしましょう。
フロントエンド側の実装 (App.tsx)
/**
* バックエンド側が返す型と合わせる。
*/
type Person = {
name: string,
age: number,
}
/**
* Backend側のコマンド関数、invokeする処理をラップしているだけ。
*/
const getPerson = async (): Promise<[Person]> => {
// コマンド関数に渡す引数は、invokeメソッドの第二引数にマップの形で指定する。
return await tauri.invoke("get_person", {count: 3});
}
const App: FC = () => {
const handleMyCommand = useCallback(async () => {
let ret = await myCommand();
console.log("my_command result:", ret);
}, []);
const handleGetPerson = useCallback(async () => {
let ret = await getPerson();
console.log("get_person result:", ret);
}, []);
return (
<div className="App">
<button onClick={handleMyCommand}>Send Command</button>
<button onClick={handleGetPerson}>Send Command(get_person)</button>
</div>
);
}
フロンドエンド側の実装は、invokeメソッドをラップした関数を定義しています。
invokeメソッドは、第二引数のargsに {key: value, ...}
のパラメータを指定するので、コマンド関数の引数と合わせた、countを指定しています。
返り値は、Personオブジェクトのリストを返す想定なので、TypeScriptの場合は、Rust側で定義したPersonと同じ型を定義しましょう。
返り値はPromise<T>
を返すので、今回はPromise<[Person]>
となります。
実行結果は、下図のとおり。
追加したボタンを押下すると、Personオブジェクトの配列を取得できることが確認できます。
invokeメソッドの第二引数に指定するキー名と、Rust側のコマンド関数の引数名は、自動バインドされるので、名称を合わせておきましょう。
例えば、
count → count
maxAge → max_age
のように、Rust側はスネークケースになるので注意。
まとめ
レンダラープロセス側で出来ない処理は、メインプロセス側にコマンド関数を定義することで、invokeメソッドを介してリクエストすることが可能。
コマンド関数は、レンダラープロセスからリクエストを送って、レスポンスを取得するフローになっています。
Tauriには、APIという形で、すでにフレームワーク側で用意してくれているコマンド関数群がいくつかあります。
- dialog ファイル選択ダイアログなどを表示
- clipboard クリップボードへのコピーなどを制御
- fs ファイルの読み書き処理
- window ウィンドウの最大化や移動、リサイズなどの制御
など、これらもメインプロセス側でしか制御できない処理ですが、APIという形で内部的にはコマンド関数が定義されていて、レンダラープロセス側でインポートして利用することができます。
メインプロセスと疎通するための仕組みとして実は他にも、Eventという仕組みもTauriでは用意されています。
こちらは、双方にイベントをemitするようなフローになるので、コマンド関数では制御できないケースの場合には、調べてみると良いと思います。
また、コマンド関数が使えるようになってくると、メインプロセス側で状態を管理したくなるようなケースが出てくるかと思います。
その場合に、TauriではStateという仕組みが用意されているので、次回の記事ではStateに関して書く予定でいます。
Tauriはまだ、2022年5月7日現在でstableリリースはされていないですが、
かなり使えるところまで来ています。
実際、私も簡単なWindowsのデスクトップアプリを自作して、Microsoft Storeにアプリ登録することができました。
ドキュメントもまだ不足しているとこも多々ありますが、DiscordのTauriチャンネル上で検索すると解決する例も多いので、実際にTauriで何か作ってみたいという方は、チェックしてみるのをおすすめします。