wasm-bindgen を使うと Rust の構造体から TypeScript の型情報を生成して wasm で受け渡しできるようになる。
やるべきことは #[wasm_bindgen]
属性をつけるだけで、wasm-bindgen がよしなにしてくれる。
// Rust use wasm_bindgen::prelude::*; #[wasm_bindgen(getter_with_clone)] pub struct User { name: String, age: u32, } #[wasm_bindgen] pub fn create_user(name: String, age: u32) -> User { User { name, age } } #[wasm_bindgen] pub fn greet(user: &User) -> String { format!( "Hello, my name is {} and I am {} years old", user.name, user.age ) }
コンパイルするとこのような型情報が生成される。
// .d.ts export class User { free(): void; age: number; name: string; } export function create_user(name: string, age: number): User; export function greet(user: User): string;
Rust の構造体がほぼそのまま TypeScript から使えるようになるのはとても便利だ。
ところで Rust の構造体はあらかじめフィールドが定まっていなければいけないので、TypeScript の Record<Keys, Type>
型のように任意のキーを受け付けるように定義することはできない。
では Record<Keys, Type>
型を wasm-bindgen で扱いたいときはどうすればいいだろうか。
Tsify-next を使うとできる。
HashMap を使ってこのように書くと、
// Rust use std::collections::HashMap; use tsify_next:: declare; #[declare] pub type MyRecord = HashMap<String, u32>;
Record<string, number>
として型情報を生成してくれる。
// .d.ts export type MyRecord = Record<string, number>;
ところがこの MyRecord は FromWasmAbi や IntoWasmAbi を impl していないので引数として渡したり関数から返したりできない。
// Rust pub fn do_something(record: MyRecord) { ... } // ^^^^^^^^ the trait `wasm_bindgen::describe::WasmDescribe` is not // implemented for `HashMap<std::string::String, u32>`
これでは不便なのでこうする。
// Rust use std::collections::HashMap; use tsify_next::Tsify; #[derive(Tsify)] pub struct MyRecord(HashMap<String, u32>);
Type Aliase をやめて1要素の Tuple Struct として定義する。
// .d.ts export type MyRecord = Record<string, number>;
すると先ほどと同じく Record<string, number>
として型情報が生成される。
Type Aliase として定義したときとの違いは、Tuple Struct として定義した MyRecord はユーザー定義型であるということだ。
ユーザー定義型であれば FromWasmAbi や IntoWasmAbi を impl できる。
しかも属性を付けるだけでマクロによってコード生成されるので追加でコードを書く必要はない。
// Rust use serde::{Deserialize, Serialize}; use std::collections::HashMap; use tsify_next::Tsify; use wasm_bindgen::prelude::*; #[derive(Tsify, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct MyRecord(HashMap<String, u32>);
#[derive(Serialize, Deserialize)]
と #[tsify(into_wasm_abi, from_wasm_abi)]
を追加した。
MyRecord を引数や返り値にする関数を書いてみる。
// Rust #[wasm_bindgen] pub fn create_my_record(keys: Vec<String>) -> MyRecord { let mut map = HashMap::new(); for (i, s) in keys.into_iter().enumerate() { map.insert(s, i as u32); } MyRecord(map) } #[wasm_bindgen] pub fn keys(my_record: MyRecord) -> Vec<String> { my_record.0.into_iter().map(|(k, _)| k).collect() } #[wasm_bindgen] pub fn values(my_record: MyRecord) -> Vec<u32> { my_record.0.into_iter().map(|(_, v)| v).collect() }
今度はちゃんとコンパイルできる。生成される型情報はこのようになる。
// .d.ts export function create_my_record(keys: (string)[]): MyRecord; export function keys(my_record: MyRecord): (string)[]; export function values(my_record: MyRecord): Uint32Array;
もちろん TypeScript から呼び出せばちゃんと動作する。
// TypeScript const record = create_my_record(['hoge', 'fuga', 'piyo']); console.log('keys: ', keys(record)); // => keys: [ 'piyo', 'hoge', 'fuga' ] console.log('values: ', values(record)); // => values: Uint32Array(3) [ 1, 0, 2 ]
Tsify-next がとても便利。
それでは、enjoy! 👋