wasm-pack で JS の Promise を await できる非同期 Rust を書いて node.js で動かす


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

Cargo.toml の構成

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"] }

setTimeout を Promise で包んで wasm rust に渡す

src/lib.rs
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())
}

ビルド

wasm-pack build -t nodejs

nodejs から呼ぶ

node v12.13.1

example.js
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 Responseimpl 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();
}

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_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 秒間コンソールに数字が流れるようになる。

所感

その他メモ