Comlink + Rust で言語とスレッドの垣根を越えた WebAssembly 開発

**この記事は [WebAssembly Advent Calendar 2018 - Qiita](https://qiita.com/advent-calendar/2018/wasm) と [WebAssembly Advent Calendar 2018 - Adventar](https://adventar.org/calendars/2970) の 25日目の記事です．**
両方とも空いていたので，ふたつを繋ぐ架け橋を兼ねて．

**片方しか読んでいなかった人も，この機会にもう片方の Advent Calendar を読んでみるのはいかがでしょう 🎄**

---

**WebWorker 上での Rust を使った WebAssembly 開発について，ZIP 展開アプリを例に，環境構築から実行までを解説します．**
JavaScript でフロントエンド開発の経験があるひとを対象にして書いているため， Node.js 周りの説明は少なめになっています．

Rust についてはまだ勉強途中ですので，気になる点があったら編集リクエストかコメント欄に書いていただけると嬉しいです．

## TL;DR

**Rust で WebAssembly 書くときは， [wasm-bindgen] と [Comlink] を組み合わせることで快適な開発ができます．**

実際のコードは， https://github.com/3846masa/wasm-zip-extractor-app にて公開しています．
完成したアプリは， https://zip-extractor.netlify.com から体験できます．

![完成した画面](https://i.imgur.com/nDGulyr.png)

## 目次

- [環境構築 | Rust](#環境構築--rust)
  - [rustup](#rustup)
  - [cargo generate](#cargo-generate)
  - [Tips | Rust のバージョン](#tips--rust-のバージョン)
- [環境構築 | Node.js](#環境構築--nodejs)
  - [パッケージのインストール](#パッケージのインストール)
  - [webpack.config.js](#webpackconfigjs)
  - [npm scripts](#npm-scripts)
- [Rust で WebAssembly を書く](#rust-で-webassembly-を書く)
  - [Cargo.toml](#cargotoml)
  - [wasm-bindgen](#wasm-bindgen)
  - [JavaScript 側の実装](#javascript-側の実装)
  - [console_error_panic_hook](#console_error_panic_hook)
  - [Tips | macro の読み込み](#tips--macro-の読み込み)
  - [ファイル名リストの読み込み](#ファイル名リストの読み込み)
  - [Rust の impl を JavaScript の Class にする](#rust-の-impl-を-javascript-の-class-にする)
  - [ファイルを Extract する](#ファイルを-extract-する)
- [Comlink で WebWorker 内の WebAssembly とやりとりする](#comlink-で-webworker-内の-webassembly-とやりとりする)
  - [なぜ WebWorker を使うのか](#なぜ-webworker-を使うのか)
  - [なぜ Comlink を使うのか](#なぜ-comlink-を使うのか)
  - [TypedArray の受け渡し](#typedarray-の受け渡し)
  - [WebWorker と Dynamic import](#webworker-と-dynamic-import)
  - [同期処理からの書き換え](#同期処理からの書き換え)
  - [Tips | TypeScript](#tips--typescript)
- [おわりに](#おわりに)

## 環境構築 | Rust

### rustup

**[rustup]** を使って準備します．
Windows の場合は実行ファイルから，Linux / macOS の場合はスクリプトからインストールします．

```bash
curl https://sh.rustup.rs -sSf | sh
```

**WebAssembly のビルドには `wasm32-unknown-unknown` ターゲット用の環境が必要です．**
次のコマンドで `wasm32-unknown-unknown` の環境も用意します．

```bash
rustup target add wasm32-unknown-unknown
```

また，Rust で WebAssembly をビルドするために色々してくれる **[wasm-pack]** もインストールしておきます．
それと一緒に，テンプレートからセットアップをする `cargo generate` コマンドを使うため，**[cargo-generate]** も入れておきます．
ここで出てくる Cargo とは Rust のパッケージマネージャです．

```bash
cargo install wasm-pack cargo-generate
```

### `cargo generate`

今回は `src/crate` フォルダに準備していきます．
[wasm-pack-template] をベースにしてセットアップをします．

```
mkdir src; cd src
cargo generate --git https://github.com/rustwasm/wasm-pack-template --name crate
cd ../
```

### Tips | Rust のバージョン

**Rust で WebAssembly 開発をするには 1.30.0 以上のバージョンが必要です．**
よく `nightly` や `beta` チャンネルを推奨する記事がありますが，2019/07/07 現在では `stable` が 1.36.0 であるため必要ありません．
以降は 1.36.0 を前提に話していきますが，バージョンが古い場合は `rustup update` で更新してください．
バージョンは `rustc --version` で確認できます．

## 環境構築 | Node.js

開発するだけであれば，webpack などのバンドラは必要ないです．
しかし，実際の開発では webpack をベースにしたものが多いと思いますので，今回は webpack で解説していきます．

### パッケージのインストール

必要なパッケージを入れていきます． yarn を使っていますが，他のパッケージマネージャでも構いません．
[clean-webpack-plugin] は生成前に前のビルド結果を消すために使います[^note-clean]．
**[@wasm-tool/wasm-pack-plugin]** は，ビルド時に wasm-pack でのビルドも行ってくれるプラグインです．
**[worker-plugin]** は， `new Worker()` の記述から自動で WebWorker の JavaScript をビルドしてくれるプラグインです[^note-worker-loader]．

[^note-clean]: もちろん `rm -r` のコマンドで代用しても構いませんが，今回は処理を webpack に寄せていきます．
[^note-worker-loader]: 余談ですが [worker-loader] では WASM のバンドルにバグがあり，PR を送ってあります．（worker-loader の最終更新が半年前なので，いつ merge されるかわかりませんが...）

```bash
yarn add --dev \
  webpack webpack-cli webpack-dev-server \
  html-webpack-plugin clean-webpack-plugin \
  @wasm-tool/wasm-pack-plugin \
  worker-plugin
```

### `webpack.config.js`

`WasmPackPlugin` の `crateDirectory` には，先程作った Rust のディレクトリを指定します．

```js:webpack.config.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const WorkerPlugin = require('worker-plugin');
const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: path.resolve(__dirname, './src/index.js'),
  output: {
    globalObject: 'self',
    path: path.resolve(__dirname, './dist'),
  },
  plugins: [
    new CleanWebpackPlugin(),
    new WorkerPlugin(),
    new WasmPackPlugin({
      crateDirectory: path.resolve(__dirname, './src/crate'),
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, './src/index.html'),
    }),
  ],
};
```

### npm scripts

`yarn start` で開発用サーバが，`yarn build` でプロダクションビルドが生成されるようにします．

```json:package.json
{
  "scripts": {
    "build": "webpack --mode production",
    "start": "webpack-dev-server --debug --mode development"
  }
}
```

## Rust で WebAssembly を書く

Rust の基本文法などの解説は省略します．
[Rust Language Cheat Sheet](https://cheats.rs/) などで確認してください．

### `Cargo.toml`

`Cargo.toml` は， Node.js における `package.json` のような立ち位置のもので，依存関係やビルドオプションなどを記述できます．
今回は **[zip] crate** を使うため， `[dependencies]` に追記します．
このとき， crate によっては **features によって，機能を選べるものもあります．**
必要ない機能はオフにしておくと，余計なライブラリを含めずに済むこともあるので，確認しておくと良いと思います．
今回は，`deflate` feature のみに絞るため， `default-features` は `false` にして個別に指定します．

```toml:src/crate/Cargo.toml
[dependencies]
zip = { version = "0.5", default-features = false, features = ["deflate"] }
```

### wasm-bindgen

wasm-pack では **[wasm-bindgen]** を使ってビルドします．
**wasm-bindgen を使うことで，JavaScript とのデータのやり取りを簡単にしたり，型定義ファイルを作ってくれたりします．**
Rust で WebAssembly 開発をする上では，とても便利なツールになります[^note-bindgen]．

先程の過程で， `./src/crate/src` のなかに `lib.rs` と `utils.rs` が生成されています．
そのうち， **`lib.rs` がエントリファイル**になります．
主にコードを書くのは，以下の部分になります．

```rust:src/crate/src/lib.rs

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet() {
    alert("Hello, wasm!");
}
```

`extern` で JavaScript の関数やクラスを Rust 側から呼び出せるようになります[^note-extern]．
`pub fn` で定義された関数は，**`#[wasm_bindgen]` attribute で JavaScript 側から呼び出せるようになります．**

これらのコードはサンプルなので，消しておきます．
代わりに，手始めとして「ZIP を渡したらファイル名一覧を返す」関数を作ってみます．

```rust:src/crate/src/lib.rs
use wasm_bindgen::JsValue;

#[wasm_bindgen(js_name = getFilenameList)]
pub fn get_filename_list(buf: Vec<u8>) -> Vec<JsValue> {
    unimplemented!();
}
```

`#[wasm_bindgen]` attribute に注目すると，`js_name` を設定しています．
**Rust では，スネークケースで関数名をつけることが主流[^note-rust-snake]ですが， JavaScript ではキャメルケースが主流です．**
`js_name` を使うと，Rust の関数を別名でエクスポートできるようになります．
つまり，**Rust の `get_filename_list` を呼び出すときは JavaScript  側から `getFilenameList` と書けるようになるわけです．**

wasm-bindgen では，JavaScript からのデータを Rust のインタフェースに合わせてくれる機能があります．
例えば， `Vec<u8>` と `Uint8Array` は相互に変換されます．
また，`JsValue` を使えば数値だけではないデータもやりとりできます．
上のコードでは，JavaScript でいう文字列の配列 `string[]` を `Vec<JsValue>` として表しています[^note-string]．
**これで定義上は `Uint8Array` を引数にとり，`string[]` を返す関数になります．**

**`unimplemented!();` は，未実装であることを示すエラーを発生させます．**
一旦 Rust 側はここまでにして， JavaScript 側の実装に移っていきましょう．

[^note-bindgen]: wasm-bindgen がなくとも Rust で WebAssembly 開発はできます．
[^note-extern]: https://rustwasm.github.io/wasm-bindgen/examples/import-js.html
[^note-rust-snake]: https://doc.rust-lang.org/1.0.0/style/style/naming/README.html
[^note-string]: JavaScript の `string` を Rust の `String` に変える機能もありますが，現状では配列の中身に対する変換が効きません．(https://github.com/rustwasm/wasm-bindgen/issues/168)

### JavaScript 側の実装

まずは，`index.html` を作ります．
CSS フレームワークに [UIkit] を，アイコンに [Eva Icons] を使っています．
ビルドした JavaScript を読み込むための `<script>` タグは，`html-webpack-plugin` によって自動挿入されます．

[UIkit]: https://getuikit.com/
[Eva Icons]: https://akveo.github.io/eva-icons/

```html:src/index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>ZIP Extractor</title>
  <link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/uikit@3.1.6/dist/css/uikit.min.css"
  />
  <link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/eva-icons@1.1.1/style/eva-icons.min.css"
  />
</head>

<body>
  <section class="uk-section">
    <div class="uk-container">
      <h1>ZIP Extractor</h1>
      <p>
        <label>
          <span class="uk-button uk-button-primary">
            <span class="eva eva-lg eva-plus-outline"></span>
            Choose a file...
          </span>
          <input
            class="uk-hidden"
            type="file"
            accept="application/zip"
            data-js="input"
          />
        </label>
      </p>
      <ul class="uk-list uk-list-divider" data-js="list">
        <template data-js="list-item-template">
          <li class="uk-flex">
            <span class="uk-flex-auto" data-js="filename">
              {{ filename }}
            </span>
            <button
              class="uk-button uk-button-primary uk-button-small"
              type="button"
              data-js="extract-button"
            >
              <span class="eva eva-lg eva-cloud-download-outline"></span>
              Extract
            </button>
          </li>
        </template>
      </ul>
    </div>
  </section>
</body>
</html>
```

JavaScript 側では，`<input type="file">` でファイルが選択されたら，`Uint8Array` として読み込んで，先程の `getFilenameList` に渡します．

```js:src/index.js
const $input = document.querySelector('[data-js="input"]');
const $list = document.querySelector('[data-js="list"]');
const $template = document.querySelector('[data-js="list-item-template"]');

$input.addEventListener('change', loadZip);

async function readFile(file) {
  const ab = await new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener('loadend', () => resolve(reader.result));
    reader.addEventListener('error', reject);
    reader.readAsArrayBuffer(file);
  });
  return new Uint8Array(ab);
}

async function loadZip(ev) {
  const wasm = await import('./crate/pkg');

  const file = ev.target.files[0];
  if (!file) {
    return false;
  }
  ev.target.toggleAttribute('disabled', true);

  const buffer = await readFile(file);
  console.log(wasm.getFilenameList(buffer));
}
```

**webpack で WebAssembly を読み込むときは，現状では Dynamic import 経由である必要があります[^note-import]．**
ここまで書けたら，実際に動かしてみます．

![未実装であるためエラーが起きる](https://i.imgur.com/Qu8fHJv.png)

DevTools で見ると，エラーが起きると思います．これは先程の Rust のコードで `unimplemented!();` しているからです．
しかし，このエラーを見てもエラーの原因がわかりづらい状態で，開発していく上で不便になるでしょう．
ここで Rust 側のコードを変更して，エラーがわかりやすくなるようにします．

[^note-import]: WebAssembly を読み込むには，`fetch` などで WebAssembly ファイルを読み込んだあとに `WebAssembly.instantiate` に渡す必要があり，この処理を同期処理する機能が実装されていないためです． https://github.com/webpack/webpack/issues/6615

### console_error_panic_hook

**[console_error_panic_hook] crate** を使うと，エラー時により詳細なスタックトレースを表示してくれるようになります．
今回のテンプレートには，既に console_error_panic_hook が含まれています．

```rust:src/crate/src/utils.rs
pub fn set_panic_hook() {
    // When the `console_error_panic_hook` feature is enabled, we can call the
    // `set_panic_hook` function at least once during initialization, and then
    // we will get better error messages if our code ever panics.
    //
    // For more details see
    // https://github.com/rustwasm/console_error_panic_hook#readme
    #[cfg(feature = "console_error_panic_hook")]
    console_error_panic_hook::set_once();
}
```

ここでは，console_error_panic_hook を有効にするための関数 `set_panic_hook` を定義しています．
**先程説明した feature によって，console_error_panic_hook をバンドルするかどうかを切り替えられます．**
プロダクションビルドのときには，この feature を外しておくと良いでしょう．

この `set_panic_hook` は， **WebAssembly の中で1度実行すれば有効になります．**
では，どこで実行すればよいでしょうか．それには `#[wasm_bindgen(start)]` attribute が関わってきます．
次のコードを `src/crate/src/lib.rs` に書きます．

```rust:src/crate/src/lib.rs

#[wasm_bindgen(start)]
pub fn initialize() {
    utils::set_panic_hook();
}
```

**`#[wasm_bindgen(start)]` attribute は，WebAssembly を読み込んだ直後に自動で実行する関数を定義できます[^note-wasm-bindgen-start]．**
この中で， `utils::set_panic_hook` を呼び出すことで，エラー表示をわかりやすくできます．

実際にこの状態でもう一度 ZIP の読み込みを試してみましょう．

![console_error_panic_hook によるエラー表示](https://i.imgur.com/YnNNQgb.png)

先程とは異なり，エラー内容と行数が表示されていることがわかります．

[^note-wasm-bindgen-start]: wasm-bindgen@0.2.29 で実装されました．

### ファイル名リストの読み込み

`get_filename_list` を実装していきましょう．
[zip] crate のドキュメントにあるサンプルコードをベースに書いていきます．

```rust:src/crate/src/lib.rs
use std::io::Cursor;
use wasm_bindgen::JsValue;
use zip::ZipArchive;

#[wasm_bindgen(js_name = getFilenameList)]
pub fn get_filename_list(buf: Vec<u8>) -> Vec<JsValue> {
    let reader = Cursor::new(buf);
    let mut zip = ZipArchive::new(reader).unwrap();
    let mut filename_list: Vec<String> = Vec::new();

    for idx in 0..zip.len() {
        let file = zip.by_index(idx).unwrap();
        let name = file.name().to_owned();
        if !name.ends_with("/") {
            filename_list.push(name);
        }
    }

    filename_list.iter().map(|x| JsValue::from_str(x)).collect()
}
```

コード自体の解説は長くなりそうなので省きますが，最終行だけ説明します．
**最後の行では， `String` を `JsValue::from_str` によって， `JsValue` に変換しています．**
これによって， wasm-bindgen が JavaScript 側に渡せるデータ形式になります．
**今回は文字列の配列を送るために `JsValue` を使いましたが，数値の配列や数値・文字列単体の場合にはそのまま値を返すだけでよいです．**

これで実際に実行すると， ZIP ファイル内のファイル名が取得できていると思います．

![ファイル名を取得した結果](https://i.imgur.com/lpbf239.png)

### Rust の impl を JavaScript の Class にする

**wasm-bindgen は Rust の impl を JavaScript の Class として扱えるようにする機能があります．**
この機能を使って，先程の処理を `ZipReader` impl として書き直してみます．

```rust:src/crate/src/lib.rs

#[wasm_bindgen]
pub struct ZipReader {
    zip: ZipArchive<Cursor<Vec<u8>>>,
}

#[wasm_bindgen]
impl ZipReader {
    #[wasm_bindgen(constructor)]
    pub fn new(buf: Vec<u8>) -> ZipReader {
        let reader = Cursor::new(buf);
        let zip = ZipArchive::new(reader).unwrap();
        ZipReader { zip }
    }

    #[wasm_bindgen(js_name = getFilenameList)]
    pub fn get_filename_list(&mut self) -> Vec<JsValue> {
        let zip = &mut self.zip;
        let mut filename_list: Vec<String> = Vec::new();

        for idx in 0..zip.len() {
            let file = zip.by_index(idx).unwrap();
            let name = file.name().to_owned();
            if !name.ends_with("/") {
                filename_list.push(name);
            }
        }

        filename_list.iter().map(|x| JsValue::from_str(x)).collect()
    }
}
```

**`#[wasm_bindgen(constructor)]` attribute で， JavaScript における constructor の役割に当たるメソッドを指定できます．**
コードの中身としては，先程の `get_filename_list` 関数とほぼ同じです．

JavaScript 側も Class を使うように書き換えてみます．

```js:src/index.js
async function loadZip(ev) {
  const wasm = await import('./crate/pkg');

  const file = ev.target.files[0];
  if (!file) {
    return false;
  }
  ev.target.toggleAttribute('disabled', true);

  const buffer = await readFile(file);
  const zipReader = new wasm.ZipReader(buffer);
  const filenameList = zipReader.getFilenameList();

  console.log(filenameList);
  zipReader.free();
}
```

とくに解説するほどコードは変わっていないですが，最後の `zipReader.free` について解説しておきます．
**WebAssembly で確保したメモリ領域は，明示的に開放してあげないと確保されたままになります．**
開放しないと，ファイルを読み込むたびにどんどんメモリ領域を圧迫していきます．
**使い終わったら開放するのを心がけていきましょう．**

実行すると先程と同じ結果になると思います．

### ファイルを Extract する

続いては，「ファイル名を渡すとバイナリを返す」メソッドを作りましょう．

```rust:src/crate/src/lib.rs
#[wasm_bindgen]
impl ZipReader {
    #[wasm_bindgen(constructor)]
    pub fn new(buf: Vec<u8>) -> ZipReader {
        // ...
    }

    #[wasm_bindgen(js_name = getFilenameList)]
    pub fn get_filename_list(&mut self) -> Vec<JsValue> {
        // ...
    }

    #[wasm_bindgen(js_name = getBinary)]
    pub fn get_binary(&mut self, filename: &str) -> Vec<u8> {
        let zip = &mut self.zip;
        let mut file = zip.by_name(filename).unwrap();
        let mut buf = Vec::with_capacity(file.size() as usize);
        std::io::copy(&mut file, &mut buf).unwrap();
        buf
    }
}
```

今度は引数に `&str` を，返り値に `Vec<u8>` を指定しています．
**wasm-bindgen によって， `&str` は `string`， `Vec<u8>` は `Uint8Array` となるため， JavaScript 側では， `string` を与えると `Uint8Array` が返る関数として出力されます．**

JavaScript 側も書き換えていきます．
まずは，ファイルを保存するためのライブラリ [file-saver] をインストールします．

```bash
yarn add file-saver
```

次のようにして，ファイル名のリストから，`<template>` に書いた内容をベースに DOM を生成します．

```js:src/index.js
import saveAs from 'file-saver';

function extractFile(zipReader, filename) {
  const buffer = zipReader.getBinary(filename);
  const blob = new Blob([buffer], { type: 'application/octet-stream' });
  const basename = filename.split('/').pop();
  saveAs(blob, basename);
}

async function loadZip(ev) {
  // ...

  for (const filename of filenameList) {
    const $item = document.importNode($template.content, true);
    const $filename = $item.querySelector('[data-js="filename"]');
    const $extract = $item.querySelector('[data-js="extract-button"]');

    $filename.textContent = filename;
    $extract.addEventListener('click', () => {
      $extract.toggleAttribute('disabled', true);
      extractFile(zipReader, filename);
      $extract.toggleAttribute('disabled', false);
    });

    $list.appendChild($item);
  }
}
```

このとき，ボタンを押したら `extractFile` 関数を呼び出すようにします．
`extractFile` 関数では， `getBinary` でデータを取ってきたあとに `Blob` へ変換して `saveAs` に渡しています．

ここまでを試してみましょう．
ここで注意してほしいのが，**Development 環境だと動作が極端に遅い点**です．
あまり大きなファイルを出力しようとすると，かなり待たされることになります．

![完成した画面](https://i.imgur.com/nDGulyr.png)

うまく保存されたら，基本の部分は完成です．

## Comlink で WebWorker 内の WebAssembly とやりとりする

### なぜ WebWorker を使うのか

WebAssembly の処理が重たいとどうなるでしょうか．
先程，大きなファイルの出力は避けるように言いましたが，いま試してみてください．

読み込んでいる間，ブラウザは固まったままになると思います．
WebAssembly は同期処理されるため，**UI スレッドで実行するとスレッドが専有されて固まります．**
処理をしている間，ブラウザが動かないのはとても困ってしまいます．

そこで新たにスレッドを用意して，そちらで実行させる手段を取ります．
そのとき必要になるのが，**WebWorker** です．

### なぜ Comlink を使うのか

処理を WebWorker に移すのは良いですが，DOM の操作などは UI スレッドで行う必要があります．
よって， UI スレッドと WebWorker 間でデータのやりとりをしなければなりません．
**このデータのやり取りは， `postMessage` を使ってやるのですが，書いてみるとコードがかなり複雑になってしまいます．**

**それを，いかにも関数や Class を使っているかのように書けるよう，これらの処理をラップしてくれるライブラリが [Comlink] です．**
Comlink について解説している記事は最近増えてきているため，ここでは割愛します．

まずは， Comlink をインストールしましょう．

```bash
yarn add comlink
```

### TypedArray の受け渡し

WebWorker 間は `postMessage` でデータをやりとりしますが，渡すデータは **[structured clone algorithm]** によって変換されます．
単純にそのまま `Uint8Array` を渡すと，値がすべて複製されてしまうため，実行に時間がかかります．

WebWorker では [Transferable] なオブジェクトを明示して送ることで，複製せずにオブジェクトを送ることができます．
Transferable なオブジェクトは，2019/07/07 現在 `ArrayBuffer` `MessagePort` `ImageBitmap` `OffscreenCanvas` の4つです．
`Uint8Array` のような TypedArray は， `buffer` プロパティに `ArrayBuffer` を保持しており，複製無しでデータを送ることができます．

[Transferable]: https://developer.mozilla.org/ja/docs/Web/API/Transferable

Comlink では，`transferHandler` を設定することで，Transferable を指定する処理を書くことができます．
他にも， structured clone algorithm で変換できないオブジェクトを相互変換する処理も書けます．
今回は `TypedArray` 全般を transfer できるような `transferHandler` を書いてみます．

```js:src/comlink.js
import * as Comlink from 'comlink';

Comlink.transferHandlers.set('TypedArray', {
  canHandle(obj) {
    return [
      Int8Array,
      Uint8Array,
      Uint8ClampedArray,
      Int16Array,
      Uint16Array,
      Int32Array,
      Uint32Array,
      Float32Array,
      Float64Array,
    ].some((type) => obj instanceof type);
  },
  serialize(obj) {
    return [obj, [obj.buffer]];
  },
  deserialize(obj) {
    return obj;
  },
});

export * from 'comlink';
```

`serialize` で変換して，`deserialize` でシリアライズ結果をもとに戻します．
`serialize` の返り値は配列で，最初の要素がシリアライズしたオブジェクト，次の要素が Transferable を列挙した配列を渡します．
今回はオブジェクトのシリアライズは必要ないため， `serialize` と `deserialize` ではオブジェクトをそのまま返しています．

**`transferHandler` は，UI スレッド側，WebWorker 側のどちらともで読む必要があるため， Comlink を読み込むときに一緒に読ませます．**
**`export * from 'comlink';` として，他のファイルから Comlink を使うときは，`src/comlink.js` から読み込むようにすると便利です．**

### WebWorker と Dynamic import 

続いて WebWorker を書いていきます．
Comlink に WebAssembly のクラスを渡してあげるだけではありますが，ここでひとつ問題があります．
先程述べたとおり，**webpack で WebAssembly を読み込むには Dynamic import 経由である必要があるのです．**
よって，次のようなコードを書かざるを得なくなります．

```js
(async () => {
  const wasm = import('./crate/pkg');
  Comlink.expose(wasm, self);
})().catch(console.error);
```

しかし，この方法には大きな問題があります．
それは， **Dynamic import されるまえに UI スレッドから呼び出しがかかるとエラーが起きる点** です．
そのため，**Dynamic import がされたかどうかを管理しなければなりません．**

ここで， Comlink の実装を思い出してみると，UI スレッド側では `Proxy` を使ったオブジェクトを返すようになっています．
つまり，**Comlink に expose したオブジェクト内をあとから書き換えても使えると考えられます．**

コードを書いて説明していきましょう．

```js:src/worker.js
import * as Comlink from './comlink';

const wasmImport = import('./crate/pkg');

const wasmModule = {
  async initialize() {
    const wasm = await wasmImport;
    Object.assign(wasmModule, wasm);
  },
};

Comlink.expose(wasmModule, self);
```

`wasmModule` がベースのオブジェクトです．
この `wasmModule.initialize` で， Dynamic import を待ち，読み終わったら `wasmModule` にマージします[^note-merge]．
**これによって， `wasmModule.initialize` を実行したあとであれば， `wasmModule` から WebAssembly のコードを呼び出せるようになります．**

[^note-merge]: 今回は雑に書いていますが，しっかり組むなら `Object.assign` は1度だけ実行されるように組むべきです．

### 同期処理からの書き換え

UI スレッド側のコードも書き換えてみましょう．
基本的な方針は，先程のコードで **WebAssembly 側の関数を呼び出していれば，すべて `await` させるだけです．**

```js:src/index.js
import * as Comlink from './comlink';

const wasm = Comlink.wrap(new Worker('./worker.js', { type: 'module' }));

async function extractFile(zipReader, filename) {
  const buffer = await zipReader.getBinary(filename);
  const blob = new Blob([buffer], { type: 'application/octet-stream' });
  const basename = filename.split('/').pop();
  saveAs(blob, basename);
}

async function loadZip(ev) {
  await wasm.initialize();

  const file = ev.target.files[0];
  if (!file) {
    return false;
  }
  ev.target.toggleAttribute('disabled', true);

  const buffer = await readFile(file);
  const zipReader = await new wasm.ZipReader(buffer);
  const filenameList = await zipReader.getFilenameList();

  for (const filename of filenameList) {
    const $item = document.importNode($template.content, true);
    const $filename = $item.querySelector('.js-filename');
    const $extract = $item.querySelector('.js-extract-button');

    $filename.textContent = filename;
    $extract.addEventListener('click', async () => {
      $extract.toggleAttribute('disabled', true);
      await extractFile(zipReader, filename);
      $extract.toggleAttribute('disabled', false);
    });

    $list.appendChild($item);
  }
}
```

...コードが変わらなすぎて，逆にどこを変えたのかがわかりづらいですね．
今回は予め `new Worker` で読み込んでおいて， `Comlink.wrap` でラップした `wasm` 変数を置いておきます．
そして，**先程のコードで Dynamic import していたところで `wasm.initialize` を待ちます．**
あとは， WebAssembly のクラスや関数に `await` をつけて待たせるだけです．

大きなファイルで実際に試してみてください．
先程は処理の途中ではブラウザが固まってしまい，閉じることすら困難でしたが， **WebWorker に処理を移すとブラウザは固まらずに済んでいると思います．**

### Tips | TypeScript

wasm-bindgen の出力は，TypeScript の型定義ファイルも生成するため，ジェネリクスを使うと補完が効くため便利になります．

```ts:src/worker.ts
export type ModuleType = typeof import('./crate/pkg') & typeof wasmModule;
```

```ts:src/index.ts
const wasm = Comlink.wrap<import('./worker').ModuleType>(new Worker('./worker', { type: 'module' }));
```

VSCode を使っている場合は，JSDoc を書くと JavaScript でも補完が効くようになります．

```js:src/worker.js
export const wasmModule = {
  // ...
};
```

```js:src/index.js
/** @typedef {typeof import('./crate/pkg') & typeof import('./worker').wasmModule} WorkerModuleType */
/** @type {Comlink.Remote<WorkerModuleType>} */
const wasm = Comlink.wrap(new Worker('./worker.js', { type: 'module' }));
```

## おわりに

ZIP 展開アプリを例に Comlink と wasm-bindgen によるシームレスな開発を紹介しました．
WebAssembly についての記事はかなり増えてきて，よい開発環境も揃いつつあります．

これから WebAssembly の技術は，フロントエンドエンジニアにとって学ばなければならない領域だと思いますが，いろんな技術を活用して楽で楽しいコーディングをしたいですね．

ここまでお読みいただき，ありがとうございました．

[wasm-bindgen]: https://rustwasm.github.io/wasm-bindgen/
[Comlink]: https://github.com/GoogleChromeLabs/comlink
[rustup]: https://rustup.rs/
[wasm-pack]: https://rustwasm.github.io/wasm-pack/book/
[cargo-generate]: https://github.com/ashleygwilliams/cargo-generate
[wasm-pack-template]: https://github.com/rustwasm/wasm-pack-template
[clean-webpack-plugin]: https://github.com/johnagan/clean-webpack-plugin
[@wasm-tool/wasm-pack-plugin]: https://github.com/wasm-tool/wasm-pack-plugin
[worker-plugin]: https://github.com/GoogleChromeLabs/worker-plugin
[worker-loader]: https://github.com/webpack-contrib/worker-loader
[zip]: https://github.com/mvdnes/zip-rs
[console_error_panic_hook]: https://github.com/rustwasm/console_error_panic_hook
[file-saver]: https://www.npmjs.com/package/file-saver
[structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm