Rust における wasm-bindgen と wasm-pack と cargo-web と stdweb の違い
※これは WebAssembly Advent Calendar 2018 の 12 日目の記事です
※ここでの作業はすべて nightly を前提としています
系統の違い
Rust から wasm へコンパイルするには emscripten を使う方法と、 wasm-bindgen を使う方法の2つあります
emscripten 系統
- もともとは clang の吐いた LLVMIR から同等の JavaScript を出力できるツール
- 後に asm.js , wasm も出力できるようになった
- libc 相当の system call が JavaScript の世界でエミュレートできる(ex. FileSystem
- 既存の C/C++ で書かれたコードをブラウザで動くように変換するのが目的
- C/C++/Rust から JavaScript を呼び出すのが主な使い方
- 今日はこの話はしない
主な登場人物
下に行くほど新しいツールです
- emscripten - C/C++ を LLVMIR を経由して asmjs やら wasm やらにコンパイルするためのツール - https://kripken.github.io/emscripten-site/docs/introducing_emscripten/about_emscripten.html
- Binaryen - 特に C/C++ を emscripten を使って wasm にコンパイルするためのツール - https://github.com/WebAssembly/binaryen
- emscripten-sys - Rust から emscripten のランタイムへのバインディングが入ったクレート - https://crates.io/crates/emscripten-sys
- stdweb - Rust から DOM を扱うためののインディングの入ったクレート - https://crates.io/crates/stdweb
- cargo-web - stdweb を使った Rust プロジェクトのビルドツール - https://crates.io/crates/cargo-web
wasm-bindgen 系統
- https://rustwasm.github.io
- Mozilla 肝いりの Rust を Web ブラウザで動かすためのツール
- emscripten 系列よりも新しいエコシステム
- JavaScript から Rust を呼び出すのが主な使い方
- 今日話すのはこれ
主な登場人物
- wasm-bindgen - 基本的な型のなどが入ったクレート -
- js-sys - Rust から JavaScript の値を生成するためのクレート
- web-sys - Rust から DOM とかを叩くためのクレート
- wasm-bindgen-futures - Rust の Future と JavaScript の Promise の型を相互変換するためのクレート
- wasm-bindgen-cli - wasm-bindgen や js-sys や web-sys クレートを使って生成した wasm ファイルに Rust と JS の FFI のランタイムを追加するビルドツール
- wasm-pack - wasm-bindgen を使った Rust コードを npm の package.json から呼ぶための設定ファイルを出力するビルドツール
おおまかな歴史
- 2000年代初頭: Java アプレット、Slackwave Flash 全盛期
- 2005年: GoogleMap 、 Ajax の登場
- 2007年: MS が Silverlight を発表
- 2010年: emscripten が登場し C/C++ コードを JavaScript に変換できるようになる
- 2011年: Google が NaCl , PNaCl を発表
- 2013年: Mozilla が asm.js を発表
- 2015年: Mozilla や Google が asm.js をより一般化した wasm を発表
- 2017年: stdweb
- 2018年: wasm-bindgen
wasm-bindgen-cli と wasm-pack の違い
- wasm-bindgen-cli は wasm と JavaScript のラッパーと TypeScript の型定義を出力してくれる
- wasm-pack は内部で wasm-bindgen-cli を使い、↑に加えて npm に公開するための package.json も出力する
wasm-bindgen-cli の生成物
これを
[package]
name = "iso-rust3"
version = "0.1.0"
authors = ["Legokichi Duckscallion <legokichi@gmail.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
futures = "0.1"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
wasm-bindgen = { version = "0.2", features = [ "serde-serialize" ] }
wasm-bindgen-futures = "0.3"
js-sys = "0.3"
[dependencies.web-sys]
version = "0.3"
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello, World!");
}
こうして
$ rustup target add wasm32-unknown-unknown
$ cargo install wasm-bindgen-cli
$ cargo build --target wasm32-unknown-unknown
$ wasm-bindgen target/wasm32-unknown-unknown/debug/iso_rust3.wasm --out-dir ./wasm
$ tree ./wasm
wasm
├── iso_rust3_bg.d.ts
├── iso_rust3_bg.wasm
├── iso_rust3.d.ts
└── iso_rust3.js
こうなる
/* tslint:disable */
import * as wasm from './iso_rust3_bg';
const lTextDecoder = typeof TextDecoder === 'undefined' ? require('util').TextDecoder : TextDecoder;
let cachedTextDecoder = new lTextDecoder('utf-8');
let cachegetUint8Memory = null;
function getUint8Memory() {
if (cachegetUint8Memory === null || cachegetUint8Memory.buffer !== wasm.memory.buffer) {
cachegetUint8Memory = new Uint8Array(wasm.memory.buffer);
}
return cachegetUint8Memory;
}
function getStringFromWasm(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory().subarray(ptr, ptr + len));
}
export function __wbg_alert_d4ac2591f07b50f4(arg0, arg1) {
let varg0 = getStringFromWasm(arg0, arg1);
alert(varg0);
}
/**
* @returns {void}
*/
export function greet() {
return wasm.greet();
}
↑は js-sys や web-sys を介して使った JavaScript 側の API を呼ぶためのランタイムコードがいろいろ追加されている。
ランタイムは JavaScript のオブジェクトを wasm の世界のメモリのヒープアロケーションをして、 Rust が読める形でデータを書き込み、そのポインタを Rust の関数に渡している。
より複雑なことをするとより多くのランタイムコードが出力される。
/* tslint:disable */
export function greet(): void;
/* tslint:disable */
export const memory: WebAssembly.Memory;
export function greet(): void;
これらの生成物は JavaScript から ESModule を使ってこのように呼ぶことがでいる
import("./wasm").then(iso_rust3 => {
iso_rust3.greet("World!");
});
wasm-pack の生成物
$ rustup target add wasm32-unknown-unknown
$ curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
$ wasm-pack build --dev
$ wasm-pack pack
$ pkg
├── iso-rust3-0.1.0.tgz
├── iso_rust3_bg.d.ts
├── iso_rust3_bg.wasm
├── iso_rust3.d.ts
├── iso_rust3.js
└── package.json
package.json 以外は同じ
{
"name": "iso-rust3",
"collaborators": [
"Legokichi Duckscallion <legokichi@gmail.com>"
],
"version": "0.1.0",
"files": [
"iso_rust3_bg.wasm",
"iso_rust3.js",
"iso_rust3.d.ts"
],
"module": "iso_rust3.js",
"types": "iso_rust3.d.ts",
"sideEffects": "false"
}
wasm-pack pack
は iso-rust3-0.1.0.tgz を生成する。 npm pack
相当。
おまけ: js-sys と web-sys
js-sys と web-sys を使うと、例えば WebAudioAPI の ScriptProcessor なども使える。
ここで FFT とかを Rust でできるようになれば嬉しいかもしれない。
ただし、このような煩雑で unwrap まみれのコードを書くハメになる。
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
use futures::prelude::*;
use wasm_bindgen_futures::{JsFuture, future_to_promise};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use js_sys::*;
use web_sys::*;
use serde_derive::*;
#[wasm_bindgen]
pub fn main(){
let window = window().unwrap();
let document = window.document().unwrap();
let media_devices = window.navigator().media_devices().unwrap();
#[derive(Serialize)]
struct Constraints {
audio: bool,
video: bool,
}
let constraints = JsValue::from_serde(&Constraints{audio: true, video: false}).unwrap();
let prm = media_devices.get_user_media_with_constraints(<MediaStreamConstraints as JsCast>::unchecked_from_js_ref(&constraints)).unwrap();
{
let cb = Closure::wrap(Box::new(move |media_stream: JsValue|{
console::log(&js_sys::Array::from(&JsValue::from_str("Hello, 💩!")));
let src = Url::create_object_url_with_source(&media_stream.into()).unwrap();
let audio = HtmlAudioElement::new_with_src(&src).unwrap();
(audio.as_ref() as &HtmlMediaElement).set_autoplay(true);
(audio.as_ref() as &HtmlMediaElement).set_controls(true);
let cb = Closure::wrap(Box::new(move |ev|{
console::log(&js_sys::Array::from(&JsValue::from_str("Hello, 💩!")));
let actx = AudioContext::new().unwrap();
let processor = (actx.as_ref() as &BaseAudioContext).create_script_processor().unwrap();
let cb = Closure::wrap(Box::new(move |ev: Event|{
// let abuf = (ev.as_ref() as &AudioProcessingEvent).input_buffer().unwrap();
// やってられん!
console::log(&js_sys::Array::from(&JsValue::from_str("Hello, 💩!")));
}) as Box<dyn FnMut(Event)>);
processor.set_onaudioprocess(Some(cb.as_ref().unchecked_ref()));
(processor.as_ref() as &AudioNode).connect_with_audio_node((actx.as_ref() as &BaseAudioContext).destination().as_ref());
cb.forget();
}) as Box<dyn FnMut(Event)>);
(audio.as_ref() as &EventTarget).add_event_listener_with_callback("loadedmetadata", cb.as_ref().unchecked_ref()).unwrap();
(document.body().unwrap().as_ref() as &Node).append_child(audio.as_ref()).unwrap();
cb.forget();
}) as Box<dyn FnMut(JsValue)>);
let prm = prm.then(&cb);
cb.forget();
}
}
JSのAPI のイベントハンドラひとつ書くにしても wasm_bindgen の Closure を使って
let cb = Closure::wrap(Box::new(move |ev|{
// hogehoge
cb.forget();
}) as Box<dyn FnMut(Event)>);
のようにクロージャのGCのタイミングも自分で指定せねばならず面倒くさい。(https://rustwasm.github.io/wasm-bindgen/examples/closures.html)
そして IF のランタイムは数百行に及ぶ(それでも emscripten のランタイムよりは遥かに少ないが)
また現状 web-sys のすべての機能を使うには以下のように features を WebIDL 毎に書いていかなければならない(そのうち改善されるらしい
[package]
name = "iso-rust3"
version = "0.1.0"
authors = ["Legokichi Duckscallion <legokichi@gmail.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
futures = "0.1"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
wasm-bindgen = { version = "0.2", features = [ "serde-serialize" ] }
wasm-bindgen-futures = "0.3"
js-sys = "0.3"
[dependencies.web-sys]
version = "0.3"
features = [
'AbortController',
'AbortSignal',
'AddEventListenerOptions',
...
'XmlHttpRequestUpload',
'XmlSerializer',
'XsltProcessor',
'console',
'css'
]
情報源
- https://rustwasm.github.io/ - 公式ブログ
- https://rustwasm.github.io/book/ - 入門者向けチュートリアル
- https://rustwasm.github.io/wasm-bindgen/ - 網羅的なドキュメント
- https://github.com/rustwasm/awesome-rust-and-webassembly - Awesome
- https://github.com/rustwasm/rfcs - rfcs