Mozillaのチュートリアルを参考にRustとWebAssemblyの開発環境を構築したんですが割とハマりました。作業内容書き残してみます。
以下のページを参考にしています。Rustで実装したWasmをコンパイルしてnpmとwebpackで配信するチュートリアルです。
上記のサイトではプロジェクト名は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"); });