RustでSocialForceModelを作ってWASMでそのエミュレーターを作ってみた

こんばんは、お盆の間にRustでSocialForceModelを作ってWASMでそのエミュレーターを作ってみたので。その過程で困ったことなど書いておきます

環境構築

主にThe Rust Wasm Bookに従って環境構築をしてみました。

Rustのバージョンが古い場合はRustのバージョンを上げておきましょう

rustup update

また、nodeのバージョンがv18.xだと、上記手順のnpm init wasm-app wwwの実行でエラーが出たため、v16.14.2に変更するとうまく通りました

また、wwwディレクトリの作成直後には.gitが含まれています。モノレポで管理したい人は忘れずに削除しておきましょう

wasm関連

上記webサイトではwasm-bindgenを使い、RustとJavascript間の相互呼び出しを可能にします。

wasm-bindgenではプリミティブな型で構成される構造体(およびその構造体から構成される構造体)やfunctionは#[wasm_bindgen]属性を付与することでJavascriptに渡すことができます。

しかし、引数や返り値にVec<T>が含まれるfunctionはwasm-bindgenでは対応していません。

#[wasm_bindgen]
struct Hoge{}

#[wasm_bindgen]
impl Hoge {
  pub fn generate_empty_vec() -> Vec<Hoge> {
    JsValue::from_serde(&vec![Hoge {},Hoge {}]).unwrap()
  }
  pub fn count_len(val: &JsValue) -> usize {
    let v: Vec<Hoge> = val.into_serde().unwrap();
    v.len()
  }
}

例えば上記のような実装ではコンパイル時に以下のようなエラーが発生します

the trait bound `Hoge: JsObject` is not satisfied
the following other types implement trait `JsObject`:
  CanvasRenderingContext2d
  Document
  DomRect
  DomRectReadOnly
  Element
  Event
  EventTarget
  HtmlCanvasElement
and 61 others
required because of the requirements on the impl of `IntoWasmAbi` for `Box<[Hoge]>`

wasm-bindgenのドキュメントには、serde-serializeを利用した実装方法が載っていましたが利用できませんでした。というのも上記のサンプルコードなら手元の環境で動いたのですが自前のコードの中の構造体ではなぜかJsValueからVec<T>へのserializeにErrorが発生します

この問題はserde-wasm-bindgenを使うことで解消できました。

[dependencies]
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
+serde-wasm-bindgen = "0.4.3"
#[wasm_bindgen]
struct Hoge{}

#[wasm_bindgen]
impl Hoge {
  pub fn generate_empty_vec() -> Vec<Hoge> {
-    JsValue::from_serde(&vec![Hoge {},Hoge {}]).unwrap()
+    serde_wasm_bindgen::from_value(&vec![Hoge {},Hoge {}]).unwrap()
  }
  pub fn count_len(val: &JsValue) -> usize {
-    let v: Vec<Hoge> = val.into_serde().unwrap();
+    let v: Vec<Hoge> = serde_wasm_bindgen::to_value(val);
    v.len()
  }
}

また、#[wasm_bindgen]が付与されているimplに含まれる関数は全てwasm経由でjsから参照可能になるため、Rust内で別のモジュールから関数を呼び出したいだけでpubを付与した場合でもjsから参照可能になってしまいます。もしその関数の返り値・引数にVec<T>が使われていると上記のようなJsValueの変換をせざるをえなくなってしまいます。

この問題は、jsから参照させる関数と参照させない関数をimplを複数定義し分割することで避けることができました。

#[wasm_bindgen]
struct Hoge{}

impl Hoge {
  // この関数は公開されない
  pub fn closed_to_wasm_function() -> Vec<Hoge> {
    // brabrabra...
  }
}

#[wasm_bindgen]
impl Hoge {
  // この関数は公開される
  pub fn opened_to_wasm_function(val: &JsValue) -> usize {
    // brabrabra...
  }
}

また、javascriptのコードでは所有権の確認は一切行ってくれません。当たり前なんですが意識せずに以下のようなコードを書いているとBの行でぬるぽのエラーになります。

const goal = Position.new(19, 19)

const nodeList = [
    Node.new(Position.new(0.0, 1.0), goal), // A
    Node.new(Position.new(2.0, 3.0), goal), // B
    Node.new(Position.new(4.0, 2.0), goal), // C
]

これはNode.new()の第二引数は参照ではなく実体であり、ここでgoalの所有権が奪われるからです。正しくは以下のように都度定義する必要がありますが、なかなかjsでこれを意識するのは難しいですね…

const nodeList = [
    Node.new(Position.new(0.0, 1.0), Position.new(19, 19)),
    Node.new(Position.new(2.0, 3.0), Position.new(19, 19)),
    Node.new(Position.new(4.0, 2.0), Position.new(19, 19)),
]

作ったモノ・書いたコード

https://gitlab.com/morifuji/social-force-model

tech  Rust  wasm 

See also