誰?
- 名前: r-chaser53
- GitHub: https://github.com/rchaser53
- Twitter: https://twitter.com/rChaser53
- 所属: LINE株式会社
- 言語開発とか好きな人(歴は1.5年くらい)
- rustfmtのお手伝いを今年はしてます
概要
- Rustで作成したJVMをwasm-packを使ってブラウザで動かした
- 何も使わないで動かすよりはるかに楽だった
- ついでに見つけた便利なmoduleの紹介
- JVM作成については別スライドをどうぞ
今回の成果物
- Rust製JVM。wasmとしてブラウザで動かすこともできる
- https://github.com/rchaser53/rust-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
- これがないとwasm内でこけても「RuntimeError: Unreachable executed」しか出ない
- これを使うとrustのエラーメッセージがconsole.errorとして出力される
- 開発中だけでも使うべき
console_error_panic_hook
使わない場合
console_error_panic_hook
使った場合
wee_alloc
- デフォルトで提供されるメモリアロケータより性能は少し低いがサイズがとても小さい
- 軽く使うくらいならデフォルトのメモリアロケータは性能が過剰
- これを使うだけでwasmのサイズが削減できるらしい(要確認)
demo
感想
- 開発自体はかなりしやすくなった
- ドキュメントを読めば簡単なことなら特に問題なく実装できそう
- 引数、戻り値の制限はあるが、致命的なレベルではないと思う
- 用途さえ見つかれば十二分に使えそうに感じる