WebAssembly Advent Calendar 2018 12日目 Rust における wasm-bindgen と wasm-pack と cargo-web と stdweb の違い の続編
あれから一年経ったので rustasync と rustwasm の進捗をチェック!
wasm-pack で JS の Promise を await できる非同期 Rust を書いて node.js で動かす
setup
rustup default nightly
rustup target add wasm32-unknown-unknown
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
cargo install cargo-generate
cargo generate --git https://github.com/rustwasm/wasm-pack-template
- nightly, wasm32-unknown-unknown, wasm-pack, cargo-generate をインストールして wasm-pack を使うためのディレクトリを生成する
- https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/getting-started.html
- 1.41.0-nightly
Cargo.toml の構成
[package]
name = "rust-wasm-nodejs"
version = "0.1.0"
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = []
[dependencies]
wasm-bindgen = { version = "0.2.55", features = ["serde-serialize", "nightly"] }
wasm-bindgen-futures = "0.4.5"
js-sys = "0.3.32"
web-sys = { vesrion = "0.3.32", features = ["console"] }
serde = { version = "1", features = ["derive"] }
-
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
について -
js-sys
について -
web-sys = { vesrion = "0.3", features = ["console"] }
について -
wasm-bindgen-futures
について
setTimeout を Promise で包んで wasm rust に渡す
use serde::Deserialize;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use js_sys::{Promise, Error};
#[wasm_bindgen(inline_js = "module.exports.sleep = function sleep(ms) { return new Promise((resolve)=> setTimeout(resolve, ms)); }")]
extern "C" {
fn sleep(ms: f64) -> Promise;
}
#[derive(Deserialize)]
pub struct Opt {
pub count: u32,
pub wait: f64,
}
#[wasm_bindgen]
pub async fn handler(opt: JsValue) -> Result<JsValue, JsValue> {
let Opt{ count, wait } = opt.into_serde()
.map_err(|err| JsValue::from(Error::new(&format!("{:?}", err))))?;
for i in 0_u32..count {
JsFuture::from(sleep(wait)).await?;
web_sys::console::log_1(&format!("{}", i).into());
}
Ok(JsValue::undefined())
}
-
opt.into_serde()
について -
js_sys::Promise
について -
Closure
について -
#[wasm_bindgen(inline_js = "...")]
について- wasm-bindgen の inline js を wasm の import 関数に割り当てる機能
- 他に
#[wasm_bindgen(module = "aws-sdk-js")]
みたいに書くこともできる - https://rustwasm.github.io/docs/wasm-bindgen/contributing/design/importing-js.html
- https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/module.html
- https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/raw_module.html
ビルド
wasm-pack build -t nodejs
-
wasm-pack build -t nodejs
について- nodejs は 8.0.0 あたりから WebAssembly に対応している
- https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly
- https://rustwasm.github.io/docs/wasm-pack/commands/build.html
nodejs から呼ぶ
node v12.13.1
const pkg = require("./pkg");
pkg.handler({count: 10, wait: 1000})
.then(console.log)
.catch(console.error);
出力
0
1
2
3
4
5
6
7
8
9
undefined
sleep (setTimeout) 関数を wasm rust の引数として渡す
wasm の関数に 非同期 IO 関数を渡したい。
wasm への入出力には通常 wasm-bindgen の serde-serialize
を使う。
しかしこれは一旦 JS オブジェクトを JSON 文字列に変換してから Rust の serde でデシリアライズしているため、 JS の関数を渡すことはできない。
一方で wasm-bindgen では JsValue (JS の any 値) が渡せるため、
型キャストすることで JsValue を Function として呼ぶことができる。
ここでは impl TryInto<JsValue> for Response
と impl TryFrom<JsValue> for Request
を実装して Rust の値と JsValue 値の変換できるようにした
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use js_sys::{Promise, Error, Function, Reflect};
use web_sys::console;
use std::convert::{TryFrom, TryInto};
pub struct Request {
pub count: u32,
pub wait: f64,
pub sleep: Box<dyn Fn(f64) -> Result<JsFuture, JsValue>>,
}
impl TryFrom<JsValue> for Request {
type Error = JsValue;
fn try_from(o: JsValue) -> Result<Self, Self::Error> {
#[derive(Deserialize)]
pub struct _Request {
pub count: u32,
pub wait: f64,
}
let sleep = {
let cb = Reflect::get(&o, &JsValue::from("sleep"))?;
if !Function::instanceof(&cb) {
return Err(JsValue::from(Error::new("sleep is not function")));
}
Function::unchecked_from_js(cb)
};
let _req: _Request = o.into_serde()
.map_err(|err| Error::new(&format!("{:?}", err)))?;
Ok(Request{
count: _req.count,
wait: _req.wait,
sleep: Box::new(move |ms|{
let prm = sleep.call1(&JsValue::NULL, &JsValue::from(ms))?;
if !Promise::instanceof(&prm) {
return Err(JsValue::from(Error::new("return value is not instanceof Promise")));
}
Ok(JsFuture::from(Promise::unchecked_from_js(prm)))
})
})
}
}
pub struct Response {
}
impl TryInto<JsValue> for Response {
type Error = JsValue;
fn try_into(self) -> Result<JsValue, Self::Error> {
#[derive(Serialize)]
pub struct _Response {
}
let Response {} = self;
JsValue::from_serde(&_Response{})
.map_err(|err| JsValue::from(Error::new(&format!("{:?}", err))))
}
}
#[wasm_bindgen]
pub async fn handler(req: JsValue) -> Result<JsValue, JsValue> {
set_panic_hook();
let Request{sleep, count, wait} = Request::try_from(req)?;
for i in 0_u32..count {
sleep(wait)?.await?;
console::log_1(&JsValue::from(format!("{}", i)));
}
Response{}.try_into()
}
pub fn set_panic_hook() {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}
-
Reflect::get
,Reflect::set
について
JS 側から sleep 関数を wasm rust に渡してみる
const pkg = require("./pkg");
pkg.handler({
sleep(ms){ return new Promise(resolve=> setTimeout(resolve, ms)); },
count: 10,
wait: 1000
}).then(console.log).catch(console.error);
これでうまくうごく。
0
1
2
3
4
5
6
7
8
9
{}
非同期 IO ストリームを模した periodic (setInterval) 関数を wasm rust に渡す
sleep を導入することができたので次は非同期 IO ストリームを模して setInterval をラップしたこんな関数を渡すことを考えてみる
const pkg = require("./pkg");
pkg.handler2({
sleep(ms){ return new Promise(resolve => setTimeout(resolve, ms)); },
periodic(ms, cb){
let i = 0;
let tid = setInterval(() => { cb(i++); }, ms);
return ()=> clearInterval(tid);
},
});
TypeScript ではこう
interface Request {
sleep(wait: number): Promise<void>,
periodic(wait: number, listener: (i: number)=> any): ()=> void,
}
この JS の引数オブジェクトの型は Rust での型はこんな感じになる
pub struct Request {
pub sleep: Box<dyn Fn(f64) -> Result<JsFuture, JsValue>>,
pub periodic: Box<dyn Fn(f64, Box<dyn FnMut(f64)>)
-> Result<Box<dyn FnOnce() -> Result<JsValue, JsValue>>, JsValue>>,
}
例によって JsValue から Rust の型にコンバートする TryFrom を実装する
impl TryFrom<JsValue> for Request {
type Error = JsValue;
fn try_from(o: JsValue) -> Result<Self, Self::Error> {
#[derive(Deserialize)]
pub struct _Request {
}
let sleep = {
let cb = Reflect::get(&o, &JsValue::from("sleep"))?;
if !Function::instanceof(&cb) {
return Err(JsValue::from(Error::new("sleep is not function")));
}
Function::unchecked_from_js(cb)
};
let periodic = {
let cb = Reflect::get(&o, &JsValue::from("periodic"))?;
if !Function::instanceof(&cb) {
return Err(JsValue::from(Error::new("periodic is not function")));
}
Function::unchecked_from_js(cb)
};
let _req: _Request = o.into_serde()
.map_err(|err| Error::new(&format!("{:?}", err)))?;
Ok(Request{
sleep: Box::new(move |ms|{
let prm = sleep.call1(&JsValue::NULL, &JsValue::from(ms))?;
if !Promise::instanceof(&prm) {
return Err(JsValue::from(Error::new("return value is not instanceof Promise")));
}
Ok(JsFuture::from(Promise::unchecked_from_js(prm)))
}),
periodic: Box::new(move |wait, cb|{
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(f64)>);
let stopfn = periodic.call2(&JsValue::NULL, &JsValue::from(wait), AsRef::<JsValue>::as_ref(&cb))?;
if !Function::instanceof(&stopfn) {
return Err(JsValue::from(Error::new("return value is not instanceof Function")));
}
let stopfn = Function::unchecked_from_js(stopfn);
Ok(Box::new(move ||{
let ret = stopfn.call0(&JsValue::NULL);
cb.forget();
ret
}))
})
})
}
}
- periodic を rust のクロージャに変換しているところ
let cb = Closure::<dyn FnMut(f64)>::new(cb);
で 作った cb を 最後cb.forget();
としているところに注意- ここでは JS の世界の setInterval の引数として Rust の
Box<dyn FnMut(f64)>
クロージャを wasm のヒープに置いてwasm_bindgen::closure::Closure
で包んで渡している - このヒープ上の
Box<dyn FnMut(f64)>
クロージャを最後 forget させるのを忘れるとメモリリークになる - https://rustwasm.github.io/wasm-bindgen/reference/passing-rust-closures-to-js.html
- https://docs.rs/wasm-bindgen/0.2.55/wasm_bindgen/closure/struct.Closure.html
- ここでは JS の世界の setInterval の引数として Rust の
-
js_sys::Function
の引数は&JsValue
が要求されるのでwasm_bindgen::closure::Closure
を AsRef を使って&JsValue
にアップキャストしている
ここまできれいにRust のコードに包むと使い勝手はふつうの Rust コードとほとんど変わらない
#[wasm_bindgen]
pub async fn handler(req: JsValue) -> Result<JsValue, JsValue> {
set_panic_hook();
let Request{periodic, sleep} = Request::try_from(req)?;
let stop = periodic(100.0, Box::new(|i|{
console::log_1(&JsValue::from(format!("{}", i)));
}))?;
sleep(3000.0)?.await?;
stop()?;
Response{}.try_into()
}
実行すると 3 秒間コンソールに数字が流れるようになる。
所感
- ここまで楽に書けるといよいよ実用できそうな気持ちがしてくる
-
aws-sdk-js
をwasm-bindgen
で wasm rust の世界に取り込んで aws lambda nodejs v12 runtime で実行したりできると個人的に嬉しい - wasm-bindgen での async の扱いの詳細はこちら
その他メモ
- wasm-pack-template についてくる
wee_alloc
について- rust をそのまま wasm にするとデフォルトアロケータ
dlmalloc
(の移植版) のランタイムが wasm ファイルのうち 10 KB を消費してしまう -
wee_alloc
に切り替えると速度を犠牲に wasm のファイルサイズを削減できる - https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/template-deep-dive/wee_alloc.html
- https://rustwasm.github.io/docs/book/reference/code-size.html
- https://github.com/rustwasm/wee_alloc
- rust をそのまま wasm にするとデフォルトアロケータ
- wasm-pack-template についてくる
utils::set_panic_hook
とconsole_error_panic_hook
について- wasm 内の rust のスタックトレースを記録しパニック時に
console.error
に吐くことができるようになる便利機能 - https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/template-deep-dive/src-utils-rs.html
- https://github.com/rustwasm/console_error_panic_hook
- wasm 内の rust のスタックトレースを記録しパニック時に
- serde-wasm-bindgen について
- wasm-bindgen 付属の
from_serde
,to_serde
は JsValue を JSON 文字列を介して rust の世界に持ってきている - serde-wasm-bindgen を使うと JsValue から直接 rust の値への変換をよしなに試みるようになる
- 実行速度は JSON 文字列を介する場合と比べてもケースバイケースで早くなったり遅くなったりするようだ
- 関数のデシリアライズはできない
- https://github.com/cloudflare/serde-wasm-bindgen
- https://docs.rs/serde-wasm-bindgen/0.1.3/serde_wasm_bindgen/index.html
- wasm-bindgen 付属の