Help us understand the problem. What is going on with this article?

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

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

by rchaser53
1 / 22

誰?


概要

  • 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

使わない場合

without_panic_hook.png


console_error_panic_hook

使った場合

with_panic_hook.png


wee_alloc

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

demo

fizzbuzz.gif


感想

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away