What is it, naokirin?

Rust + wasm-bindgen + trunk でWASMを実装して、サイトを作ってみる

みなさん、年末年始の休暇はいかがお過ごしでしょうか?

今回はそんな年末年始休暇にRustでWASMを実装してみました。

右から左に全国1474市区町村名が流れて、クリックすると、詳細を確認できます。

流れる SHI・KU・CHO・SON

リポジトリは以下です。

GitHub - naokirin/wasm_citiescrowl

バージョン

  • Rust: 1.66.0
  • wasm-bindgen: 0.2.83
  • trunk: 0.16.0

wasm-bindgenとは

wasm-bindgenは、wasmモジュールとJavaScriptの間のやり取りをしやすくしてくれるRustのライブラリ、およびCLIツールです。

github.com

これにより、おもに以下のようなことが簡単にできるようになります。

  • JavaScriptAPI をRustから簡単に呼べるようにする
  • クラス、関数などを Rust から JavaScript にエクスポートする
  • 単純な32bitのuint型やfloat型ではなく、文字列、数値、オブジェクト、クロージャなどの複雑な型を扱えるようにする
  • TypeScript バインディングを自動的に生成する

trunkとは

trunkは、wasm-bindgenを用いて、簡単にWebアプリケーションをバンドルしてくれるツールです。

trunkrs.dev

wasm-bindgenは、単体では通常JavaScriptへ公開するライブラリの出力になります。

ただ、trunkを用いることで、WASMのコードを呼び出すJavaScriptを書くことなく、WASMを呼び出すWebサイトを実装できます。

開発の準備

Rust、Cargo

とりあえず、Rustを使えるようにする必要があるので、Rust、Cargoあたりはインストールします。

このあたりはほぼ準備済みだったのと、他のWebサイトでも紹介されているので割愛します。

個人的にはコンテナ以外で利用する場合は、直接インストールせずに rustup を利用するのがよいと思います。

wasm-bindgenのインストール

ここもCargo.tomlに記載するか、cargo-edit で追加します。

github.com

それ以外にも色々とインストールしておくことになるのですが、色々あるのと、必要に応じて入れることになるかと思うので、今回開発で利用したCargo.tomlのURLだけ置いておきます。

https://github.com/naokirin/wasm_citiescrowl/blob/main/Cargo.toml

trunkのインストール

こちらは、以下でインストールします。

$ cargo install --locked trunk

M1 Macなどは以下も必要になります。

$ cargo install --locked wasm-bindgen-cli

これにより、 index.htmllib/main.rs を用意すると、 trunk serve コマンドを実行することで簡単に確認できるようになります。

また、trunk serve では、自動的な変更検知による再バンドルと、表示中のWebサイトへの適用を自動で行ってくれます。

JavaScriptAPIを呼び出してDOM操作をする

まずは、一般的なDOM操作から試してみます。

// window、documentの参照
let window = web_sys::window().unwrap();
let document = window.document().unwrap();

// document.getElementById()
let contents = document.get_element_by_id("html_element_id").unwrap();
let element: web_sys::HtmlElement = contents.dyn_into::<web_sys::HtmlElement>().unwrap();

// document.createElement()
let p = document.create_element("p").unwrap();

// Element.appendChild()、Element.innerHtml
element.append_child(&p)?;
element.set_inner_html("テキスト")?;

キャメルケースからスネークケースに変わっていること以外、ほぼJavaScriptAPIそのままとなっており、直感的にJavaScriptAPIが呼び出せることがわかります。

EventListenerの登録をする

EventListenerの登録をするときは少し難易度が上がりますが、ポイントさえ押さえれば、そこまで難しくありません。

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

…

// イベントが発火したときに呼び出されるクロージャを生成する
let onclick:Closure<dyn FnMut(_)> = Closure::wrap(Box::new(move |_event: web_sys::MouseEvent| {
    log("clicked!");
}));

// elementに対して、クリックイベントのリスナを登録する
element.add_event_listener_with_callback("click", onclick.as_ref().unchecked_ref()).unwrap();

// 明示的に「メモリリーク」させる
onclick.forget();

まず、EventListenerとしてコールバック登録する処理は、 Closure にします。

次にEventListenerに登録するのですが、そのあとに、Rustのライフタイムの管理によって破棄されてしまわないようにするために、 forget() によって明示的に「メモリリーク」させる必要があります。

一定時間後の実行を簡単に実行する

JavaScriptで一定時間後の実行といえば、 setTimeout()setInterval() による呼び出しですが、先ほどのEventListenerと同等の処理となるため、Rustの実装に慣れているとやや冗長に感じます。

そこで、 wasm-timer クレートを用いて、Rustっぽく実装してみます。

use std::time::Duration;
use wasm_timer::Delay;

...

async fn foo() {
    Delay::new(Duration::from_millis(5000)).await.unwrap();
    log("5秒後にログがでます");
}

上記のように、簡単にRustの await を用いて一定時間後に実行するといったことができます。

setInterval() に関しても、 loop と組み合わせることで可能になるかと思います。

現在のスレッドで非同期に実行する

前述の wasm-timer を用いると、Rustの await を用いて簡単に一定時間後の処理を行うことができますが、現在のスレッドで待つことになるため、並行して処理はできません。

そのような場合に、JavaScript側の Promise.then と同等の処理を実行する wasm_bindgen_futures::spawn_local() を利用できます。

fn main() {
    wasm_bindgen_futures::spawn_local(async_func());
    log("すぐにログが出ます");
}

async fn async_func() {
    Delay::new(Duration::from_millis(5000)).await.unwrap();
    log("5秒後にログが出ます");
});

この場合、5秒待つことなく「すぐにログが出ます」のログが出ます。

Fetch API を reqwest で簡単に実行する

Fetch APIreqwest-wasm クレートを用いて簡単に実行できます。

let client = reqwest_wasm::Client::new();
let request = client
    .get("https://google.com")
    .build()
    .unwrap();
let body = client.execute(request).await.unwrap().text().await.unwrap();

エラーをわかりやすくする

最後に、実行時のコンソールログのエラーをわかりやすくする console_error_panic_hook を紹介しておきます。

以下のように設定しておくことで、wasm上のエラー箇所だけでなく、Rustコード上のエラー箇所などをわかりやすく表示してくれます。

extern crate console_error_panic_hook;

...

panic::set_hook(Box::new(console_error_panic_hook::hook));

まとめ

Rust で WASM を実装するというのは、もう少し難しいかと思っていましたが、思ったよりは簡単でした。

一方で、特有の対応が必要な部分もあり、そのあたりの難しさや、パフォーマンス等を気にすると考えることが増える印象がありました。

とはいえ、RustとJavaScriptをある程度知っていれば、入門レベルならサクッと動くものを作れる程度には簡単なので、食わず嫌いせずに試してみるとよいかと思います。