RustでwasmでWebWorkerでTypescriptな開発

本記事は[sudachiclone-rs開発](https://qiita.com/Yasu-umi/items/2844d2bdff019b4c188d)の続編であり、Rustのコードをwasmにし、WebWorker内で状態を持ったまま使いつつ、Typescriptで開発する、ということをやる場合にハマりそうなところに悉くハマった気がするので、その備忘録です。

## 成果物
[レポジトリ](https://github.com/Yasu-umi/sudachiclone-wasm-demo): Rustで開発したsudachiclone-rsをwasmにコンパイルし、WebWorker内で動くようにしたもののデモ。
[デモページ](https://yasu-umi.github.io/sudachiclone-wasm-demo/): 上記をhostingしています。pipから辞書を取ってきてブラウザ上のみでわかち書きできるデモ。

## 使ったツール群
- [typescript-definition](https://github.com/onelson/typescript-definitions)

Rustのstructやfunctionからtypescriptの`d.ts`を生成してくれる優れもの。crates.ioにあるのもを使っていないのは今のRustでは動かないから。(詳しくは[issue参照](https://github.com/arabidopsis/typescript-definitions/issues/8))

- [wasm-bindgen](https://crates.io/crates/wasm-bindgen)

色んな人が解説してるので詳しくは割愛。wasmではそのまま扱えない文字列やObjectのやりとりを手助けしてくれるやつ。

- [console_error_panic_hook](https://crates.io/crates/console_error_panic_hook)

デバッグでめっちゃお世話になりました。Rust内で起こったエラーを`console.error`に良い感じに吐き出してくれる。

- [@wasm-tool/wasm-pack-plugin](https://www.npmjs.com/package/@wasm-tool/wasm-pack-plugin)

これも詳しくは割愛。webpackのbuild時にrustのbuildをしてくれる。

- [comlink](https://www.npmjs.com/package/comlink)

これもまた詳しくは割愛。WebWorkerとmain threadは通常postMessageでやりとりする必要があり、これを書くのが面倒だが、このライブラリはその辺良い感じにラップしてくれる。

- [worker-plugin](https://www.npmjs.com/package/worker-plugin)

WebWorkerをロードするためのパスをwebpackのビルドに合わせて良い感じに変えてくれるやつ。

- [cargo-wasi](https://crates.io/crates/cargo-wasi)

`cargo wasi build`とか`cargo wasi test`とかできるようにしてくれる。これのおかげで実際のブラウザなしでテストなどが可能になっている。

## ハマったポイントその1 < wasmで動かないrustのコード編 >

まず最初にやることは、既存のRustのコードがwasm/wasiにコンパイルしてみてテストする、ということ。
sudachiclone-rsはローカルに置いてある辞書ファイルのあるディレクトリにシンボリックリンクを張り読み込む、ということをしていたのですが、これに使っていた[symlink](https://crates.io/crates/symlink)は内部で`cfg`を使い、`target_os`によってコードを切り替えることで各OSで動くようになっていました。
wasm/wasiにコンパイルすると、`target_os`がwasm32/wasiになり、[`#[cfg(target_od = unix))]`](https://gitlab.com/chris-morgan/symlink/-/blob/master/src/lib.rs#L52)みたいなコードはコンパイルされなくなります。
つまり、`use symlink::{remove_symlink_dir, symlink_dir};`としようとしても`unresolved imports`が起きてしまいます。
ブラウザ上で動かす時は`remove_symlink_dir`や`symlink_dir`が呼ばれることはないので、実際には問題ないのですが、コンパイルは通す必要があります。
そこで[こんな感じ](https://github.com/Yasu-umi/sudachiclone-rs/blob/master/src/config.rs#L10:L24)で適当にエラーを返す関数を置いておくことで、`target_os`がsymlink内の関数をコンパイルしない場合にも見た目上同じ型の関数があるようになり、コンパイルが通せました。

```rust
#[cfg(any(target_os = "redox", unix, windows))]
use symlink::{remove_symlink_dir, symlink_dir};

#[cfg(not(any(target_os = "redox", unix, windows)))]
fn remove_symlink_dir<P: AsRef<Path>>(_path: P) -> Result<(), IOError> {
  Err(IOError::new(
    IOErrorKind::Other,
    "can't call remove_symlink_dir",
  ))
}
#[cfg(not(any(target_os = "redox", unix, windows)))]
fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(_src: P, _dst: Q) -> Result<(), IOError> {
  Err(IOError::new(IOErrorKind::Other, "can't call symlink_dir"))
}
```

## ハマったポイントその2 < wasm-bindgenが生成する`d.ts`編 >

wasm-bindgenはデフォルトで下記のような`d.ts`を生成してくれます。

```typescript
/* tslint:disable */
/* eslint-disable */
/**
* return TokenizeResult
* @param {string} text 
* @returns {any} 
*/
export function tokenize(text: string): any;
/**
* return ErrorValue
* @param {Uint8Array} system_dict_bin 
* @param {Uint8Array} char_def_bin 
* @returns {any} 
*/
export function read_from_bin(system_dict_bin: Uint8Array, char_def_bin: Uint8Array): any;
```

見ての通り、返り値が`any`になってしまっています。これだと何もわかりません。
本来jsから見たwasmの世界はArrayBufferしかありません。これをwasm-bindgenが良い感じにラップしてくれるのですが、やっていることはserdeを使ったserialize/deserializeです。
wasmとして露出する関数の返り値をstructにしたい場合、`JsValue`で包んでやる必要があり、これが`any`として解釈されます。

```rust
#[wasm_bindgen]
pub fn read_from_bin(system_dict_bin: Vec<u8>, char_def_bin: Vec<u8>) -> JsValue {
  #[cfg(feature = "console_error_panic_hook")]
  console_error_panic_hook::set_once();
  let result = ErrorValue {
    error: _read_from_bin(system_dict_bin, char_def_bin)
      .map_err(|e| format!("{}", e))
      .err(),
  };
  JsValue::from_serde(&result).unwrap()
}
```

理想的な解決としては、

1. `ErrorValue`のtypeが定義される
2. `read_from_bin`の返り値の型として`ErrorValue`のtypeが使われる

この両方が実現することですが、これは僕では無理でした。
なのでここれは、`typescript-definition`によって1のみ実現することとしました。

```rust
#[derive(Serialize, TypescriptDefinition)]
pub struct ErrorValue {
  error: Option<String>,
}
```

これだけです。

`@wasm-tool/wasm-pack-plugin`は型定義ファイルを生成しますが、modeがrelease(webpackのproduction mode)だと`#[wasm_bindgen]`の付いていないものは型定義ファイルにも出力されません。
では、出力したいstructにも`#[wasm_bindgen]`付けて、プロパティにも`pub`を付ければ、となるのですが、

```bash
the trait `wasm_bindgen::convert::traits::FromWasmAbi` is not implemented for `std::boxed::Box<[std::string::String]>`
```

という感じで怒られます。これは`wasm-bindgen`がstructをそのままjsに露出する場合、アクセスにはwasmのABI boundaryから(ArrayBufferから)値を復元できることが必要になり、全てのプロパティにこれが必要になってしまうせいです。

今回の目的はstructのプロパティに直接jsからアクセスすることではなく、あくまで返り値の型定義が欲しいだけなので、そこまでする必要はありません。
なので、[`extraArgs`に`--no-typescript`を追加し](https://github.com/Yasu-umi/sudachiclone-wasm-demo/blob/master/webpack.config.js#L36)。
その上で、型定義を出力するために`yarn run build-tsd`を[こんな感じ](https://github.com/Yasu-umi/sudachiclone-wasm-demo/blob/master/package.json#L7)で作り、webpackを走らせる前にビルドしてやります。
これでrust側の準備は完了です。

## ハマったポイントその3 < comlink & WebWorker編 >
これについては[Comlink + Rust で言語とスレッドの垣根を越えた WebAssembly 開発](https://qiita.com/3846masa/items/92d24e16ebb5151b08ba)という大変優れた記事があるので、こちらを読んでみてください。
wasmのimportはPromiseを返すためawaitしないといけない部分なんかは、[top levelでのawait](https://github.com/tc39/proposal-top-level-await)ができるようになればだいぶ書くのが楽になりそうな気がするのですが、今回は[initializeされたかどうかのフラグを1つ作って管理しています。](https://github.com/Yasu-umi/sudachiclone-wasm-demo/blob/master/src/worker.ts#L26)

あとは、typescriptで書く上でwasmの持つ関数(今回はread_from_binとtokenize)を`Comlink.expose`に渡すオブジェクトのプロパティとして持たせ、ここに型を持たせるため[thisの型を指定している](https://github.com/Yasu-umi/sudachiclone-wasm-demo/blob/master/src/worker.ts#L11:L12)ところくらいでしょうか。
wasmの持つ型はwasm-bindgenの生成した`d.ts`になるのですが、先述したように返り値がanyになってしまっているため、ここで`typescript-definition`で出力した型定義を返り値の型として指定することで、これ以降気にする必要がなくなります。

## ハマりポイント番外編その1 < lazy_staticがSyncを要求してくる >

今回の本題とは逸れるのですが、作業量としてはこれの対応に一番時間がかかったハマりポイント。
後述する理由でTokenizerオブジェクトをwasm側でglobalなオブジェクトとして持っておく必要があり、lazy_staticを使おうとしたところ、Tokenizerが`Sync`を満たす必要があり、つまり大体のものが`Sync`を満たす必要がでてきてしまいました。

今回はpythonからコピーした関係で`Rc<RefCell<T>>`をそこら中で使っており、これは`Sync`を満たしません。
そこで全ての`Rc<RefCell<T>>`を`Arc<Mutex<T>>`に変換し、`.borrow()`を`.lock()`に。
ここでdeadlockの問題が発生し、`.lock()`を`.try_lock()`に書き換えて2回目のロックを見つけ…というデバッグ作業を行いました。
(`lock`は解放をひたすら待つが、`try_lock`はその実行時に既に別にロックがされていた場合エラーを吐く)

## ハマりポイント番外編その2 < untar編 >

sudachiの辞書をブラウザに持ってくるため、pypiに登録されているpipパッケージをfetchすることにしました。
pipパッケージはtar.gz形式で配布されとり、gzipは[pako](https://www.npmjs.com/package/pako)で解凍できるのですが、tarはブラウザで動くものでtypescript型定義もあるものがなく、js-untarをtypescriptでコピーする必要がありました。
成果物は[こちら](https://github.com/Yasu-umi/sudachiclone-wasm-demo/blob/master/src/untar.ts)。

## 感想

色々と嵌ったり作り方で迷ったりしたものの、ツールは大変充実してきており、ちょっとしたものでもrustで書いてWebWorkerに乗せるような開発は十分実用的な段階に来てる感じがありました。

wasmのコンパイル後のサイズですが、dev buildでは5MBあり、あーやっぱりこんなもんなのかーと思ったのですが、release buildではなんと400KBしかありません。([デモページ](https://yasu-umi.github.io/sudachiclone-wasm-demo/)で確認してみてください。)
デモページのフロントエンドは`react`で`material-ui`をちょっと使っただけのもので、これが90KBくらいなので、これならちょっと大きいフロントのライブラリくらいと強弁できるのではないでしょうか。

そして肝心のsudachiclone-rsのwasm版の実用性としては…皆無でした。
公開されている辞書のうち一番小さい`sudachiclone_small`が40MBほどあり、これのfetchに手元で40秒弱かかります(そしてパースにも10秒ほど…)。
パースはコード側の問題もありそうなのですがfetchはどうにもなりません、とても実用的とは言えないでしょう。


## その他

Q: なぜwasmをWebWorkerの中で使う必要があったのか

A: 今回ブラウザで動かしたかったsudachiclone-rsは数十MBある辞書を読み込み、与えられた日本語のテキストをわかち書きするというもの。
この辞書の読み込みやわかち書きの処理はmain threadで実行すると他の処理をブロックしてしまい、ページが完全に動かなくなってしまうため、処理を別スレッドに移したかった。

Q: なぜlazy_static? Tokenizer自体を毎度main pageに転送すれば良いのでは?

A: 辞書を読み込んだTokenizerオブジェクトはそれ自体がMB単位のサイズになってしまうため、これをtokenizeの度にwasmに転送し、そこからwasmに渡して、オブジェクトをパースして再構成するコストが厳しそうだった。(というのは言い訳で実際には試してはおらず、FromWasmAbiを満たすの難しそう…ということで諦めました)