こんばんは、お盆の間に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)),
]