🧊

バンドラを使わずにRustをWASMにする

調べると初手`wasm-pack`があまりに多くて、諸事情により`webpack`いらないんですけど・・ってなシーンでどうすればいいかわからんかった。

それを2019年末にあれこれトライアンドエラーしてみた結果のメモです。

最小構成で試す

とりあえずやってみる。

Rustを書く

まずモジュールを作る。

`cargo new --lib wasm`的な感じでプロジェクトを作って書いてく。

#[no_mangle]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

あわせて`Cargo.toml`にこれを書き足す。

[lib]
crate-type = ["cdylib"]

これをビルドする。
`cargo build --target wasm32-unknown-unknown --release`とかで。

ホストする

適当にサーバーを立てればいい。

`.wasm`は、MIME-TYPEを`application/wasm`として返す必要があるので注意。
Pythonワンライナーみたいなのだとダメだった。

ブラウザで実行する

こんなJSで読める。

(async () => {
  const path = "./wasm/target/wasm32-unknown-unknown/release/wasm.wasm";
  const {
    instance: {
      exports: { add }
    }
  } = await WebAssembly.instantiateStreaming(fetch(path), {});

  console.log(add(10, 7));
})();

`instantiateStreaming()`はSafariではまだ使えない。
その場合は`fetch()`を`ArrayBuffer`でやって、`instantiate()`に渡すひと手間が必要。

wasm-bindgenを使う

とりあえず動くものの、これだとRust側で`i32`, `i64`, `f32`, `f64`しか扱えないので、ほぼ何もできない・・。
配列も返せないし文字列も返せない!

実際にRustで`String`を返す関数を作っても、実行すると`undefined`が返ってきちゃう。

なので、このあたりをいい感じに橋渡ししてくれる存在のデファクトである、`wasm-bindgen`を使う。

GitHub - rustwasm/wasm-bindgen: Facilitating high-level interactions between Wasm modules and JavaScript

モジュールをこんな感じで書き直す。

extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet() -> String {
    "from wasm".to_string()
}

クレートを読み込んで`prelude`書いて、`no_mangle`の代わりに`wasm_bindgen`ってするだけ。

ただコレを使うと、↑に書いた最低限コードでは読み込めなくなるので、`wasm-bindgen-cli`を使って変換する必要がある。

`cargo install wasm-bindgen-cli`すると、`wasm-bindgen`コマンドが使えるようになる。

`wasm-bindgen`コマンドは、ビルドした`.wasm`に対して実行するイメージ。

ES Modulesにする

`wasm-bindgen`のドキュメントにやり方は書いてある。

Without a Bundler - The `wasm-bindgen` Guide


`wasm-bindgen target/wasm32-unknown-unknown/release/wasm.wasm --out-dir ./pkg --target web`でビルドできる。

この`--target web`が、ES Modulesにする指定。

そして読み込む。

// in script[type=module]
import init, { add, greet } from './pkg/wasm.js';

(async () => {
  // これがいわゆるWASMのロードになり、これをしないと使えない
  await init();

  // あとは自由に
  console.log(greet());
})();

これでよし。

WebWorkerからも使いたい

ES ModulesがWorker内で使えない2019年現在、どうすればいいか。
ChromeのM80から、WebWorkerでだけはES Modulesが使えるようになるらしいけど、それでもChromeだけ。

メインスレッドでWASMを読み込んで、それをまるごとWorker側に`postMessage()`する方法もあるけど、なんとなく避けたい・・。

そこで使えるのが`no-modules`っていうターゲット!

`wasm-bindgen target/wasm32-unknown-unknown/release/wasm.wasm --out-dir ./pkg --target no-modules`でビルドできる。

`--target no-modules`が重要。
`--target web`のときと、成果物の種類は変わらない。

それをこのように読み込む。

// in WebWorker
importScripts("./pkg/wasm.js");
const { wasm_bindgen } = self;
const { add, greet } = wasm_bindgen;

(async () => {
  // またも先に読み込みが必要
  await wasm_bindgen('./pkg/wasm.wasm');

  // あとは自由に
  greet();
})();

仕組みは`web`ターゲットとほとんど一緒で、いったん初期化のステップを踏まないといけない。

あとは使いやすいようによしなにすればよい。
ちなみにこれは、ES Modulesを使わない場合に、メインスレッド側でWASMを読み込む方法としても使える。

バンドラを使うべきなのか

Rustで書いたものをモジュールとして使う場合、実際ほとんど`webpack`になると思うし、やっぱり`wasm-pack`にしとけって感じなんやろなという予想・・。

GitHub - wasm-tool/wasm-pack-plugin: webpack plugin for Rust

ただサクッと試す分には`wasm-bindgen-cli`で十分やと思った。