WebAssembly を使うまでの環境構築

依存ツール

依存は以下のコマンドでインストールします。

# wasm-pack のインストールがまだの場合
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

# npm
yarn add -D \
  @wasm-tool/wasm-pack-plugin \
  html-webpack-plugin \
  ts-loader \
  typescript \
  webpack \
  webpack-cli \
  webpack-dev-server \

1. wasm-pack

wasm-packは、 Rust コードを WebAssembly へコンパイルし、JavaScript で扱うためのパッケージ化を行ってくれます。

吐き出される JavaScript は完全ではないですが TypeScript ですぐ使える形になっているので、ここでも TypeScript で見ていこうと思います。

2. Webpack

Webpack は、複数の JavaScript ファイルを実質 1 つの JavaScript ファイルにまとめます。

3. webpack-dev-server

webpack-dev-server は、 Webpack によるビルド後の結果でサーバーを建ててくれます。ビルドは行いますが、インメモリなファイルとして扱われるので、実際にあるディレクトリ内のファイルにビルド結果を出力したりはしません。
また、関連ファイルに変更が入った際に自動リロードで助けてくれます。

4. html-webpack-plugin

html-webpack-plugin は、 Webpack によるビルドで生成されるファイルを読み込んだ形の HTML ファイルを作成してくれます。

設定オプションのfilenameindex.html(デフォルト)の時、これはwebpack-dev-serverによるサーバーをブラウザで開いた時に表示される最初の HTML になります。

5. wasm-pack-plugin

wasm-pack-plugin は、 Webpack から wasm-pack を実行する為のプラグインです。これにより webpack でビルドする際に、 wasm-pack と Webpack 両方のビルドを走らせることができます。

Webpack 設定

以下は設定例です。

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin')

module.exports = (_, argv) => {
  return {
    mode: argv.mode || 'development',
    devtool:
      argv.mode === 'production' ? 'source-map' : 'eval-cheap-source-map',
    target: 'web',
    entry: {
      main: path.join(__dirname, 'src/main.ts')
    },
    output: {
      path: path.join(__dirname, 'out'),
      filename: '[name].js'
    },
    resolve: {
      extensions: ['.ts', '.tsx', '.js', '.jsx', '.wasm']
    },
    devServer: {
      port: 33857
    },
    module: {
      rules: [
        {
          test: /\.tsx?$/,
          loader: 'ts-loader',
          options: {
            transpileOnly: true
          }
        }
      ]
    },
    plugins: [
      new HtmlWebpackPlugin(),
      new WasmPackPlugin({
        crateDirectory: path.join(__dirname, '.')
      })
    ]
  }
}

注意したい点は 2 つです。

  1. resolve.extensions.wasmを含める
  2. WesmPackPlugincreateDirectoryには、 Cargo プロジェクトのディレクトリを指定する(今回は.なので Webpack 設定ファイルが置かれているディレクトリに Cargo.toml も置かれている)

この設定でビルドを行うとWesmPackPluginは、 Webpack 設定ファイルと同じディレクトリにpkg/というディレクトリを作ります。これは WebAssemly な JavaScript パッケージなっている為、後はこれをメインとなる JavaScript プロジェクト側から読み込むと、 Rust 側でエクスポートした関数などを実行できます。

Rust

Cargo.tomlは以下のようにします。

[package]
name = "get-started-wasm"
version = "0.0.0"
authors = ["nju33 <nju33.ki@gmail.com>"]
edition = "2018"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2.60"

[profile.release]
opt-level = "s"

wasm-bindgenというパッケージで Rust と JavaScript を紐付けます。

例として、 JavaScript から関数を実行するとコンソールにHello wasmと出力されるように実装してみます。
まずは、ブラウザのconsole.logを Rust で実行できるようにする為にsrc/console.rsファイルを作ります。

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
  #[wasm_bindgen(js_namespace = console)]
  pub fn log(s: &str);
}

このextern "C"ブロックの中に JavaScript 側の関数に関する定義を書くことで、その関数を Rust から使えるようになります。これには#[wasm_bindgen]属性を付けてあげる必要があります。
この時console.logのような.を挟むものはjs_namespace属性値いを指定してあげる必要があります。(foo.bar.bazのような深いネストの関数などは、自分でその関数をwindow以下に設定し、それを定義付けすることで使えました)これでlog("foo")などでコンソールにログが出せるようになりました。

次に JavaScript から呼ぶ用の関数を作りますが、そのような関数にも#[wasm_bindgen]属性は必要です。またその関数はResultを返す必要があります。

今度はsrc/lib.rsファイルを作ります。

mod console;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn hello_wasm() -> Result<(), JsValue> {
  console::log("Hello Wasm");

  Ok(())
}

mod consoleで先程のファイルの中身が使えるようになります。例えばlogconsole::logという形と、ほぼ JavaScript と同じ様に使えます。

そしてhelloo_wasmという関数を定義しました。これによりwasm-packで生成された JavaScript パッケージのモジュールからhello_wasmという関数をインポートできるようになります。

TypeScript

tsconfig.jsonを以下のようにします。module: esnextが必須です。

{
  "include": ["src"],
  "compilerOptions": {
    "target": "es6",
    "module": "esnext",
    "strict": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "isolatedModules": true
  }
}

そしてsrc/main.tsファイルを作り、以下のようにします。

// eslint-disable-next-line @typescript-eslint/no-floating-promises
;(async () => {
  const module = await import('../pkg')
  module.hello_wasm()
})()

export {}

重要な点はwasmなパッケージは必ず Webpack のimportを使って読み込む事です。.wasmは今の所この方法による読み込みをしなければエラーになるようです。

後はyarn webpack-dev-serverなどからlocalhostを開き、コンソールにHello wasmと表示されれば WebAssembly を使うまでの環境構築は完了です。

サンプル

ここまでの動作確認ができるものは nju33-com/get-started-wasm に置いてあります。