初投稿、勉強用メモ
なぜこれを書くのか
Rust、WebAssemblyに興味があった
Webブラウザ上で高速に動作するアプリを作る方法が知りたい
学んだ内容を見返すためのメモ
進め方
- Rust 🦀 and WebAssembly 🕸を適宜端折ってまとめていきます。
- 細かな情報はいらん。具体的な作業を教えてくれという方は4節へどうぞ。
- 誤っている箇所などあればコメントを頂けると幸いです。
- 当方Rust, WebAssembly, javascriptすべて初心者です。
#何を学べるか? (Rust and WebAssemblyより抜粋)
- RustからWebAssemblyへコンパイルする際に使うツールのセットアップ
- Rust, WebAssembly, javascript, HTML, CSS等、多言語で構成されるプログラムを作る際におけるワークフロー
- Rust, WebAssembly, javascriptの強みを生かすためのAPIを設計する方法
- RustでコンパイルされたWebAssemblyをDebugする方法
- RustとWebAssemblyで書かれたプログラムを高速化する方法
- RustとWebAssemblyで作成された.wasmファイルのサイズを小さくする方法
次節よりtutorialのまとめです。
1.はじめに
誰のための本?
- Web上で動作する高速かつ信頼性の高いプログラムを作るためにRustをWebAssemblyへコンパイルしたい人
- Rustをまあまあ知っていてかつjs、html、cssに馴染みがある人。(私はどちらにも当てはまりませんがとりあえず写経しながら進めます)
この本の読み方
- まずはRustとWebAssemblyを一緒に使う動機とその背景に関するページを読みましょう
- tutorial章は最初から最後まで取り組むように書かれています
- 書いて、コンパイルして、走らせるまでを自分でちゃんとやりましょう
2.RustでWebAssemblyを使う理由
使いやすい低レベル制御
- javascript Webアプリケーションでは信頼できるパフォーマンスの維持に苦労している。少しのコード修正でJITのパフォーマンスが著しく低下する場合がある。そこでRustを用いて信頼性の高い低レベル制御を実現しよう
小さな".wasm"サイズ
- .wasmはネットからダウンロードするのでコードサイズは大切。Rustならruntimeがないのでサイズが小さくなります
すべてをRustに書き換える必要はない
- 今あるコードをすべて捨ててRustに書き換える必要はなく、パフォーマンスにクリティカルなところだけ書き換えればOK。ある程度高速化して満足すればそこでやめてもOK。
ほかとも遊ぶ (うまく翻訳できず)
- jsの他のツールと共存できます
3.WebAssemblyとは何か?
- 多くの仕様を持つハードウェアモデルと実行形式のフォーマットである
- 可搬性が高くコンパクト
プログラミング言語としてのWebAssemblyは2つの形式で書かれる。1つ目は.watテキスト形式、2つ目は.wasmバイナリ形式である。.watは読める形式、wasmはバイナリ形式。wat2wasmデモでwat形式とwasm形式を比較可能。
###リニアメモリ
- 本質的にフラットなリニアメモリモデルを持つ。メモリモデルはページサイズの倍数(64KB)で拡張できる。
WebAssemblyはWeb専用か?
- wasmはホスト環境を指定しないので将来的にポータブルな実行形式となる可能性はある。
- 現状はJSとの関わりが深い。
#4.チュートリアル
RustとWebAssemblyでコンウェイのゲームオブライフを実装するチュートリアル
###チュートリアルの対象者
- Rustとjavascriptの基本操作を知っている人
- Rust, javascript, HTMLを一緒に使う方法を知りたい人
###何を学べるか?
- RustからWebAssemblyへコンパイルする際に使うツールのセットアップ
- Rust, WebAssembly, javascript, HTML, CSS等、多言語で構成されるプログラムを作る際におけるワークフロー
- Rust, WebAssembly, javascriptの強みを生かすためのAPIを設計する方法
- RustでコンパイルされたWebAssemblyをDebugする方法
- RustとWebAssemblyで書かれたプログラムを高速化する方法
- RustとWebAssemblyで作成された.wasmファイルのサイズを小さくする方法
4.1 セットアップ
RustプログラムをWebAssemblyにコンパイルするためのツールチェーンの導入方法が解説されている。
###Rustツールチェーン
rustup, rustc, cargoを手順に従ってインストールする。Rust1.30以降が必要。
curl https://sh.rustup.rs -sSf | sh
source $HOME/.cargo/env
###wasm-pack
Rustで生成されたWebAssemblyをビルド、テスト公開するためのコンビニみたいなもの(one-stop shop)です。ここからインストールする。
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
###cargo-generate
新規Rustプロジェクトを作成する際に既存のgitリポジトリを活用するためのツール。
cargo install cargo-generate
###npm
javascriptのパッケージマネージャ。javascriptバンドラと開発サーバをインストール&実行できる。
npmはここからダウンロードできる。
installされているかどうかは下記コマンドを実行してバージョンが表示されればOK。
npm -v
6.13.4
4.2 Hello, World!
RustとWebAssemblyのプログラムをビルドして実行する方法を説明し、"Hello, World!"というアラートを出すプログラムを作成する。
開始前に[セットアップ](## 4.1 セットアップ)が完了しているか確認すること。
###テンプレプロジェクトをクローンする
プロジェクトを作成したいフォルダ内で次のコマンドを実行する。
実行するとプロジェクト名を聞かれるので"wasm-game-of-life"と入力しenterを押す。
cargo generate --git https://github.com/rustwasm/wasm-pack-template
wasm-game-of-life
###クローンしてきたプロジェクトの中身を確認する
wasm-game-of-lifeフォルダの中へ入る。
cd wasm-game-of-life
中身を確認すると以下のディレクトリ構造が確認できる。内容物は以下の通り。
wasm-game-of-life/
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
└── src
├── lib.rs
└── utils.rs
####wasm-game-of-life/Cargo.toml
wasm-game-of-lifeプロジェクトで使うパッケージ情報が記述されている。
cargo(Rustのパッケージマネージャ兼ビルドツール)が読むプロジェクトの設定ファイルのみたいなもの。
ぱっと見たでは依存関係、バージョン、最適化レベルの情報が書かれてるっぽい。
[package]
name = "wasm-game-of-life"
version = "0.1.0"
authors = ["hoge <hoge@gmail.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.1", optional = true }
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. It is slower than the default
# allocator, however.
#
# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now.
wee_alloc = { version = "0.4.2", optional = true }
[dev-dependencies]
wasm-bindgen-test = "0.2"
[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"
####wasm-game-of-life/src/lib.rs
src/lib.rsはWebAssemblyへコンパイルするRustクレート(パッケージ/ライブラリのこと)の元となるファイル。
#[wasm_bindgen]はRustとjavascript間でやり取りがある目印(修飾子)として用いられる。
1つ目の#[wasm_bindgen]で修飾している箇所ではRustプログラムからjacvascriptプログラムの公開関数window.alertを使うためにextern宣言をしている。
2つ目の#[wasm_bindgen]で修飾している箇所ではgreet関数定義している。#[wasm_bindgen]で修飾するとjavascript側でgreet関数を使えるようになる。
mod utils;
use wasm_bindgen::prelude::*;
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello, wasm-game-of-life!");
}
####wasm-game-of-life/src/utils.rs
src/utils.rsはWebAssemblyにコンパイルされたRustコードをいじるときのユーティリティを提供するファイル。wasmのデバッグをする際に用いる。
###プロジェクトのビルド
これからwasm-packを用いてビルドする。手順は下記。
- Rust1.30以上であることを確認する。
- cargoを用いてRustコードをWebAssemblyのバイナリファイル(.wasmファイル)へコンパイルする。
- wasm-bindgenを用いて.wasmファイルのjavascript向けAPIを生成する。
まずはプロジェクトディレクトリ(wasm-game-of-life内部)で下記ビルドコマンドを実行する。
wasm-pack build
こんな感じのメッセージが出てきて無事ビルドされた。
[INFO]: 🎯 Checking for the Wasm target...
[INFO]: 🌀 Compiling to Wasm...
Compiling proc-macro2 v1.0.9
Compiling unicode-xid v0.2.0
Compiling log v0.4.8
Compiling syn v1.0.17
Compiling wasm-bindgen-shared v0.2.59
Compiling cfg-if v0.1.10
Compiling lazy_static v1.4.0
Compiling bumpalo v3.2.1
Compiling wasm-bindgen v0.2.59
Compiling quote v1.0.3
Compiling wasm-bindgen-backend v0.2.59
Compiling wasm-bindgen-macro-support v0.2.59
Compiling wasm-bindgen-macro v0.2.59
Compiling console_error_panic_hook v0.1.6
Compiling wasm-game-of-life v0.1.0 (/Users/hoge/wasm-game-of-life)
warning: function is never used: `set_panic_hook`
--> src/utils.rs:1:8
|
1 | pub fn set_panic_hook() {
| ^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
Finished release [optimized] target(s) in 22.36s
[INFO]: ⬇️ Installing wasm-bindgen...
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended
[INFO]: ✨ Done in 22.51s
[INFO]: 📦 Your wasm pkg is ready to publish at /Users/hoge/wasm-game-of-life/pkg.
pkgファイルの中身を確認すると下記構造となっていた。READMEはメインプロジェクトからコピーされていた。ほかは新規のファイル群。
####wasm-game-of-life/pkg/wasm_game_of_life_bg.wasm
.wasmファイルは先程紹介したRustファイルからコンパイルによって生成されたWebAssemblyバイナリファイルである。これには、先程実装した「greet()」関数の機能がある。
####wasm-game-of-life/pkg/wasm_game_of_life.js
このjavascriptファイルはwasm-bindgenから生成されたものである。WebAssemblyへコンパイルしたRustコードをjavascriptから呼び出すラッパー関数が含まれている。
import * as wasm from './wasm_game_of_life_bg';
// ...
export function greet() {
return wasm.greet();
}
####wasm-game-of-life/pkg/wasm_game_of_life.d.ts
この.d.tsファイルにはjavascriptとつなぐためのTypescriptの型宣言が含まれている。Typescriptを使う場合はWebAssembly関数への呼び出しで型チェックすることができ、IDEでは自動補完機能を提供できるようになる。Typescriptを使わない場合、このファイルは無視しても構わない。
export function greet(): void;
####wasm-game-of-life/pkg/package.json
package.jsonファイルには、生成されたjavascriptファイルとWebAssemblyパッケージのメタ情報が記述されている。この情報はnpmとjavascriptバンドラーが依存関係やヴァージョン情報を決めるために利用される。これらは、javascripttoolを使って統合する作業とnpmへpackageを公開する際に助けになる。
{
"name": "wasm-game-of-life",
"collaborators": [
"Your Name <your.email@example.com>"
],
"description": null,
"version": "0.1.0",
"license": null,
"repository": null,
"files": [
"wasm_game_of_life_bg.wasm",
"wasm_game_of_life.d.ts"
],
"main": "wasm_game_of_life.js",
"types": "wasm_game_of_life.d.ts"
}
###Webページへの導入
私達のwasm-game-of-lifeを取得しWeb pageで使うために、create-wasm-appというjavascript project templateを用いる。
このコマンドをwasm-game-of-lifeディレクトリ内で実行する。
npm init wasm-app www
下記ファイル群が生成された。
wasm-game-of-life/www/
├── bootstrap.js
├── index.html
├── index.js
├── LICENSE-APACHE
├── LICENSE-MIT
├── package.json
├── README.md
└── webpack.config.js
もう一度、これらの中身を見ていきましょう。
####wasm-game-of-life/www/package.json
このpackage.jsonファイルは、npmへと公開されているwasm-pack-templateパッケージの初期バージョンであるhello-wasm-packと同様、webpack,wabpack-dev-serverとともに事前構成される。
####wasm-game-of-life/www/webpack.config.js
このファイルはwabpackとローカルサーバを構成する。事前に設定されているのでwebpackとローカル開発サーバを機能させるための設定は必要ない。
####wasm-game-of-life/www/index.html
これは、WebpageのためのルートHTMLである。bootstrap.jsをロードする以外の機能は特にない。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello wasm-pack!</title>
</head>
<body>
<script src="./bootstrap.js"></script>
</body>
</html>
####wasm-game-of-life/www/index.js
このindex.jsはWebpageのエントリポイントである。WebAssemblyへコンパイルされたwasm-pack-templateとjavascriptグルーを含むhello-wasm-packのnpmパッケージをimportし、hello-wasm-packのgreet関数を呼び出している。
import * as wasm from "hello-wasm-pack";
wasm.greet();
###依存関係をインストールする
まずwasm-game-of-life/wwwディレクトリ内で
npm install
を実行し、ローカル開発サーバと依存関係がインストールされているかどうかを確認する。
###ローカルにあるwasm-game-of-lifeをwww内で使う
私達はnpmのhello-wasm-packを使うのではなく、ローカルのwasm-game-of-lifeを使いたい。これにより、Game of Life programを少しずつ開発できるようになる。
まず、wasm-game-of-life/www/package.jsonを開き、"devDependencies"の上に"dependencies"のフィールドを追加する。下記の通り
{
// ...
"dependencies": { // Add this three lines block!
"wasm-game-of-life": "file:../pkg"
},
"devDependencies": {
//...
}
}
次は、hello-wasm-packの代わりにwasm-game-of-lifeをimportするためにwasm-game-of-life/www/index.jsを修正する。
import * as wasm from "wasm-game-of-life";
wasm.greet();
新しい依存関係を宣言したので再度
npm install
を実行する。
###ローカルで実行する
ターミナルを開きwasm-game-of-life/wwwへ移動し、
npm run start
を実行しhttp://localhost:8080/ へ移動すると、
Hello, wasm-game-of-life
というメッセージが表示される。
####演習
wasm-game-of-life/src/lib.rsのgreet関数を、name:&strパラメータを取得しwasm-game-of-life/www/index.jsのgreet関数から名前を取得できるように修正してみましょう。
#[wasm_bindgen]
pub fn greet() {
alert("Hello, wasm-game-of-life!");
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
wasm.greet();
wasm.greet("Your name");
4.3 ライフゲームのルール
Wikipediaの解説はこちら。
ゲームオブライフの宇宙は正方形のマス目(以降ではセルと呼ぶ)が無限に続く2次元のマス目である。各セルは生きているか死んでいるかの2状態を取り、すべてのマス目は隣接する8つのセルと相互作用する。各タイムステップで4種の状態変化が発生する。
####Rule 1.過疎
生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
####Rule 2.生存
生きているセルに隣接する生きたセルが2つか3つならば、次のステップでも生存する。
####Rule 3.過密
生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。
####Rule 4.誕生
死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
・次の初期宇宙を考える。生きているセルは■、死んでいるセルは□で表す。
□□□□□
□□■□□
□□■□□
□□■□□
□□□□□
この状態に上記の4つのルールを適用し次の世代を計算する。
まず、最左上のセルを見ると死んでいることがわかる。死んでいるセルに対してはRule 4が適用されるが、最左上セルの近接セル周辺には生きているセルが存在しないため、次のステップにおいても死んだままとなる。1行目にある他のセルも同様。
2行3列目の生きているセルを考えると、Rule 1が適用され次の世代では死亡する。
3行3列目の生きているセル周辺には最近接セルのうち2つが生きているセルなので次のステップでも生きている。
興味深い場合として、3行2列と3行4列のセルがある。これらのセルの最近接セルには3つの生きているセルがいるためRule 4が適用され、次のステップでは生きているセルとなる。
結果として、次のステップにおける宇宙の状態は
□□□□□
□□□□□
□■■■□
□□□□□
□□□□□
となります。
これらのシンプルで確定的なルールから独特でエキサイティングな動作が現れます!
これとか、これとか、これ
####演習
・サンプル宇宙の次のステップを計算してみましょう。
答え:
□□□□□
□□■□□
□□■□□
□□■□□
□□□□□
となる。このサンプル宇宙は2ステップ置きに初期状態に戻る。
・安定した初期宇宙を見つけてみましょう
答え:無数にある。自明なものは生きているセルが存在しない宇宙。2行2列ももちろん安定した宇宙である。
4.4 ライフゲームの実装
4.5 ライフゲームのテスト
4.6 ライフゲームのデバッグ
4.7 インタラクティブ機能の追加
4.8 パフォーマンスの改善
4.9 .wasmのサイズを小さくする
4.10 npmへ公開
まとめ
tutorialが終わったら書きます。