RustとWebAssemblyでウェブアプリを作る

## はじめに
この記事ではRustとWebAssemblyを使って簡単なウェブアプリとしてカウンターを作成します。
[Rust]([公式ドキュメント](https://doc.rust-jp.rs/book-ja/title-page.html))と[WebAssembly](https://webassembly.org/)そのものについては紹介しません。Rustは[公式ドキュメント](https://doc.rust-jp.rs/book-ja/title-page.html)、WebAssemblyは[MDN](https://developer.mozilla.org/ja/docs/WebAssembly)がわかりやすいです。
さらに、rustupとNodejsがインストールされていることを前提としています。

## プロジェクトの準備
この記事ではRust WasmグループがRust×webpackのテンプレートとして提供している[`rust-webpack-template`](https://github.com/rustwasm/rust-webpack-template)を用いてプロジェクトを準備します。
このテンプレートは以下のコマンドで生成します。

```sh
npm init rust-webpack counter
```

`counter`の部分は生成するフォルダ名です。フォルダ名がない場合は現在のディレクトリに生成されます。

### 生成物の解説
`Cargo.toml`はRustを管理、`package.json`はNode.jsを管理するためのファイルです。
`js`フォルダにはwebpackのエントリーファイルが置かれています。基本的には変更しません。
`src`フォルダにはRustのコードが置かれます。
`static`フォルダにはビルド後のフォルダにそのまま移行させたいファイルが置かれます。
`tests`フォルダはRustのコードに対するテストですが、今回は利用しません。
`webpack.config.js`ファイルはwebpackの設定が書かれたコードです。基本的には変更しません。

### 動作の確認
Node.jsが依存するパッケージを`package.json`を元にインストールします。
```
npm i
```
次に開発サーバーを起動して動作を確認します。Nodeのバージョンが17以上の場合は
```sh
NODE_OPTIONS=--openssl-legacy-provider npm run start
```
と入力してください。17未満の場合は
```sh
npm run start
```
で`localhost:8080`に開発サーバーが立ち上がります。
`wasm-pack`を導入していないことによるエラーが発生する場合は
```sh
cargo install wasm-pack
```
で導入してください。

最終的に`localhost:8080`にアクセスするとページには何も表示されずに開発ツールを確認すると`console`に`Hello world!`と表示されているはずです(その他にもWDSからのメッセージが表示されます)。これで準備は完了です。

![スクリーンショット 2023-08-15 23.58.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/aab3dab6-6bb4-a8de-6347-417486965cb7.png)

ファイルをビルドしたいときは以下のコマンドで`dist`に対して結果を吐き出させられます。
```
// Nodejs v17以上
NODE_OPTIONS=--openssl-legacy-provider npm run build
// Nodejs v17未満
npm run build
```
同時に`target`フォルダと`pkg`フォルダも生成されるはずです(この2つは`npm run start`の段階で生成されます)。
`target`フォルダはRustコンパイラによって生成されたバイナリやライブラリです。
`pkg`フォルダはRustから生成したWebAssemblyのバイナリをJavaScriptから呼び出すために必要なファイル群です。実際にwebpackのエントリーポイントとなる`js/index.js`も`pkg`フォルダを参照しているはずです。
`dist`フォルダはそれらを複合して`webpack`から吐き出された成果物です。`static`フォルダにあったファイルと`webpack`によって生成されたJavaScriptのファイル、WebAssemblyのファイルが置かれます。

### Rustファイルの解説
`src/lib.rs`ファイルについてみていきます。このファイルは先ほど見たように`console`に対する出力を行なっています。
```rs
use wasm_bindgen::prelude::*;
use web_sys::console;


// When the `wee_alloc` feature is enabled, this uses `wee_alloc` as the global
// allocator.
//
// If you don't want to use `wee_alloc`, you can safely delete this.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;


// This is like the `main` function, except for JavaScript.
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
    // This provides better error messages in debug mode.
    // It's disabled in release mode so it doesn't bloat up the file size.
    #[cfg(debug_assertions)]
    console_error_panic_hook::set_once();


    // Your code goes here!
    console::log_1(&JsValue::from_str("Hello world!"));

    Ok(())
}
```
[`wee_alloc`](https://github.com/rustwasm/wee_alloc)はRustのWebAssemblyバイナリをより小さくするためのグローバルアロケータとしてデフォルトで使用されているものです。

`#[wasm_bindgen(start)]`は`main_js`を`wasm-bindgen`マクロを使用して、WebAssemblyモジュールのエントリーポイントを定義しています。

`main_js`の返り値は`Result<(), JsValue>`となっています。`JsValue`はJavaScriptで扱う値を意味しており、失敗した時はJavaScriptで扱える値で返す必要があるということです。


```rs
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
```
この部分はRust側で生じた`panic`に関する処理を記したものです。`console_error_panic_hook::set_once();`が宣言されていれば、Rust内で`panic`が生じた場合はスタックトレースをエラーと共に`console`に表示します。宣言されていなければ`RuntimeError: Unreachable completed`のような単調なメッセージが表示されます。
`#[cfg(debug_assertions)]`のように書くことで、開発中はスタックトレースまで表示し、本番では簡素なメッセージを出すように出し分けを行なっています。



```rs
console::log_1(&JsValue::from_str("Hello world!"));
```
そして、この部分では`web_sys`の`console`モジュールを呼び出して`log_1`を使うことで、JavaScriptの`console.log`関数を呼び出しています。
`&JsValue::from_str("Hello world!")`はRustの文字列`"Hello world!"`をJavaScriptの文字列に変換しています。
そして`log`ではなく`log_1`となっているのはJavaScriptに実装された`console.log`関数の引数は可変長であるのに対して、Rustでは可変調な引数を定義できないので`console.log`の引数に合わせて`log_1`、`log_2`のように定義されているからです。


## カウンターの作成
プロジェクトの準備と解説を行ったので早速カウンターの作成を行います。
カウンターはとても単純なものでボタンを押すとボタンの持つ数値が1ずつ増えていくようなものを作ります。

まずは、ボタンを表示するように`static`フォルダにある`index.html`に`button`要素を追加します。
```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>My Rust + Webpack project!</title>
  </head>
  <body>
    <script src="index.js"></script>
    <button id="counter">0</button>
  </body>
</html>
```
Rust側で扱いやすいように`id`に`counter`を与えました。

次にその`button`をRustで扱えるようにします。`button`が持つテキストを取得して`console`に出力させてみましょう。

```rs
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let counter = document
    .get_element_by_id("counter")
    .unwrap()
    .dyn_into::<web_sys::HtmlButtonElement>()
    .unwrap();

console::log_1(&counter.inner_text().into());
```

`console`に`Hello World!`と吐き出していた部分を上記のコードに置き換えます。
このコードでは`web_sys`の`window`メソッドを用いて`window`を取り出し、そこから`document`メソッドで`document`を取り出しています。
`button`はその`document`から`get_element_by_id`メソッドを用いてを指定した`id`のボタンを取得して`dyn_into`で`web_sys`に定義された`HtmlButtonElement`へ型を変換しています。
最後にそのボタンが持つテキストを`console`に出力しています。
このままコードを貼り付けるだけではコードは動きません。`Cargo.toml`を開いて`web-sys`の`fetures`に利用するbrowser apiを記述します。

```toml
[dependencies.web-sys]
version = "0.3.22"
features = ["console", "Window", "Document", "HtmlButtonElement"]
```

このようにすることで画面にテキストが0のボタンが表示され、コンソールにも0が表示されたはずです。
![スクリーンショット 2023-08-15 23.56.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/24152f20-c920-6718-e6a7-a8263a7f7433.png)



最後にこのボタンに対して加算処理を加えます。通常のJavaScriptと同じようにクリックイベントに追加します。

```rs
let counter_clone = counter.clone();
let click_handler = Closure::wrap(Box::new(move || {
    let next_count = counter_clone.inner_text().parse::<u32>().unwrap_or(0) + 1;
    counter_clone.set_inner_text(&next_count.to_string());
}) as Box<dyn FnMut()>);
counter.add_event_listener_with_callback("click", click_handler.as_ref().unchecked_ref())?;
click_handler.forget();
```
`console`の箇所を削除して、`counter`のクリックイベントにボタンの値を前回の値+1の値に設定するようなメソッドを追加しています。

まずは`counter`をクローンしてきます。これはクリックイベントに渡すコールバック関数に所有権を譲るための変数です。
```rs
let counter_clone = counter.clone();
```
次にボタンの値を前回の値+1の値にする関数です。
```rs
let click_handler = Closure::wrap(Box::new(move || {
    let next_count = counter_clone.inner_text().parse::<u32>().unwrap_or(0) + 1;
    counter_clone.set_inner_text(&next_count.to_string());
}) as Box<dyn FnMut()>);
```
`Box::new`メソッドを使用して`Closure`を作成し、`move`を使用して`counter_clone`変数を`Closure`に移動します。次に`counter_clone`の`inner_text`メソッドを使用してボタンのテキストを取得し整数型に変換します(変換できない場合は`０`にします)。最後に`counter_clone`の`set_inner_text`メソッドを使用してボタンのテキストを更新します。
JavaScriptの文脈で実行させる関数を扱う場合はこのようにRust側で`Closure`を用いて定義することが多いです。

そして、`counter`に対して先ほど作成した`Closure`をクリックイベントとして登録します。`as_ref`と`unchecked_ref`メソッドは`Closure`からJavaScriptの`Function`型に変換しています。
```rs
counter.add_event_listener_with_callback("click", click_handler.as_ref().unchecked_ref())?;
```
最後に`Closure`オブジェクトがドロップされた時のメモリーリークを防ぐために登録した`Closure`を忘却させます。
```rs
click_handler.forget();
```

これで画面のボタンをクリックすることで加算されるようになりました。
![画面収録_2023-08-15_23_54_33_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/3716565d-07a9-4e04-b494-a07ae587f9b4.gif)