Help us understand the problem. What is going on with this article?

Rust における wasm-bindgen と wasm-pack と cargo-web と stdweb の違い

More than 1 year has passed since last update.

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 を呼び出すのが主な使い方
  • 今日はこの話はしない

主な登場人物

下に行くほど新しいツールです

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 の生成物

これを

Cargo.toml
[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"
src/main.rs
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

こうなる

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 の関数に渡している。
より複雑なことをするとより多くのランタイムコードが出力される。

iso_rust3.d.ts
/* tslint:disable */
export function greet(): void;
iso_rust3_bg.d.ts
/* 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 毎に書いていかなければならない(そのうち改善されるらしい

Cargo.toml
[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'
]

情報源

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした