wasmを使ってRust製JVMをブラウザで動かしてみた


誰?


概要

  • Rustで作成したJVMをwasm-packを使ってブラウザで動かした
  • 何も使わないで動かすよりはるかに楽だった
  • ついでに見つけた便利なmoduleの紹介
  • JVM作成については別スライドをどうぞ

今回の成果物


wasm-packとは

  • wasm-bindgenを使って生成したwasmファイルにRustとJavaScriptのFFIのランタイムを追加するビルドツール
  • 引数や戻り値として普通に文字列や配列が使える
  • ドキュメントはそれなりに充実している
  • 細かい用語などに関してはlegokichiさんの資料がとても良くまとまっているのでオススメ

実際にどう使うの?

  • Rust側
  • JavaScript側

Rust側

use wasm_bindgen::prelude::*;

// JavaScriptから関数をimport
#[wasm_bindgen(module = "/import.js")]
extern "C" {
    // 意味はほぼ無いが32bitのintを取ってStringを返す関数
    fn import_from_js_fn(input: i32) -> String;
}

// JavaScriptへ関数をexport
#[wasm_bindgen]
pub fn export_to_js_fn(input: i32) -> String {
    // 意味はほぼ無いが32bitのintを取ってStringを返す関数
    input.to_string()
}


JavaScript側

// index.js
const wasm = await import('./pkg');
const result = wasm.export_to_js_fn(111);
console.log(result);     // "111"

// import.js
export function import_from_js_fn(val) {
   return val.toString();
}

JavaScript側

実際に使ったのはwasm-pack-plugin。設定は少なめ

/* pluginsだけ表示 */
plugins: [
  new HtmlWebpackPlugin({
    template: 'index.html'
  }),
  new WasmPackPlugin({
    // wasm-packが作成してくれるwasmなどのファイルが入っているディレクトリ
    // pkgのpathを指定する
    crateDirectory: path.resolve(__dirname, "crate")
  }),
  // JavaScriptはutf16, Rustはutf8のStringで非常に具合が悪い
  // この子たちがなんとかしてくれる
  new webpack.ProvidePlugin({
    TextDecoder: ['text-encoding', 'TextDecoder'],
    TextEncoder: ['text-encoding', 'TextEncoder']
  })
],

JavaScript側

  • 後はwebpack-dev-serverを立ち上げればrustのビルドも一緒にやってくれる
  • Rust側のhot reloadもしてくれるので開発は結構サクサク

どれくらい楽になったの?

何も使わないで簡単な操作を行うことで試してみる
レポジトリ: https://github.com/rchaser53/vanilla-rust-wasm


Rustから文字列を返してみよう

Rust側

#[macro_use]
extern crate lazy_static;
use std::sync::Mutex;

// staticな領域にメモリを確保。これを用いてJavaScriptとデータのやりとりをする
lazy_static! {
    // RustのStringはutf8。全ての要素を0にすることで初期化する
    pub static ref STRING_MEMORY: Mutex<[u8; 1_000]> = Mutex::new([0; 1_000]);
}

// マングリングを行わない
#[no_mangle]
pub fn get_string() -> *const u8 {
    // 文字列を生成してメモリにコピーする
    let data = String::from("hello world");
    let length = data.len();
    let s = data.as_bytes() as &[u8];
    let mut memory = STRING_MEMORY.lock().unwrap();
    memory[..length].clone_from_slice(&s[..length]);

    // 先端のポインタを返す
    memory.as_ptr()
}

文字列を返してみよう

JavaScript側

WebAssembly.instantiateStreaming(fetch(path), {})
  .then(({ instance }) => {          
    const getString = (instance) => {
      const pointer = instance.exports.get_string();
      const buffer = new Uint8Array(instance.exports.memory.buffer, pointer);
      const input = new Uint8Array(extractInput(buffer));
      // RustのStringはutf8
      const decoder = new TextDecoder('utf-8');
      let result = decoder.decode(input);
      console.log({ result })
    }

    const extractInput = (heap) => {
      let index = 0;
      let inputArray = [];
      // 何も値が入っていない(0)までwhileで回す
      while (heap[index] !== 0) {
        inputArray.push(heap[index]);
        index += 1;
      }
      return inputArray;
    }

    getString(instance);
  });

とても辛い(実際に使うならもっと色々と考慮する必要があると思う)


まだまだ制限はある

// referencesは返せない
/* error: cannot return references in #[wasm_bindgen] imports yet */
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen(module = "/web/map.js")]
extern "C" {
    pub fn get_file_content_from_js(key: &str) -> &[u8];
}

// lifetimeや型パラメータは使えない
/* error: can't #[wasm_bindgen] functions with lifetime or type parameters */
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen(module = "/web/map.js")]
extern "C" {
    pub fn get_file_content_from_js<'a>'(key: &'a str) -> Vec<u8>;
}

便利な子たち

wasm-bindgenのドキュメントの方には何故書かれていない便利な子たち2つ
- console_error_panic_hook
- wee_alloc


console_error_panic_hook

  • これがないとwasm内でこけても「RuntimeError: Unreachable executed」しか出ない
  • これを使うとrustのエラーメッセージがconsole.errorとして出力される
  • 開発中だけでも使うべき

console_error_panic_hook

使わない場合


console_error_panic_hook

使った場合


wee_alloc

  • デフォルトで提供されるメモリアロケータより性能は少し低いがサイズがとても小さい
  • 軽く使うくらいならデフォルトのメモリアロケータは性能が過剰
  • これを使うだけでwasmのサイズが削減できるらしい(要確認)

demo


感想

  • 開発自体はかなりしやすくなった
  • ドキュメントを読めば簡単なことなら特に問題なく実装できそう
  • 引数、戻り値の制限はあるが、致命的なレベルではないと思う
  • 用途さえ見つかれば十二分に使えそうに感じる

ご視聴ありがとうございました