LoginSignup
18
10

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-13

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

所感

その他メモ

18
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
10