虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

WebAssemblyで遊んでみる〜Rust+wasm-pack環境構築編〜

こんにちは。とらのあなラボ所属のY.Fです。

最近情報収集していると俄にWebAssemblyの盛り上がりを感じます。
私はフロントエンドベースにして、Web周りを何でもやるエンジニアとして働いているのですが、
フロントエンドやるにあたってWebAssemblyについていけないとまずいなと感じたのでRustで入門してみます。
ちなみにRustは少し前にプログラミング言語Rustを一通りやってみた程度にしか知りません。

そもそもWebAssemblyとは

MDNの解説に詳しく載っていますが、Webブラウザ上で動作するバイナリファイルです。
調べた限りではWebAssemblyをビルドできる言語には以下のような物があるようです。

今回はこの中でRustのwasm-packというツールでWebAssemblyの環境構築をしてみます。
また、ちょうどわかりやすいチュートリアルもあるのでやってみました。

rustwasm.github.io

Rustとツールのインストール

基本的にはこのチュートリアルのsetupの章に従えば完了です。

$ curl https://sh.rustup.rs -sSf | sh
$ curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
$ cargo install cargo-generate
$ npm install npm@latest -g

チュートリアルやってできるもの

何をするかは上記チュートリアルを見てもらえばいいので割愛します。
出来上がるのは以下のようなライフゲームです。

f:id:toranoana-lab:20190726121639g:plain

webpackと連携する

上記のチュートリアルをやると以下のようなフォルダ構成で最終的に出来上がると思います

.
├── Cargo.lock
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
├── pkg
│   ├── README.md
│   ├── package.json
│   ├── wasm_game_of_life.d.ts
│   ├── wasm_game_of_life.js
│   ├── wasm_game_of_life_bg.d.ts
│   └── wasm_game_of_life_bg.wasm
├── src
│   ├── lib.rs
│   └── utils.rs
├── target
├── tests
│   └── web.rs
└── www
    ├── LICENSE-APACHE
    ├── LICENSE-MIT
    ├── README.md
    ├── bootstrap.js
    ├── index.html
    ├── index.js
    ├── package-lock.json
    ├── package.json
    └── webpack.config.js

これはwasm-packによってWebAssemblyでnpmモジュールを作るような構成になっているようです。
そのため、www以下に画面表示用のhtmlやjsファイルが置かれるようになっています。
一方で、フロントエンドの一部としてWebAssemblyを開発したいと思ったときに、package.jsonがあるディレクトリをメインに置いて、Rustのプロジェクトをサブディレクトリに置きたいと感じる方は多いと思います。

幸いなことにwebpackのプラグインとして wasm-pack-plugin が用意されています。これを使うことでwebpackとwasm-packを連携して、wasmを一括管理することができるようになります。
これでwebpack + wasm-packの環境に上記環境を移行していきます。 (参考 wasm-bindgen + wasm-pack + webpack で フロントエンド - Qiita)

webpack化することで以下のような恩恵が受けられます。

  • フロントエンドに慣れている人からするとフロントエンドのソースとRustのソースを同じような形で扱える
  • Rust側のソースを変更した場合webpack-dev-serverによって自動ビルドなどができる

webpackの環境準備

よくある一般的なwebpack環境のインストール手順です。

$ mkdir wasm-game-of-life-wabpack
$ cd wasm-game-of-life-wabpack
$ npm init
$ npm install --save-dev webpack webpack-cli webpack-dev-server typescript ts-loader html-webpack-plugin @wasm-tool/wasm-pack-plugin
$ mkdir src
$ touch src/index.ts
$ touch src/index.html
$ touch webpack.config.js
$ touch tsconfig.json

webpack.config.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')
    })
  ]
};

ほかtsconfigなどの中身は代わり映えしないので割愛です。

Rustのプロジェクトを設置

以下コマンドでRustプロジェクトを作ります。

$ cargo new wasm-game-of-life --lib

出来上がったフォルダ内のCargo.tomlに追記します。チュートリアルで使ったものをそのままコピペでも動くと思いますが、余計なものが入るので以下のようにしました。

[package]
name = "wasm-crate"
version = "0.1.0"
authors = ["y-fujiwara"]
edition = "2018"

[dependencies]
wasm-bindgen = "^0.2"
console_error_panic_hook = { version = "0.1.1", optional = true }
wee_alloc = { version = "0.4.2", optional = true }

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

[features]
default = ["console_error_panic_hook", "wee_alloc"]

[dev-dependencies]
wasm-bindgen-test = "0.2"

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

[dependencies.web-sys]
version = "0.3"
features = [
    "console",
]

また、srcディレクトリが出来ているので、その中にチュートリアルで作成した lib.rs 及び utils.rs をコピペします。
コピペしたらビルドのみしておきます。

$ cargo build
$ wasm-pack build

この段階で以下の様なフォルダ構成になるはずです。

.
├── package-lock.json
├── package.json
├── node_modules
├── src
│   ├── index.html
│   └── index.ts
├── tsconfig.json
├── wasm-game-of-life
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── pkg
│   │   ├── package.json
│   │   ├── wasm_crate.d.ts
│   │   ├── wasm_crate.js
│   │   ├── wasm_crate_bg.d.ts
│   │   └── wasm_crate_bg.wasm
│   ├── src
│   │   ├── lib.rs
│   │   └── utils.rs
│   └── target
└── webpack.config.js

wasm-pack-pluginを使ってwebpackとwasm-packを連携する

webpack.config.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, 'wasm-game-of-life'),
      outName: 'wasm_game_of_life',
    }),
  ],
};

wasmを読み込むファイルを作っておきます。

$ touch lifegame.ts

中身はチュートリアルで作ったindex.jsと基本は同じなのですが、package.jsonに "wasm-game-of-life": "file:./wasm-game-of-life/pkg" の記載がない場合は以下のようにwasmを読み込みます。

import {Universe, Cell} from '../wasm-game-of-life/pkg';
import {memory} from '../wasm-game-of-life/pkg/wasm_crate_bg';

// 以下略

次に、index.tsを実装します。注意点としてはwasmの読み込みは非同期である必要があるのでlifegame.tsを非同期読み込みするように書く必要があります。

import('./lifegame')
  .then(mod => mod.run())
  .catch(e => console.error('Error importing `lifegame.js`:', e));

実行

npm start して http://localhost:8080 でライフゲームが動いていることを確認できれば完了です!

動かない場合はルートディレクトリのpkgフォルダを消してみたり、wasm-game-of-life/pkgを消して再ビルドしてみたりすると良いかと思います。

まとめ

今回は環境構築をしてチュートリアルで作ったアプリをwebpackに連携するまで実施しました。
次回はcanvasを使ってもう少し高度なゲームをなどを実装してみたいと思います。 また、個人的にはAssemblyScriptに注目しているのでなにかあればまたブログ書きたいと思います。

(次の記事はこちら) toranoana-lab.hatenablog.com

P.S

虎の穴では一緒に働く仲間を絶賛募集中です! この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧下さい。 yumenosora.co.jp

また、今月7/30には会社説明会を開催いたします。ご興味のある方は是非ご応募ください! yumenosora.connpass.com