wasm-bindgen + wasm-pack + webpack で フロントエンド

色々やったら手数が多くてややこしかったので、現時点のセットアップをメモがてら記事に書くことにした。

想定読者は フロントエンドプログラマ or Rust プログラマ。両方詳しいことは想定していないので、それぞれで自明なこともみっちり書くことにした。

node/npm と rust/cargo (rustup) のセットアップは略。ググればいくらでも出てくるので…

## 概要

- webpack: 静的アセットを生成するバンドラー
  - ts-loader: webpack で `.ts` ファイルを typescript として読み込む loader。wasm-pack が typescript の型定義を生成するので、せっかくなので ts もセットアップする
  - @wasm-tool/wasm-pack-plugin: webpack 内で wasm-pack で生成されるコードを読み込むもの
  - html-webpack-plugin: webpack で index.html を生成するプラグイン
- webpack-dev-server: 開発用サーバー
- rust
  - wasm-pack: rust プロジェクトを js で読み込みやすい wasm にコンパイルする
  - wasm-bindgen: `rust <-> js` のデータ構造をバインディングするライブラリ。これを使わないと数値と ArrayBuffer しか受け渡せないのでほぼ必須

## とりあえず動かしてみたい人用

```
$ curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
$ git clone git@github.com:mizchi-sandbox/wasm-playground.git
$ cd wasm-playground
$ yarn install

# dev server
$ yarn dev

# bundle static
$ yarn build # => dist
```

## webpack プロジェクトのセットアップ

webpack + typescript のセットアップ

※ webpack 詳しくない人向け。フロントエンドの人にとってはいつものやつなので、飛ばして大丈夫

```
$ npm i -g yarn
$ mkdir wasm-playground
$ cd wasm-playground
$ yarn init -y
$ yarn add -D webpack webpack-cli webpack-dev-server typescript ts-loader html-webpack-plugin
```

yarn 使わない人は `npm install -D/npx` で読み替えてください。

`tsconfig.json` を編集

```json
{
  "compilerOptions": {
    "target": "es2018",
    "module": "esNext",
    "strict": true,
    "strictNullChecks": true,
    "moduleResolution": "node",
    "noEmit": true,
    "esModuleInterop": true
  }
}
```

`src/index.ts`

```ts
console.log("hello");
```

`src/index.html`

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>wasm-playground</title>
  </head>
  <body>
    <h1>Wasm Playground</h1>
  </body>
</html>
```

これはテンプレートで、ビルド時は `html-webpack-plugin` によって body 末尾に `<script src="...">` が自動的に挿入される。

`webpack.config.js`

```js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".jsx"]
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: "ts-loader",
        options: {
          transpileOnly: true
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, "src/index.html")
    })
  ]
};
```

ここまでの構成

```
src/
  index.html
  index.ts
node_modules/...
package.json
tsconfig.json
webpack.config.js
```

`$ yarn webpack-dev-server` でサーバーを実行

`http://localhost:8080` を開く。 `Cmd(Ctrl)+Shift+I` で DevTools を開いて `console.log("hello")` が実行されていれば OK。

静的吐き出ししたい場合は `yarn webpack --mode production` で `dist` ディレクトリに生成される。

## crate ディレクトリに rust プロジェクトを生成

※ この節は Rust の人には馴染み深いが、フロントエンドには馴染みがないもの

こういうディレクトリを想定している

```
src/index.ts
crate/
  src/
    lib.rs
  Cargo.toml
```

というわけで crate ディレクトリを `cargo new` する。

```bash
$ cargo new crate --lib
```

ディレクトリ的な主従関係はどっちでもいいが、とりあえず混ぜないほうが幸せだと思う。

`crate/Cargo.toml` に `wasm-bindgen` を追加

```toml
[package]
name = "crate"
version = "0.1.0"
edition = "2018"

[dependencies]
wasm-bindgen = "^0.2"

[lib]
crate-type = ["cdylib"]
```

`crate/src/lib.rs` に wasm-bindgen でバインディングされたコードを書く

```rs
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greeting() -> String {
    "hello".to_string()
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn it_works() {
        assert_eq!(greeting(), "hello".to_string());
    }
}
```

build して テストする

```bash
$ cargo build
$ cargo test
```

とりあえずテストが通るところまで確認

## wasm-pack + wasm-pack-plugin で wasm ローディング

wasm-pack をインストールしておく

```
$ curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
$ wasm-pack -h # 確認
$ wasm-pack build # crate ディレクトリ
```

これをすると、`crate/pkg` に次のようなファイルが生成される。

```
crate/pkg/
  crate.d.ts
  crate.js
  crate.d.ts
  crate_bg.wasm
  crate_bg.d.ts
  package.json
```

一つの npm モジュールのようになっている。

`*_bg.wasm` が wasm のバイナリで、`crate.js` がそれをラップしたもの。`crate.d.ts` はそれに対する typescript 型定義ファイル。今回はこういう感じのものが生成されているはず。

```ts
/* tslint:disable */
/**
 * @returns {string}
 */
export function greeting(): string;
```

これらを webpack から扱うためのプラグインを追加

```
$ yarn add @wasm-tool/wasm-pack-plugin -D
```

`webpack.config.js` を次のように変更。差分としては、`resolve.extensions` (省略可能拡張子) に `.wasm` を追加して(wasm-pack がそれを要求するコードを生成している)、plugins に `WasmPackPlugin` を追加して、`crate` ディレクトリにビルド結果を生成するようにする。

```js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");

module.exports = {
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".jsx", ".wasm"]
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: "ts-loader",
        options: {
          transpileOnly: true
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, "src/index.html")
    }),
    new WasmPackPlugin({
      crateDirectory: path.join(__dirname, "crate")
    })
  ]
};
```

準備が整ったので、 `src/index.ts` からこの `crate/pkg` を呼び出す。(`create/pkg/package.json` の main 指定によって、`crate/pkg/crate.js` がエントリポイントになっている)

```ts
import("../crate/pkg").then(mod => {
  console.log(mod.greeting());
});
```

静的 import ではなく dynamic import を使っている。これは wasm をロードする基点は非同期でないといけない、という制約があるため。

現在のディレクトリ構成

```
src/
  index.html
  index.ts
crate/
  pkg/...
  src/
    lib.rs
  Cargo.toml
  Cargo.lock
node_modules/...
package.json
tsconfig.json
webpack.config.js
```

この状態で `yarn webpack-dev-server` でサーバーを立てると、このログが読み込まれているはず。

wasm-bindgen を使わない場合、TextEncoder を使って自分で文字列組み立てしないと行けないので、だいぶだるい。wasm-bindgen 様様

## .gitignore

```
dist
node_modules
pkg
target
.cache
.DS_Store
*.log
```

これらはコミットしなくていい

## 次にやりたいこと

- js-sys/web-sys
- https://github.com/fitzgen/dodrio

## 感想

やることは多いけど、 wasm-pack(-plugin) のおかげで昔やったときよりかなりストレスフリーになっていてよかった

## LICENSE

MIT