wasm-bindgen を使った Rust と JavaScript のデータ共有
Rust は WebAssembly ターゲットに対応しているため、Rust コードをブラウザ上で JavaScript から呼び出せる形でコンパイルすることが簡単にできます。さらに wasm-bindgen を使うと、Rust と JavaScript の間のデータのやり取りも簡潔に行えるようになります。
ここではビルドを wasm-pack を使って行う前提で説明します。
データの持ち方
wasm-bindgen では、オブジェクトを Rust 側で確保するか JavaScript 側で確保するかの二通りがあります。いずれのオブジェクトも Rust 側、JavaScript 側双方でアクセスすることが可能ですが、アクセス時のコストなどに差があります。
Rust 側に置いたオブジェクトの扱い
struct
の宣言に #[wasm_bindgen]
を付けると、その struct
は JavaScript 側に持ち出してアクセスできるようになります。
#[wasm_bindgen]
pub struct Foo {
pub x: i32,
pub y: i32,
}
関数についても同様に #[wasm_bindgen]
を付けることで JavaScript 側からアクセスできるようになります。
#[wasm_bindgen]
pub fn gen_foo(a: i32) -> Foo {
Foo {
x: 42,
y: a,
}
}
> pkg = require('./path/to/generated/package')
{
...
}
> f = pkg.gen_foo(100)
Foo { __wbg_ptr: 1114152 }
> f.x
42
> f.y
100
struct
を引数として渡すこともできます。
#[wasm_bindgen]
pub fn receive_foo(f: Foo) -> i32 {
f.x
}
この宣言では receive_foo
の呼び出し時に引数 f
の所有権が移動します。JavaScript 側から呼び出す場合も、引数として渡した Foo
オブジェクトは以後無効となります。なお、これは Foo
が Copy
であっても変わりません。もちろん、receive_foo
が &Foo
を受け取る場合は引数として渡した Foo
オブジェクトは無効とはなりません。
> f = pkg.gen_foo(100)
Foo { __wbg_ptr: 1114120 }
> pkg.receive_foo(f)
42
> f
Foo { __wbg_ptr: 0 }
> pkg.receive_foo(f)
Uncaught Error: null pointer passed to rust
...
Rust 側のオブジェクトを JavaScript 側で作る
#[wasm_bindgen(constructor)]
をつけたメソッドがあれば JavaScript 側でも new
でオブジェクトを作ることができます。
#[wasm_bindgen]
impl Foo {
#[wasm_bindgen(constructor)]
pub fn new() -> Foo {
Foo { x: 0, y: 0 }
}
}
> f = new pkg.Foo()
Foo { __wbg_ptr: 1114120 }
> f.x = 100
100
> f.x
100
> f.y
0
一方で、{ x: number, y: number }
型のものを直接 Foo
のインスタンスとして扱うことはできません。
> pkg.receive_foo({x: 10, y: 20})
Uncaught Error: expected instance of Foo
...
JavaScript 側とやりとりできるもの
次のようなものは #[wasm_bindgen]
をつけた関数の引数、戻り値、struct
のフィールドとして JavaScript 側とやりとりできます(完全な一覧ではありません)。
- 数値型 (
i32
,u64
など) bool
String
-
T
が JavaScript 側とやりとりできるとき、Option<T>
- TypeScript の型としては (
T
に対応する型) |undefined
となります
- TypeScript の型としては (
-
T
が JavaScript 側とやりとりできるとき、Vec<T>
-
T
が数値型のとき、Box<[T]>
struct
のフィールドに pub
(getter / setter が自動で用意されます) かつ Copy
でないものが存在する場合には #[wasm_bindgen(getter_with_clone)]
が必要です。
ドキュメント には、Vec<T>
は JavaScript の通常の配列 (Array
) となる一方、Box<[T]>
は Int32Array
などに変換されると書いてあります。ところが、手元で試した限りは Vec<i32>
も Int32Array
として扱われるようでした。加えて T
が数値型でなくても Box<[T]>
が Vec<T>
と同様に配列として変換されるようで、このあたりはよくわかりません。
パフォーマンス上の注意
Rust 側のオブジェクトについてもプロパティのアクセスは普通のオブジェクトのようにできるように見えますが、wasm-pack の生成した JavaScript コードを見てもわかるようにこのプロパティへのアクセスは setter / getter を介して行われます。string
や Int32Array
などにアクセスする場合には、getter の呼び出しで内容全体がコピーされるため、注意しないと思いがけず計算量が増大してしまう場合があります。
例えば
#[wasm_bindgen(getter_with_clone)]
pub struct A {
pub a: Vec<i32>,
}
#[wasm_bindgen]
pub fn get_a() -> A {
A { a: vec![0; 100000] }
}
のようなコードを Rust 側で書いた場合、JavaScript 側で
obj = pkg.get_a()
for (let i = 0; i < obj.a.length; ++i) {
...
}
のようにするとループの各繰り返しで obj.a
のコピーが走るため、obj.a.length
の 2 乗に比例した時間がかかってしまい、想定外に遅くなることが考えられます。この場合は、const a = obj.a;
のようにプロパティの内容を変数に持っておくことで、プロパティに直接アクセスする回数を減らすことが有効です。
JavaScript 側に置いたオブジェクトの扱い
前述の方法では、JavaScript のオブジェクト {x: 5, y: 10}
などを直接 Rust 側に渡すことはできません。例えば関数が多数のオプションを取る場合、それを個別の引数で指定するのは間違えやすいため、オブジェクトとして渡せると便利であると考えられます。
obj.drawRect({
leftX: 100,
topY: 200,
width: 50,
height: 200,
fillColor: "red",
borderColor: "black",
borderWidth: 2,
cornerRadius: 5
});
Rust の関数の引数や戻り値を JavaScript のオブジェクトとしたい場合、serde および tsify を使うと簡潔に記述することができます。
use serde::{Deserialize, Serialize};
use tsify::Tsify;
#[derive(Tsify, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Foo {
x: i32,
y: i32,
}
#[wasm_bindgen]
pub fn update_foo(a: Foo) -> Foo {
Foo { x: a.y, y: a.x }
}
> a = { x: 5, y: 10 }
{ x: 5, y: 10 }
> pkg.update_foo(a)
{ x: 10, y: 5 }
このとき、TypeScript の型宣言も自動で生成されます。
export interface Foo {
x: number;
y: number;
}
#[serde(rename = "name")]
を付けることで、JavaScript 側でのフィールド名を変更することもできます。
Option
値の使用
struct
のフィールドとして Option<T>
を使用することは可能ですが、デフォルトでは TypeScript での型は T | null
となります。
#[derive(Tsify, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Foo {
x: i32,
y: i32,
z: Option<i32>,
}
export interface Foo {
x: number;
y: number;
z: number | null;
}
フィールドを省略できるようにするには、この型を T | undefined
とする必要があります。このためには #[tsify(optional)]
を付けるとよいです。
#[derive(Tsify, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Foo {
x: i32,
y: i32,
#[tsify(optional)]
z: Option<i32>,
}
export interface Foo {
x: number;
y: number;
z?: number;
}
Discussion