みなさん、年末年始の休暇はいかがお過ごしでしょうか?
今回はそんな年末年始休暇にRustでWASMを実装してみました。
右から左に全国1474市区町村名が流れて、クリックすると、詳細を確認できます。
リポジトリは以下です。
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ツールです。
これにより、おもに以下のようなことが簡単にできるようになります。
- JavaScript の API をRustから簡単に呼べるようにする
- クラス、関数などを Rust から JavaScript にエクスポートする
- 単純な32bitのuint型やfloat型ではなく、文字列、数値、オブジェクト、クロージャなどの複雑な型を扱えるようにする
- TypeScript バインディングを自動的に生成する
trunkとは
trunkは、wasm-bindgenを用いて、簡単にWebアプリケーションをバンドルしてくれるツールです。
wasm-bindgenは、単体では通常JavaScriptへ公開するライブラリの出力になります。
ただ、trunkを用いることで、WASMのコードを呼び出すJavaScriptを書くことなく、WASMを呼び出すWebサイトを実装できます。
開発の準備
Rust、Cargo
とりあえず、Rustを使えるようにする必要があるので、Rust、Cargoあたりはインストールします。
このあたりはほぼ準備済みだったのと、他のWebサイトでも紹介されているので割愛します。
個人的にはコンテナ以外で利用する場合は、直接インストールせずに rustup を利用するのがよいと思います。
wasm-bindgenのインストール
ここもCargo.tomlに記載するか、cargo-edit で追加します。
それ以外にも色々とインストールしておくことになるのですが、色々あるのと、必要に応じて入れることになるかと思うので、今回開発で利用した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.html
や lib/main.rs
を用意すると、 trunk serve
コマンドを実行することで簡単に確認できるようになります。
また、trunk serve
では、自動的な変更検知による再バンドルと、表示中のWebサイトへの適用を自動で行ってくれます。
JavaScriptのAPIを呼び出して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("テキスト")?;
キャメルケースからスネークケースに変わっていること以外、ほぼJavaScriptのAPIそのままとなっており、直感的にJavaScriptのAPIが呼び出せることがわかります。
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 API も reqwest-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をある程度知っていれば、入門レベルならサクッと動くものを作れる程度には簡単なので、食わず嫌いせずに試してみるとよいかと思います。