Tauri を GUI にして Rust と React に入門する
   4 min read

はじめに

Tauri という、 WebView を利用して GUI を実現する Rust 製フレームワークを試してみました。

Rust も React も入門書から陸続きで書き始められるので、余計なことに気を取られずに済みそうです(ただし、 JS - Rust 間のデータ受け渡しに利用する JSON オブジェクトのシリアライズ/デシリアライズに利用する Serde についての知識は少し必要になりそうでした)。

本エントリは、 Tauri で Hello World するアプリケーションを作成しビルドしてみた過程の記録です。 次のような処理を行うプログラムを作成しました。

  • JS 側でパラメータを伴った コマンド をリクエスト

  • Rust 側でパラメータを基に計算を実行

  • 計算結果を JS 側に返して出力

今回実装したコードはこちらです:

環境

yarn tauri info コマンド実行結果より。

  • Ubuntu 20.04

  • Tauri 1.0.0-beta.8

  • Rust 1.55.0

  • Node 14.18.0

  • React 17.0.2

初期セットアップ

公式ドキュメントに従って進めるだけです。 私は前述の通り Ubuntu 上で開発しているので、 Setup for Linux の Debian の項に書かれているパッケージをそのままインストールしました。

Node や Rust 開発環境は既にセットアップしていたものをそのまま利用しています。

プロジェクト雛形作成と起動

Integrate with Tauri 章をそのまま実行するだけです。

yarn create tauri-app

上記コマンドを実行すると、いくつか作成する雛形について質問されます。

例えば JS フレームワークについて。 私は create-react-app を選択しました。 この後さらに TypeScript テンプレートを利用するか聞かれます。

? What UI recipe would you like to add?
❯ Vanilla.js (html, css, and js without the bundlers)
  create-react-app (https://create-react-app.dev/)
  create-vite (https://vitejs.dev/guide/#scaffolding-your-first-vite-project)
  Vue CLI (https://cli.vuejs.org/)
  Angular CLI (https://angular.io/cli)
  Svelte (https://github.com/sveltejs/template)
  Dominator (https://crates.io/crates/dominator/)

コマンド実行が正常終了すると、次のようなファイル/ディレクトリが生成されます。

create-react-app(以降CRA)で生成したプロジェクトテンプレートに加えて、 src-tauri というディレクトリが生成されています。この後 src-tauri が Rust 側のプロジェクトです。

.
├── README.md
├── node_modules
├── package.json
├── public
├── src
├── src-tauri
├── tsconfig.json
└── yarn.lock

プロジェクトテンプレートが作成できたら次のコマンドで起動できます。

yarn tauri dev

まずCRAの yarn start と同じくウェブブラウザが起動し、その後 Rust のコンパイル完了後に Tauri アプリケーションが起動します。 JS に閉じたアプリケーションであれば、 CRA と同じくウェブブラウザの方を見て開発できそうですが、 API を利用して Rust 側とコミュニケーションする場合は、ウェブブラウザの方は API にアクセスするとエラーになるので利用できません。 (補足: どうもこの挙動は不具合だったらしく、 #2793 で対応が入ったようです)

パッケージングは次のコマンドです。 --verbose は必須ではありませんが、付けないと何か問題が発生した場合でもほとんど情報が出ませんでした。

yarn tauri build --verbose

実装

上記で作成したプロジェクトテンプレートに Create Rust Commands を参照しながら実装していきます。

まず、 JS 側を実装しました(重要でない行は適当に省略しています。全コードはリンク先参照)。

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

function App() {

  const [message, setMessage] = useState("");

  const submit = () => {
    const data: Request = { personalData: { name, birthDay } };
    invoke<Response>("greet", data).then((resp) => {
      setMessage(resp.message);
    });
  };

  return (...);
}

invoke 関数の第1引数にコマンド名、第2引数にJSONでコマンドパラメータを設定し呼び出します。 戻り値は Promise 型です。

続いてこのコマンドに対応する処理を Rust 側に実装します。

#![cfg_attr(
  all(not(debug_assertions), target_os = "windows"),
  windows_subsystem = "windows"
)]

use chrono::{DateTime, Datelike, Local, Utc};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Debug)]
struct PersonalData {
  name: String,
  birthDay: DateTime<Utc>,
}

#[derive(Serialize, Debug)]
struct Response {
  message: String,
}

#[tauri::command]
fn greet(personal_data: PersonalData) -> Response {
  println!("recieve: {:?}", personal_data);
  let age: i32 = calc_age(&personal_data.birthDay, &Local::now());
  let message = format!("こんにちは, {}({}歳)!", personal_data.name, age);

  Response { message }
}

fn calc_age(birth_day: &DateTime<Utc>, now: &DateTime<Local>) -> i32 {
  let year = now.year() - birth_day.year();
  let delta = match now.month() as i32 - birth_day.month() as i32 {
    m if m > 0 => 0,
    m if m < 0 => -1,
    _ => {
      if now.day() as i32 - birth_day.day() as i32 >= 0 {
        0
      } else {
        -1
      }
    }
  };

  year + delta
}

fn main() {
  tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![greet])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

コマンド名と同じ名前の関数を定義し #[tauri::command] アトリビュートを付与、 builder の invoke_handler 関数の引数に設定します。

引数や戻り値は JS の JSON と Rust の構造体に(基本的には(?) Serde の自動で)相互変換されるようでした。

命名規則は決まっており、名前や型が異なると呼び出されませんでした(意図通り動かなかった場合、原因を探すのが少し大変そうです)。

大変そう/大変だったところ

  • 冒頭でも少し触れましたが、 Serde という crate が JS - Rust 間データ変換を担っているので、この crate の知識が少し必要になりそうでした。

    • 今回のコードでいうと、 Date 型のオブジェクトを渡すのに Cargo.toml の dependencieschrono = { version = "0.4.19", features = ["serde"] } を追加する必要がありましたが、解答に辿り着くまで結構時間がかかりました。

  • 解説やサンプルが少ないです。公式/非公式ドキュメントも少なく、 showcase からリンクされているコードを理解しようにも、まず動かすまでにも至れなかったりしました。

    • 公式リポジトリの examples が数少ない情報源でした。今回のことについては commands が該当します。

    • examples 以下のコードを実行するには、リポジトリをチェックアウトして cargo run --example commands (など)。

  • 現状、 JS - Rust 間のやりとりは JSON のみなので、例えばバイナリを高頻度で送受信する必要があるようなアプリケーションではパフォーマンス問題が表出しそうにも思われます。