LoginSignup
72
55

More than 5 years have passed since last update.

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

Posted at

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

想定読者は フロントエンドプログラマ 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 を編集

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

src/index.ts

console.log("hello");

src/index.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

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 productiondist ディレクトリに生成される。

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

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

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

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

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

$ cargo new crate --lib

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

crate/Cargo.tomlwasm-bindgen を追加

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

[dependencies]
wasm-bindgen = "^0.2"

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

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

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 して テストする

$ 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 型定義ファイル。今回はこういう感じのものが生成されているはず。

/* 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 ディレクトリにビルド結果を生成するようにする。

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 がエントリポイントになっている)

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

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

次にやりたいこと

感想

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

LICENSE

MIT

72
55
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
72
55