今更だけどRustでwasmを試す

RustでWebAssemblyしとかないと時代の流れに取り残されそう、という強迫観念にかられ腹をくくって飛び込んだ。
触るだけなら案外いけたので、その気持を忘れないようにここにメモする。

書かないこと

準備

wasm の知識

とくに勉強してない。LLVMがどうとかよくわからんレベル。
触って覚えようスタイル。

最低限 Rust で簡単なコードを書けるようにする

マークアップ生まれ JavaScript 育ちとしては、流石にRustをすっ飛ばして Rustでwasm するのは気が早すぎるという警告を受信したので、
なんとなく 言語処理100本ノック 2015 で軽くRustを素振りしておいた。

不まじめなので、12, 13あたりまでやって、なんかやろうとしてることとちょっと違うなと思って止めてしまったけど、

String&str に悩んだり、 usizeuint で悩んだり
vec![] はいいらしい、とか、 変数の mut と 参照?の mut はなるほど?
if, match, iterator あたりはなんとなくやっておいて、
関数の書き方、モジュールをまたいだ関数の呼び出しくらいは調べながらできるようになった。

シンプルに試す

とにかく触るという目標なので、 素の rust 開発環境 だけで開発&wasmにビルドして、
jsから呼ぶというのが今回の目標。というかそんなミニマルにできるようになったんだ、と驚いてる。

Rustのコードを書く

cargo new rust-wasm-vanilla --lib でプロジェクト作って早速書く。

src/lib.rs
#[no_mangle]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub fn fib(count: u32) -> u32 {
    if count < 2 {
        return count;
    }

    let (_, ret) = (0..count-2).fold((0, 1), |(former, ret), _| (ret, ret + former));
    ret
}

#[no_mangle]
pub fn fib_array(count: u32) -> Vec<u32> {
    match count {
        0 => vec![],
        1 => vec![0],
        2 => vec![0, 1],
        _ => (2..count).fold(vec![0, 1], |mut acc, i| {
            let new = acc[(i-1) as usize] + acc[(i-2) as usize];
            acc.push(new);
            acc
        })
    }
}

fn main() {
    println!("{:?}", add(10, -100));
    println!("{:?}", fib(40));
    println!("{:?}", fib_array(10));
    assert!(fib_array(10).len() == 10);
}

#[no_mangle] はアトリビュートと言い, Java のデコレータのようにブロックやメソッドなどを修飾する構文です. #[no_mangle] では「Rust コンパイラに次の関数の名前をマングリングせずにコンパイルせよ」と指示します. これにより, JavaScript から add という名前で関数にアクセスできるようになります. また, 関数を公開して外部から呼び出せるようにするため pub キーワードを付けています.

https://wasm-dev-book.netlify.com/hello-wasm.html#webassembly-%E3%82%92%E8%A9%A6%E3%81%99

こんな感じでざっと関数を定義して ESM的に言うなら export する。

Rustに詳しくない人も fn main() みたらなにか感じたかも知れないけど、
せっかくなので rust として実行してみる。

Rustとして実行&ビルド

$ rustc src/lib.rs # ./lib というバイナリにCompile
$ ./lib # 実行

-90
63245986
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

やったぜ🐱
これはただのRustなのでまだ全然wasmしてない。

今度は wasm32-unknown-unknown というビルドターゲットを指定してBuildする。

$ cargo build --target wasm32-unknown-unknown
   Compiling rust-wasm-vanilla v0.1.0 (<PROJECT_PATH>)
warning: function is never used: `main`
  --> src/lib.rs:30:1
   |
