無駄と文化

実用的ブログ

wasm-bindgen で Record<Keys, Type> 型を取り扱う

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! 👋