yikegaya’s blog

仕事関連(Webエンジニア)と資産運用について書いてます

Rust + WebAssemblyのDocker開発環境構築メモ

Mozillaチュートリアルを参考にRustとWebAssemblyの開発環境を構築したんですが割とハマりました。作業内容書き残してみます。

以下のページを参考にしています。Rustで実装したWasmをコンパイルしてnpmとwebpackで配信するチュートリアルです。

developer.mozilla.org

上記のサイトではプロジェクト名はhello-wasmとしていますが今回はこの後ブラウザで作るテトリスを実装する予定なのでプロジェクト名は「rust-tetris」としています

Dockerfile

参考にしたページではDockerは使わずホストに環境構築する内容だったんですが自分のPCにはいろいろとプロジェクトが載っていてホストでバージョン管理などするのが面倒だったのでDockerで構築しました。

できたDockerfile

# 言語はrustとnode.jsが必要なんですが今回ベースイメージはrustにしてみました。
# またrustはまだ言語そのものやライブラリなど周辺環境の開発が盛んな印象があるのでlastestで指定せずにVer1.77を指定しています。
FROM rust:1.77

WORKDIR /app
COPY . .

# nodeとnpmのinstall
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
RUN apt-get install -y nodejs
RUN npm install -g npm@10.5.0

# WebAssembly開発に必要なwasm-packのinstall
RUN cargo install wasm-pack
# なくてもいいですがリンタのinstall
RUN rustup component add rustfmt

# webpackのbuild
WORKDIR /app/site
RUN npx webpack

docker-composeも作りました。 コンテナ1つ作るだけなのでdockerコマンドだけでも開発できそうですがvolume作ったりport指定するのにオプションが長くなっちゃうので作っといた方が開発しやすいかと思います。

version: '3.8'

services:
  tetris:
    build:
      context: .
    working_dir: /app/site
    command: ["npm", "run", "serve"]
    # 3000とか8080は別のプロジェクトで使ってたので適当に空いてるポートを割り当て
    ports:
      - 3456:8080
    volumes:
      - .:/app      
      - cargo-cache:/usr/local/cargo/registry
      - target-cache:/app/target
      - cargo-bin:/usr/local/cargo/bin

volumes:
  cargo-cache:
  target-cache:
  cargo-bin:

WebAssembly環境の構築

フォルダ構成

上記のDocker環境にWebAssemlyとWebpackの環境を作っていくわけですがフォルダ構成は次のようになりました。

 /app
  │
  ├── Cargo.lock
  ├── Cargo.toml
  ├── Dockerfile
  ├── README.md
  ├── docker-compose.yml
  ├── pkg // WebAssemlyのbuild先
  │   
  ├── site // npm、webpack関連
  │   ├── dist // webpackのbuild結果出力先
  │   ├── index.js
  │   ├── index.html
  │   ├── node_modules
  │   ├── package-lock.json
  │   ├── package.json
  │   └── webpack.config.js
  │   
  ├── src
  │   └── lib.rs // WebAssemlyの実装
  │   
  └── target

RustでのWebAssemblyコード実装

チュートリアルのコードそのままです。ページを開いた時にalertを表示します。

wasm-bindgenというツールを使ってjavascriptとRustのコードを繋いでいます。

use wasm_bindgen::prelude::*;

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

#[wasm_bindgen]
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

プロジェクトルートで以下のコマンドを実行するとnpmで使えるようコンパイルできます。 --target bundlerオプションでwebpackのようなbundlerで実行できる形式への変換できます。

wasm-pack build --target bundler

実行するとpkgフォルダ以下にjavascriptとtypescriptのコードとpackage.json、READMEが出力されます。

root@1d9867bd702a:/app# ls -l pkg
total 44
-rw-r--r-- 1 root root   520 Mar 28 06:08 README.md
-rw-r--r-- 1 root root   516 Mar 28 06:08 package.json
-rw-r--r-- 1 root root   115 Mar 28 06:08 rust_tetris.d.ts
-rw-r--r-- 1 root root   160 Mar 28 06:08 rust_tetris.js
-rw-r--r-- 1 root root  2553 Mar 28 06:08 rust_tetris_bg.js
-rw-r--r-- 1 root root 16843 Mar 28 06:08 rust_tetris_bg.wasm
-rw-r--r-- 1 root root   287 Mar 28 06:08 rust_tetris_bg.wasm.d.ts

build後にnpm linkコマンドを実行してwasmのコードをグローバルなnpmリポジトリにリンクさせます。 ここのコマンド実行もdockerコンテナ作成時に組み込んだ方がいいかもしれませんが一旦コンテナのシェルにattachして実行してます。

docker exec -it tetris_tetris_1 bash
cd /app/pkg
npm link

Webpack実行環境構築

/app/siteにwebpackとnpmでhtml、jsを配布する環境を作ります。

まず先ほどグローバルnpmリポジトリにリンクしたパッケージを/app/siteフォルダにインストールします。

cd site
npm link rust-tetris

package.json

次にpackage.jsonを作成します。今回は以下のように書きました

{
  "scripts": {
    "serve": "webpack-dev-server"
  },
  "dependencies": {
    "rust-tetris": "^0.1.0"
  },
  "devDependencies": {
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^5.00"
  }
}

チュートリアルのpackage.jsonは以下のようになっており現時点でのwebpackのバージョンが古いのですが今回は5系のバージョンをインストールしてみました。

またwebpack-cliとwebpack-dev-serverを指定すれば依存関係でwebpackも入るんじゃないかと思って端折ってみてますがそれで問題なさそうです。

{
  "scripts": {
    "serve": "webpack-dev-server"
  },
  "dependencies": {
    "hello-wasm": "^0.1.0"
  },
  "devDependencies": {
    "webpack": "^4.25.1",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.10"
  }
}

package.jsonを作成した状態でnpm installを実行するとwebpack関連のライブラリが/app/site/node_modules以下にインストールされます。

webpack.config.json

次にwebpack.config.jsを記述します。

const path = require("path");
module.exports = {
  entry: "./index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "index.js",
  },
  mode: "development",
};

チュートリアルでは上記のようになっていますが、webpack5ではdevserverのstaticとasyncWebAssemblyの指定がないとエラーが発生するようです。

設定を追加したwebpack5向けのconfigは次の通りです。

const path = require('path');

module.exports = {
  entry: "./index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js", // index.jsよりもbundle結果のjsであることがわかりやすいので命名変えてます
  },
  devServer: {
    port: 8080,
    // 追加
    static: {
      directory: "./",
    },
  },
  // 追加
  experiments: {
    asyncWebAssembly: true,
  },
};

index.htmlとindex.js

あとはwebpackから配布されるindex.htmlとhtmlから呼び出されるwasmを実行するjsを用意して完了です。

index.html

<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>hello-wasm example</title>
  </head>
  <body>
    <h1>hello-wasm example</h1>
    <script src="./dist/bundle.js"></script>
  </body>
</html>

index.js

import("./node_modules/hello-wasm/hello_wasm.js").then((js) => {
  js.greet("WebAssembly with npm");
});