30 | fn main() {
   | ^^^^^^^^^
   |
   = note: #[warn(dead_code)] on by default

    Finished dev [unoptimized + debuginfo] target(s) in 3.44s

すると ./target/wasm32-unknown-unknown/debug/rust_wasm_vanilla.wasm がBuildされる。

Rustで試す用のコードそのまま放置してるので怒られが発生してるけど気にしなくてOK。
(エラーにあるようにattribute使えば回避できる)

JavaScriptからwasmを利用する

生み出した .wasmjs のcontextから叩くための準備をする。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>WasmTest</title>
  </head>
  <body>
  <script>
    const wasmPath = './target/wasm32-unknown-unknown/debug/rust_wasm_vanilla.wasm';
    WebAssembly.instantiateStreaming(fetch(wasmPath), {})
      .then(({ instance }) => {
        const fib = 47;
        console.log(instance.exports);
        console.log('add', instance.exports.add(2, 8));
        console.log('fib', fib, instance.exports.fib(fib));

        console.log('fib_array', instance.exports.fib_array(47));
        console.log(instance.exports.fib(50));
      });
  </script>
  </body>
</html>

ちなみに、WebAssembly.instantiateStreaming 非対応ブラウザならこうする。

fetch(wasmPath)
  .then(response => response.arrayBuffer())
  .then(bytes => WebAssembly.instantiate(bytes, {}))
  .then(({ instance }) => {
    // 略
  });
  1. fetch(): .wasm を取得する Response を作る
  2. Body.arrayBuffer(): Response のストリームから wasm バイナリデータをすべて読み取る

    • ArrayBufferresolve する Promise が返る
  3. WebAssembly.instantiate(): wasmバイナリの ArrayBuffer をインスタンス化する。その過程で module の Compiling もされるのでそれも返る。

    • {
        module: コンパイル済みの WebAssembly.Module
        instance: 上記 module を instance 化したもの
      }

      resolve する Promise が返る


とにかく開ければいいので最低限の .html をシュッと作る。
ここから、ローカルの .wasm に向けて fetch 叩いても リクエストしないので、 ローカルのWebサーバを経由で見るようにする。

$ npx http-server . -p <適当なポート番号>

または、 php -S localhost:<適当なポート番号> でもなんでもいいけど、
wasm 渡すときの MIMEtype 周りにちょっと注意。

実行結果

Chromeでの実行結果はこんな感じ。 result

  • instance.exports

    • いろいろはいってるけど略
  • instance.exports.add(2, 8)

    • OK
  • instance.exports.fib(fib)

    • OK
  • instance.exports.fib_array(47)

    • Vec が都合よく Array になるなんてことはなくて虚無になる
  • instance.exports.fib(50)

    • rust のコード内で panic してる
    • std::u32::MAX である 4294967295 を超える 7778742049 になるため
    • だからといってrust側で戻り値のtypeを u64 にすると

      Uncaught (in promise) TypeError: wasm function signature contains illegal type
        at WebAssembly.instantiateStreaming.then

      というRuntime Errorが発生する。
      多分 u64(rust) -> i64(wasm) のあとに i64Number にtypecast出来なくて死んでるっぽい

現時点のまとめ

i32 の範疇でなにかやるくらいなら全然これでいい。

あと、文字(char) はどうなのと思って試した

#[no_mangle]
pub fn get_char_a() -> char {
    'a'
}
//...略
const a = instance.exports.get_char_a();
// 97

97...

まず, Rust と WebAssembly のそれぞれでサポートされるプリミティブ型を確認します. Rust は array, bool, char, i32 などを含む 25 種類のプリミティブ型がサポートされます. 一方, WebAssemblyのプリミティブ型は i32, i64, f32, f64 の 4 種類のみしかサポートされません.

https://wasm-dev-book.netlify.com/hello-wasm.html#%E6%9A%97%E9%BB%99%E3%81%AE%E5%9E%8B%E5%A4%89%E6%8F%9B

というわけで、Rust における char も例外ではなく wasm に持っていった段階ですべて int or float になっちゃうので、jsで文字(列)として decode する必要がある。

TextDecoder があるので、そこに Uint8Array 、つまりバイトストリームにして、decode methodに食わせる。

const charAByte = new Uint8Array([instance.exports.get_char_a()];
const decoder = new TextDecoder('utf-8');

decoder.decode(charAByte);
// a

こういった諸々の手続き踏めば色々な型の表現ができるんだけど、これはいちいちやってられないし、
自前で頑張るのはなかなかしんどい、ということで必然的に rustwasm/rust-bindgen という wasm-js 間の binding 生成ライブラリを使う。これが結構凄い。これについては次回。

やり残し

あとやってないけどjsから環境を渡すことで WebAssembly から JavaScript の関数を呼び出す ことができるっぽい。
Rust 側で予め環境のInterface定義して呼ぶような感じ。

あとせっかくやるならRust のサードパーティ製ライブラリの利用 もやりたい。

適当なタイミングで、この2つは試してみようと思う。


参考