Rust で WebAssembly (WASM) やってみた

そろそろ WebAssembly (略して WASM) を触っておくかぁと思って、触った。

作ってみたコードは以下。

実際に WASM が動作しているデモページは以下。ブラウザの開発者ツールでコンソールを確認してみて欲しい。

WASM は GitHub Pages のような静的ホスティング環境でも動作するのが素晴らしいね…!

Rust で WASM プロジェクトを作るための wasm-pack というツールが存在する。コレを使うのが簡単だった。

$ wasm-pack new コマンドで雛形を用意し、Rust のコードを書いて、$ wasm-pack build --target web でビルドして WASM バイナリにコンパイルした。ほとんど以下の記事を参考にした。

まだ Rust 言語に慣れていないところも多いが、wasm-pack で用意した雛形に埋め込まれている wasm-bindgen というライブラリが JavaScript とのデータの受け渡しを色々担当してくれていて、比較的分かりやすくコードを書ける。

ビルドすると、.wasm ファイルと .js ファイルを吐き出してくれる。今回コンパイルした WASM ファイルは 15.8KB 程度で、さすが Rust だとかなり軽量 (Go や TinyGo だと結構大きめのバイナリになったりする)。 1 通常、WebAssembly.instantiateStreaming() という関数を使って .wasm ファイルをロードしてインスタンス化するのだが、.js ファイルの方そのインスタンス化や、WASM 側にある関数への受け渡しをやってくれるヘルパーコードなどをまるっと生成してくれる。なので WebAssembly.instantiateStreaming() などを自分で書かなくて良くなる。

HTML 側で WASM を読み込むには、ES Modules (ESM) 形式で JS を書いてやる必要がある。

<script type="module">
// ↑ この type="module" が必要

// wasm-pack が出力してくれた JS ファイルをインポートする
// 同じ階層に、同じく `./pkg/practice_wasm_rust_bg.wasm` という WASM ファイルもある状態
import * as wasm from './pkg/practice_wasm_rust.js';

// まずは初期化処理を呼び出す
// 今は Top-Level await が書けるようになっている!
// `(async () => { })();` というような即時関数でのラップは不要!
const module = await wasm.default();  // Init・戻り値 module は基本的に使用しない

// 初期化処理を呼び出したあと、任意の関数をコールできるようになる
wasm.greet();  // Rust 側で作ってエクスポートしてある greet() 関数を呼び出す例

</script>

こんな感じで、コメントを除けば import・Init・そして関数呼び出しの3行で WASM を実行できるようになっている。

<script type="module"> という ESM が慣れていなくて、確か非同期に動くみたいな仕様だったと思うので実行順序などを確認した。すると、通常の <script> よりも後、HTML は全て読み込み終わった DOMContentLoaded イベントに近いタイミングで実行されていた。

Rust のコードは wasm-pack でビルドしたものの、それを読み込む HTML 側は Vanilla JS でも問題なく、さらに .wasm を含めて GitHub Pages にデプロイしてホスティングさせられた。思ったより手っ取り早く試せてよかった。

今のところ自分が作ろうとしているモノで WASM が必要なケースが想像できないけど、

みたいな場面で、いざとなったら Rust なり C++ なり Go なりで実装しちゃえばやれるんだ、と思うと、ちょっと夢がひろがりんぐなので、活用法を考えていきたい。