Rust + TS でマルチプラットフォーム対応のデスクトップアプリを作る　～ Tauri の紹介と基本的な使い方～

## はじめに

本記事は[「Develop fun!」を体現する！ Works Human Intelligenceのカレンダー](https://qiita.com/advent-calendar/2022/advent-works-hi)の 13 日目の記事です。

本記事では、ウェブレンダリングエンジン[^web-renderer]を使用したマルチプラットフォーム対応デスクトップアプリケーションフレームワークである [Tauri](https://tauri.app/) の紹介と、類似のフレームワークである Electron と比較したメリット／デメリットの紹介、そして Tauri を使用した簡単なデスクトップアプリの作成とマルチプラットフォームでのビルドおよび実行をおこなっていきます。

[^web-renderer]: 語弊を恐れずに簡単に言うと、ウェブブラウザ

### 本記事の想定する読者

- シェルの基礎的な操作ができる
- Rust についての基礎的な知識がある
    - Cargo や rustc などの開発環境が既に構築されている
- モダンな ウェブフロントエンド開発（JavaScript / TypeScript や Node.js 、Vue.js / React など）についての基礎的な知識がある
    - JS / TS や npm / yarn などの開発環境が既に構築されている
- モダンなフレームワークを使ってデスクトップアプリを作ってみたいと思っている
    - ウェブフロントエンド開発の知識や経験（もしくは既存のコード）を生かして作ってみたいと思っている
    - せっかくなので、今話題の Rust を使って開発してみたいと思っている

### 本記事が目指すゴール

本記事では、 Tauri を使用したデスクトップアプリ、具体的には「**クリスマスまでの日数と時間をカウントダウンするアプリ**」[^advent]を**マルチプラットフォーム (Windows, macOS, Linux) でビルドし、実行**することをゴールとします。

[^advent]: 申し訳程度のアドベントカレンダー要素

本記事では、そのゴールを達成するために、下記のステップを踏むこととします。

1. Tauri とは何か、その概要を理解する
1. Tauri について、類似のメジャーなフレームワークである Electron と比較したメリット／デメリットを理解する
1. Tauri での開発環境を構築する
1. アドベントカレンダーにちなんで、クリスマスまでの日数をカウントダウンするデスクトップアプリを作成する
1. 作成したアプリが実行できることを確認する
1. 作成したアプリがマルチプラットフォームでビルド・実行できることを確認する

### 本記事の開発・実行環境

Windows 11 64bit で開発・実行をおこない、マルチプラットフォーム対応の検証として MacBook Pro 2021 (macOS Ventura 13.0.1) および その MBP 上の [UTM](https://mac.getutm.app) に構築された仮想マシン Ubuntu 22.04 LTS で実行しました。

いずれの環境でも、執筆時点で最新の stable 版である Rust 1.65.0 および Tauri 1.2 を使用しています。

## Tauri とは

https://tauri.app

ウェブレンダリングエンジンベースのデスクトップアプリケーションフレームワークです。

簡単に言うと、そのアプリ専用のブラウザ画面を表示し、その上でウェブアプリケーションを実行・描画する、という仕組みでデスクトップアプリを動作させるものです。

実際に表示されるのはウェブアプリケーションのため、ウェブフロントエンド技術と親和性が非常に高く、開発経験や既存のコードを生かしやすいのが特徴と言えます。

一方で、タスクバーにアイコンを表示したり、ファイルを読み書きしたり、通知を出したりといった OS ネイティブの機能に依存する部分も API が整備されており、アプリケーションから呼び出して実行することができます。

また、モダンなデスクトップアプリケーションにとっては必須と言える、インストーラ／アンインストーラやアップデータもフレームワークが提供してくれます。

## Electron との比較

類似のフレームワークとして、最も有名なものに [Electron](https://www.electronjs.org) があります。
主に筆者の主観による、 Electron と比較した Tauri のメリットとデメリットを挙げると以下の通りです。

### メリット

#### 生成される実行ファイルのフットプリントが圧倒的に小さい

Electron は Chromium を内包した形でビルドを行うため、生成される実行ファイルが 100 MB を優に超えることが当たり前となっています。
それに対し、 Tauri はレンダリングエンジンとして各々の OS にインストールされたものを使用するため、多くの場合実行ファイルが数 MB の規模に収まります。 

また、実行時のメモリ使用量もTauriのほうが少なくなるケースが多いです。

Tauri の公式サイトに、 Electron / Tauri / [Wry](https://github.com/tauri-apps/wry) の3者で[ベンチマークした結果が記載](https://tauri.app/v1/references/benchmarks)されています。

#### よりセキュリティに配慮されている

Rust 自体がメモリ安全性が高い言語として有名であることに加え、 Tauri は

- OS機能へのアクセスを厳密なホワイトリスト制としている。
    - そのため、「このアプリが OS の機能にアクセスするのはファイルの閲覧だけです。閲覧できるファイルも○○というディレクトリの中のファイルだけです」のような制御・保証が非常に容易。
- [CSP (Content Security Policy)](https://developer.mozilla.org/ja/docs/Web/HTTP/CSP) の制御が容易

といった特徴があります(cf. [Security | Tauri Apps](https://tauri.app/v1/references/architecture/security/))。

#### 開発環境の構築が比較的簡単

Electron の場合は npm / yarn から Electron をセットアップしていく形となるため、「 TypeScript を入れて、 `tsconfig.json` をセットアップして、Electron を入れて、 React を入れて、 Webpack をセットアップして……」とかなり複雑な手順が必要です。

一方で Tauri はそうした雛形を対話形式で作成してくれる `create-tauri-app` が存在するため、比較的簡単に開発環境の構築が可能です。

#### モバイルアプリへのビルドも対応（予定）

公式のロードマップにモバイル端末（ iOS および Android ）への対応が最優先として挙げられており、実際につい先日の12月9日に、[モバイルアプリへのビルドに対応したアルファ版がリリース](https://tauri.app/blog/2022/12/09/tauri-mobile-alpha/)されました。

### デメリット

#### 場所によって Rust と JS / TS という2種類の言語を使い分けなければいけない

Electron は全てが Node.js 上に乗っており、実行プロセス本体（メインプロセス）も画面に表示される部分（レンダラプロセス）も JS / TS で書くことができました。

しかし、 Tauri は実行プロセス本体（[Core プロセス](https://tauri.app/v1/references/architecture/process-model/#the-core-process)）は Rust で書き、画面に表示される部分（[WebView プロセス](https://tauri.app/v1/references/architecture/process-model/#the-webview-process)）は主に JS / TS で書く必要があるため、どうしても言語間のスイッチングや言語の習得の面でコストが高くなると考えられます。

Rust と JS / TS で相互にデータをやりとりする仕組みは[整備されています](https://tauri.app/v1/guides/features/command)ので、開発を進める上で言語間でのデータのやりとりに困る部分は（あまり）ありません[^inter-lang]。

[^inter-lang]: もちろん、無いわけではない。

#### OS ごとに見た目や動作が異なってくる可能性がある

Electron は全ての OS でレンダリングエンジンとして、あらかじめアプリに内包された Chromium を使用するため、基本的には OS ごとの見た目や動作の差異に注意を払わなければならない機会は少ないといえます。

一方で、 Tauri はレンダリングエンジンとして各々の OS にインストールされたものを使用します。

具体的には、 Windows では Microsoft Edge (≒ Chromium )をベースとした Webview2 が使用されます。
ただし、 Windows 11 では Webview 2 がプリインストールされているものの、 Windows 10 やそれよりも古い Windows では、  Webview2 を追加でインストールする必要があるようです。

また、 macOS と Linux では WebKit が使用されます。
基本的には Safari に使用されているものと同じエンジンですが、 OSのバージョンやディストロによって、使用されるエンジンのバージョンにばらつきがあるようです。( cf. [Webview Versions | Tauri Apps](https://tauri.app/v1/references/webview-versions/) )

そのため、実行環境のOSやOSのバージョンなどによって、動作や見た目が異なってくる可能性に注意を払う必要があると考えられます。

#### まだまだコミュニティが小さい

Electron はコミュニティも大きく、何か壁にぶつかってもググれば大抵は解決します。

一方で Tauri はまだまだコミュニティが小さく、公式のドキュメント（もしくはソースコード）以外に参考となる情報が少ないのが実情です。

## 開発環境のセットアップ

:::note
ここでは、既に Rust の開発環境（ `rustc` や`cargo` ）と、JS / TS の開発・実行環境（ Node.js およびそのパッケージマネージャの npm / yarn ）がセットアップされていることを前提とします。
まだセットアップされていない方は、  [rustup](https://rustup.rs) や [Node.js](https://nodejs.org/ja/) からセットアップを完了して下さい。
:::

まず、 `cargo` で `create-tauri-app` をインストールします。
[`create-tauri-app`](https://github.com/tauri-apps/create-tauri-app) は、実行すると対話形式で Tauri アプリの雛形を作ってくれる Tauri 公式ツールです。

```
$ cargo install create-tauri-app
```

インストールが完了したら、作業ディレクトリを生成したいディレクトリ（e.g. `~/Code` ）に移動し、`create-tauri-app` を実行します。

```
$ cd ~/Code
```

```
$ cargo create-tauri-app
```

実行すると、まず

```
$ cargo create-tauri-app

? Project name (tauri-app) ›
```

とプロジェクトの名前を聞かれます。
ここでは「クリスマスまでの日数をカウントダウンするアプリ」を作りたいので、「 `xmas-countdown-app` 」などと入力します。
お好みでカッコいい名前をつけてあげて下さい。

次に、使用するパッケージマネージャを聞かれます。

```
$ cargo create-tauri-app

✔ Project name · xmas-countdown-app
? Choose your package manager ›
  cargo
  pnpm
❯ yarn
  npm
```

ここではウェブフロントエンドメインのアプリであることと、筆者の好みにより、 `yarn` を選びます。
目的や好みに応じて好きなパッケージマネージャを選んで下さい。

[^select-react]: 

最後に、使用するフロントエンドフレームワークを聞かれます。

```
$ cargo create-tauri-app

✔ Project name · xmas-countdown-app
✔ Choose your package manager · yarn
? Choose your UI template ›
  vanilla
  vanilla-ts
  vue
  vue-ts
  svelte
  svelte-ts
  react
❯ react-ts
  solid
  solid-ts
  next
  next-ts
  preact
  preact-ts
  angular
  clojurescript
  svelte-kit
  svelte-kit-ts
```

ここでは、主に筆者の好みにより、 `react-ts` を選択します。
目的や好みに応じて好きなフレームワークを選んで下さい。

```
$ cargo create-tauri-app

✔ Project name · xmas-countdown-app
✔ Choose your package manager · yarn
✔ Choose your UI template · react-ts

Please follow https://tauri.app/v1/guides/getting-started/prerequisites to install the needed prerequisites, if you haven't already.

Done, Now run:
  cd xmas-countdown-app
  yarn
  yarn tauri dev

```

表示されたメッセージに従い、[開発に必要なツールのセットアップガイド](https://tauri.app/v1/guides/getting-started/prerequisites)を参照します。
その上で、必要があればインストールします。

```
$ cd xmas-countdown-app
```

で作業ディレクトリに移動し、

```
$ yarn
```

で必要なパッケージをインストールします。

インストールが完了したら、動作するかを確認するために

```
$ yarn tauri dev
```
を実行してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627744/2456ff18-7452-3a88-2ff6-fd032e3b3036.png)

このようなウィンドウが表示されたらセットアップ成功です。おめでとうございます。

:::note
生成されたプロジェクトディレクトリの中には、Gitで管理するために必要なファイルが `.gitignore` を除いて存在しません。
Gitをつかってバージョン管理をおこないたい場合、このタイミングで

```bash
$ git init
```

しておくのが良いでしょう。
:::

## コードの記述

セットアップが完了して画面が表示できた時点で、本記事の目的の 70% は達成しました。
でもせっかくのアドベントカレンダーなので、クリスマスまでの日数＋時間のカウントダウンをやってみましょう。

### TypeScriptファイル

`main.tsx`, `App.tsx`の各ファイルの中身を下記のように書き換えます。

```main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./style.css";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

```

```App.tsx
import React from "react";
import "./App.css";

interface TimeRemainingResult {
  isTimeRemaining: boolean;
  days: number;
  hours: number;
  minutes: number;
  seconds: number;
}

function getTimeRemaining(currentDate: Date, targetDate: Date): TimeRemainingResult {
  const diffMilliSec = targetDate.getTime() - currentDate.getTime();

  if (diffMilliSec <= 0) {
    return {
      isTimeRemaining: false,
      days: 0,
      hours: 0,
      minutes: 0,
      seconds: 0,
    };
  }

  const days = Math.floor(diffMilliSec / (1000 * 60 * 60 * 24));
  const hours = Math.floor((diffMilliSec / (1000 * 60 * 60)) % 24);
  const minutes = Math.floor((diffMilliSec / (1000 * 60)) % 60);
  const seconds = Math.floor((diffMilliSec / 1000) % 60);

  return {
    isTimeRemaining: true,
    days,
    hours,
    minutes,
    seconds,
  };
}

function XmasCountdown() {
  // The reason for `12 - 1` for month is that the argument of `Date()` is `monthIndex` (i.e. from 0 to 11),
  // instead of the actual month (i.e. from 1 to 12).
  const xmasDate = new Date(2022, 12 - 1, 25);

  const [timeRemaining, setTimeRemaining] = React.useState(getTimeRemaining(new Date(), xmasDate));
  React.useEffect(() => {
    const interval = setInterval(() => {
      setTimeRemaining(getTimeRemaining(new Date(), xmasDate));
    }, 100);
    return () => clearInterval(interval);
  }, []);

  if (!timeRemaining.isTimeRemaining) {
    return (
      <div className="merryXmasStrDiv">
        Merry Christmas!!
      </div>
    );
  }

  return (
    <div className="countdownStrDiv">
      {timeRemaining.days.toString().padStart(3)} 日
      {timeRemaining.hours.toString().padStart(2, "0")} 時間
      {timeRemaining.minutes.toString().padStart(2, "0")} 分
      {timeRemaining.seconds.toString().padStart(2, "0")} 秒
    </div>
  );
}

export default function App() {
  return (
    <div className="countdownContainer">
      <div className="countdownTitleDiv">
        <h1>クリスマスまであと</h1>
      </div>
      <XmasCountdown />
    </div>
  );
}

```

### CSSファイル

続いて、 `style.css`, `App.css` も下記のように書き換えます。 
```style.css
html,
body {
  height: 100%;
  margin: 0;
}

#root {
  height: 100%;
  background-color: red;
  color: white;
}

```

```App.css
.countdownContainer {
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: center;
}

.countdownTitleDiv {
  margin: 24px 0 0;
}

.countdownTitleDiv>h1 {
  margin: 0;
}

.merryXmasStrDiv {
  margin: 0 0 24px;
  background-color: green;
  color: yellow;
  font-size: 32px;
  text-shadow: 0 0 16px white;
}

.countdownStrDiv {
  margin: 0 0 24px;
  background-color: green;
  color: yellow;
  font-size: 32px;
}

```

`src`ディレクトリ内のそれ以外のファイル（各種ロゴ画像など）は使わないので、削除してしまって構いません。

### 設定ファイルの変更

アプリケーションの設定ファイルである `src-tauri/tauri.conf.json` の `identifier` を設定します。
この文字列はアプリケーション毎にユニークな識別子となる必要があるため、デフォルトの  `com.tauri.dev` のままではビルドができません。

```diff_json:src-tauri/tauri.conf.json
-      "identifier": "com.tauri.dev",
+      "identifier": "app.h53.xmas-countdown",
```

ここでは適当に `app.h53.xmas-countdown` としました。

最低限バイナリをビルド・実行するための設定は以上ですが、その他にもお好みで色々設定すると良いでしょう。

これで最低限の準備は整いました。

:::note
そういえば、肝心の Rust のコードには全く触らずにここまで来てしまいました。
実は本記事で実現したい機能は WebView プロセス内で完結できるため、Core プロセス内の Rust のコード部分には特に触る必要がありません。 

`src-tauri` ディレクトリ以下に Rust のコードがあるので、興味のある方はそちらを触ってみてください。
Core プロセス側の実装が必要になる処理（例えば「システムトレイに常駐させる」など）を入れてみるのも面白いかと思います。
:::

全ての準備が完了したら、試しに `yarn tauri dev` で実行してみましょう。

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627744/98d1e48e-0ee5-eaf9-ce1e-5b10da48c6cc.gif)

上記画面のとおり、クリスマス（2022年12月25日0時）までの残り日時をリアルタイムでカウントダウンしてくれるデスクトップアプリが完成しました。

筆者がデザインセンスのカケラもないことがバレる画面ですが、画面の配色がクリスマスっぽくて良いですね🎄

## ビルド・実行

どの OS でも、コードを Git リポジトリ等からクローン

```
$ git clone {リポジトリ}
```

して、必要なパッケージをインストール

```
$ yarn
```

して、ビルド

```
$ yarn tauri build
```

すると、実行可能なバイナリとインストーラが生成されます。
設定すればクロスコンパイルも可能なようですが、今回はそれぞれのOSでビルド・実行しています。

（デフォルトの場合）バイナリは `src-tauri/target/release` の中に、
インストーラは `src-tauri/target/release/bundle` の中に生成されます。

### Windows

<img width = "240px" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627744/d254c67e-0424-9b2e-0652-ba2ae80295be.png" />

実行ファイルが生成されました。
アイコン等は特に設定していないので、デフォルトの Tauri のアイコンになっていますね。
ファイルサイズが 7 MB 弱と、 Electron と比較すると驚くほど小さくなっています。

この実行ファイルを実行すると、先ほど見たカウントダウン画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627744/b5c193da-a67c-b027-e589-acab21c84dde.png)

実行時のメモリ使用量は 61.3 MB ほどでした。

### macOS

<img width = "240px" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627744/1ca47d9f-7587-898f-86c7-3f02804033ea.png" />

macOS でも同様に実行ファイルが生成されました。
こちらもファイルサイズがおよそ 8.3 MB と、充分に小さくなっています。

この実行ファイルを実行すると、

![macOS での実行画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627744/c72c5daa-59ee-0558-49c4-f22a1d416f87.png)

Windows とはフォント等が少し異なります[^font]が、先ほどとほぼ同じ画面が実行されています。

[^font]: これはフォントをCSSで指定しておらず、デフォルトのフォントが使用されているためです。

### Ubuntu

<img width = "360px" src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627744/3ef44c2c-f6c2-b0d8-5b93-c110e5caacb9.png" />

Ubuntu でも同様に実行ファイルが生成されました。
こちらもファイルサイズがおよそ 14.4 MB と、他の OS と比べるとやや大きい気もしますが、充分に小さくなっています。

:::note warn
Ubuntu 環境でのみビルドに失敗する場合、
[Prerequisites](https://tauri.app/v1/guides/getting-started/prerequisites) に示されている依存ライブラリがインストールされていない可能性があります。
上記ページに従って

```
sudo apt update
sudo apt install libwebkit2gtk-4.0-dev \
    build-essential \
    curl \
    wget \
    libssl-dev \
    libgtk-3-dev \
    libayatana-appindicator3-dev \
    librsvg2-dev
```

を実行してみてください。
:::

この実行ファイルを実行すると、

![Ubuntu での実行画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627744/7e1a73fe-b76b-af7d-0705-5b6e3b0a8e51.png)

他の OS とほぼ同じ画面が実行されていることが確認できます。

以上、マルチプラットフォームに対応していることが無事確認できました。

## おわりに

Tauri は[今年の6月にようやく 1.0 に到達した](https://tauri.app/blog/2022/06/19/tauri-1-0/)ばかりで、Electron などと比較するとまだまだマイナーなフレームワークです。

今回、そんなフレームワークを取り上げた理由は下記の通りです。

- 筆者が普段の業務で Windows 向けデスクトップアプリケーションを開発する業務に携わることも多く、過去に Electron や .NET Framework といったデスクトップアプリケーションフレームワークを使用して開発してきたため。
- 元々 Rust に興味があり、数年前から会社の Rust dojo という勉強会で Rust を学んできたため。
- 上記2つの理由により、「 Rust でデスクトップアプリが作れたら面白そう」と、 Tauri に対して注目していたため。
- Tauri が 1.0 に到達したことで安定版となり、趣味や業務などでも使いやすくなったと考えたため。

この記事を読んでくださる方にとって、この記事が参考になり、 Rust + Tauri がデスクトップアプリ開発をおこなう上で選択肢の1つとなれば幸いです🦀

## 参考文献

- [Build smaller, faster, and more secure desktop applications with a web frontend | Tauri Apps](https://tauri.app/)
    - [Quick Start | Tauri Apps](https://tauri.app/v1/guides/getting-started/setup)
    - [Webview Versions | Tauri Apps](https://tauri.app/v1/references/webview-versions/)
    - [Security | Tauri Apps](https://tauri.app/v1/references/architecture/security/)
    - [Prerequisites | Tauri Apps](https://tauri.app/v1/guides/getting-started/prerequisites)
- [React hooksを基礎から理解する (useState編) - Qiita](https://qiita.com/seira/items/f063e262b1d57d7e78b4)
- [React hooksを基礎から理解する (useEffect編) - Qiita](https://qiita.com/seira/items/e62890f11e91f6b9653f)
- [【React】useEffectをちゃんと理解する - Qiita](https://qiita.com/diskszk/items/333511fb97d24f52a439)