RustでWebAssembly: Rust側に状態をもたせたり、JS ObjectをRustに渡したり、DOMにアクセスする


はじめに

前回 は単にconsoleにログを出すだけでした。今回は、Rust側に状態をもたせたり、DOMにアクセスしたり、JsのObjectをRustに渡してみます。この辺からボチボチどうやるのが一番いいのかもうわからなくなりつつあります。

※ 基本的なVersionはこの記事と同じです。

Crates

今回使うcrateたちはこんな感じになっています。
- wasm-bindgen の features が増えた
- serde が登場
- web-sys の features に Window, Document, HtmlDivElement が増えた

Cargo.toml
...略
[dependencies]
wasm-bindgen = { version = "0.2.67", features = ["serde-serialize"] }
js-sys = "0.3.44"
serde = { version = "1.0", features = ["derive"] }

[dependencies.web-sys]
version = "0.3.44"
features = [
  'Window',
  'Document',
  'HtmlDivElement',
  'console',
]

内容

Rust側に状態をもたせる

いくつか方法はありそうですが、一番シンプルだと思うのは、JS側でRustのObjectを生成し、保持させておくことかなと思います。JS側としても特に違和感のないところです。

例えば、以下のようにRust側をしておきます。

#[wasm_bindgen]   // これ大事
pub struct MyApp {
    age: u32,
}

#[wasm_bindgen]  // この中のpubメソッドはJSから呼べるようになる。
impl MyApp {
    pub fn new(age: u32) -> MyApp {
        MyApp { age }
    }
}

で、 JS側で MyApp.new() を呼ぶとRust側のObjectをKeepできます。

const myApp = wasm.MyApp.new(88);
console.log(myApp.age);  // -> 88

JS ObjectをRustに渡す

例えば、各種設定のようなものをまとめてRust側に渡したいときに

const myApp = MyApp.new({"key1": value1, "key2": value2});

みたいにやりたい。全部指定するのは面倒なので default値も設定しておきたい。

という場合があります。
以下のようにやると、だいぶイメージに近い感じになりました。

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
pub struct MyAppOptions {
    pub name: String,
    pub age: Option<u32>,
    pub email: Option<String>,
}

#[wasm_bindgen]
pub struct MyApp {
    name: String,
    age: u32,
    email: Option<String>,
}

#[wasm_bindgen]
impl MyApp {
    pub fn new(options: JsValue) -> MyApp {
        let opts: MyAppOptions = options.into_serde().unwrap();
        MyApp {
            name: opts.name,
            age: opts.age.unwrap_or(18),  // default値で埋めておく場合
            email: opts.email,            // Option のままの場合
        }
    }
}

ポイントは、

let opts: MyAppOptions = options.into_serde().unwrap();

の辺りかな。 MyAppOptions#[derive(Serialize, Deserialize)] がついているので JsValue からこの構造体に値をうつしてくれます。

呼び出すJS側は、以下のように書くことができます。

const myApp = wasm.MyApp.new({name: "mokemoke", email: "[email protected]", other: 88});

age が省略できていることと、 otherという関係ないKeyがあってもエラーにならないことがポイントです。

DOMにアクセスする

DOMにアクセスするには web_sys の ものを色々使います。
WindowDocumentにはよくアクセスするので以下のようなショートカットを作っておくとちょっとだけ便利です。

fn window() -> web_sys::Window {
    web_sys::window().expect("no global `window` exists")
}

fn document() -> web_sys::Document {
    window()
        .document()
        .expect("should have a document on window")
}

document.getElementById() などをRustで書くとこんな感じになります。


fn get_element_by_id<T: JsCast>(id: &str) -> T {
    document()
        .get_element_by_id(id)
        .expect("not found")
        .dyn_into::<T>()
        .map_err(|_| ())
        .unwrap()
}

この .dyn_into::<型>() とかはJs系のCastをするときに結構出てきます。

で、
例えば、指定したdiv要素のinner_textを更新するにはこんな感じにかけば良いです。

#[wasm_bindgen]
impl MyApp {
    pub fn show_status(&self, div_id: &str) {
        let div: web_sys::HtmlDivElement = get_element_by_id(div_id);
        div.set_inner_text(&format!("name={}, age={}, email={}",
                                    self.name,
                                    self.age,
                                    self.email.as_ref().unwrap_or(&"".into()) // 野暮ったい...
        ));
    }
}

それぞれどういうMethodがあるかは web_sysのAPI Document などを見ると良いです。

さいごに

コード全体はこちらになります。

なんか、 unwrap() したり dyn_into() したり、serdeがどうこうしたり、、と慣れるまで何がなんだかわかりません。今もよくわかってないところも多いですが、コンパイラのエラーメッセージ(これが結構親切なのがとても助かる)を見ながら治せるくらいにはなってきました。