0. はじめに
この記事は Rustその2 Advent Calendar 2019 19日目 の記事です。
JavaのWORAじゃないですけど、RustのWebAssemblyは書いたものをブラウザで実行できるのでとても魅力的です!
しかしデプロイまでとなると結構苦戦します。少なくとも筆者は。そこで一連の流れを今回の記事に自分なりにまとめることにしました。よろしくお願いします。
Wasmには Tutorial - Rust and WebAssembly というライフゲームを題材にした素晴らしいチュートリアルがあるのですが、本記事はこれをかなーり簡略化したものです。英語が得意で時間に余裕がある方はこっちのチュートリアルをやったほうがいいです。
対象読者はRust、JavaScriptの基本的な事項とCargoの基本的な役割を理解しているものとします1。また本記事での使用OSは Ubuntu 18.04.1 LTS
、使用シェルは bash
です。
1. 環境構築
環境構築面倒くさいですよね...頑張りましょう。
まずは以下のコマンドを使用できるようにします。
rustup
rustc
cargo
この記事を読んでくれている方は多分すでにインストールされているとは思いますが、一応コマンドを一から書いておきます。詳しくは検索してください( インストール - The Rust Programming Language )。省略を ...
とします。
$ sudo apt update
...省略...
$ sudo apt install curl
...
$ curl https://sh.rustup.rs -sSf | sh
...
default host triple: x86_64-unknown-linux-gnu
default toolchain: stable
profile: default
modify PATH variable: yes
1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
>1
...
stable installed - rustc 1.39.0 (4560ea788 2019-11-04)
Rust is installed now. Great!
To get started you need Cargo s bin directory ($HOME/.cargo/bin) in your PATH
environment variable. Next time you log in this will be done
automatically.
To configure your current shell run source $HOME/.cargo/env
$ source $HOME/.cargo/env
$ rustup --version
rustup 1.20.2 (13979c968 2019-10-16)
$ rustc --version
rustc 1.39.0 (4560ea788 2019-11-04)
$ cargo --version
cargo 1.39.0 (1c6ec66d5 2019-09-30)
$
本記事でのバージョンは上で確認しているとおりとなります。以下についてもバージョンはコマンドで確認しているものになります。
wasm-pack
wasm-pack はRustが生成したWebAssembly関連の操作をもろもろやってくれます。リンク先にあるコマンドを実行してインストールします。
$ curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
info: downloading wasm-pack
info: successfully installed wasm-pack to `/home/user_name/.cargo/bin/wasm-pack`
$ wasm-pack --version
wasm-pack 0.8.1
$
cargo-generate
他の人が作ってくれたRustプロジェクトをGitレポジトリからいい感じで取ってきてくれます。今回は素直にテンプレに従いましょう。
ashleygwilliams/cargo-generate: cargo, make me a project
$ cargo install cargo-generate
...
# 数分後
...
$ cargo generate --version
cargo-generate 0.5.0
ちなみに cargo-xxx
系のツールを cargo install
で取ってくると、 cargo xxx
のように使用できるようになります。
npm
Node.jsのパッケージマネージャです。今回はJS周りと開発用サーバ、デプロイのために使用します。
Node.js を入れると自動的に付いてくるみたいです。Node.jsのインストールは Ubuntuに最新のNode.jsを難なくインストールする - Qiita が参考になりました。
$ sudo apt install -y nodejs npm
...
$ sudo npm install n -g
...
$ sudo n stable
...
$ sudo apt purge -y nodejs npm
...
$ exec $SHELL -l
$ node -v
v12.14.0
2. Hello, World!
Hello, World! - Rust and WebAssembly の内容ままです。RustのWasmプログラムをHTMLから呼び出してみます!
テンプレートを取ってくる
いい感じに設定してくれたテンプレがあるので取ってきます。プロジェクト名を聞かれるのでとりあえずここでは wasm-lesson
にでもしましょう。
どこかワークスペースに移動したら以下を実行します。
$ cargo generate --git https://github.com/rustwasm/wasm-pack-template
Project Name: wasm-lesson
Creating project called `wasm-lesson`...
Done! New project created .../wasm-lesson
$
テンプレートの中身を見てみるとこんな感じになっています2。
$ cd wasm-lesson/
$ tree
.
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
├── src
│ ├── lib.rs
│ └── utils.rs
└── tests
└── web.rs
2 directories, 7 files
$
それぞれのファイルを細かく見ていきます。
Cargo.toml
Rustを一通りやったことがある人ならすでに見たことがあると思いますが、依存関係等の設定が書かれたファイルです。重要。
今回は wasm-bindgen
への依存が書かれていたり、 crate-type
にWasmにするためのもろもろが書かれていたりします。
[package]
name = "wasm-lesson"
version = "0.1.0"
authors = ["username"]
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2"
console_error_panic_hook = { version = "0.1.1", optional = true }
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"
もちろん依存を何か追加したければここに追加することになります。
src/lib.rs
今回Wasm化したいコードをここに書きます。すでに色々書き込まれていますが、変更を加えるのは static ALLOC: ...
の行より下となります。use
文ですでに wasm_bindgen
関連のものが加えられているのがわかります。
extern
内では、JavaScriptの関数を使用可能にしています。
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-lesson!");
}
Hello, ...
がしたいので、とりあえずこのままで良さそうですが、便宜のために greet
関数を aisatsu
という名前に変えておきます。
src/utils.rs
デバッグ関連のものが入っていますが、本記事では説明を省きます。詳しく知りたい方は件の ライフゲームのチュートリアル の方を確認してください。
プロジェクトのビルド
cargo build
の代わりに、コンパイル3等と同時にjs用のAPIなどを用意してくれる wasm-pack build
を実行します。
$ wasm-pack build
...
warning: function is never used: `set_panic_hook`
--> src/utils.rs:1:1
|
1 | pub fn set_panic_hook() {
| ^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
Finished release [optimized] target(s) in 1m 37s
[INFO]: Installing wasm-bindgen...
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'.
These are not necessary, but recommended
[INFO]: :-) Done in 1m 42s
[INFO]: :-) Your wasm pkg is ready to publish at ./pkg.
$
最後あたりで警告がでますが、これは utils.rs
の関数を呼び出していないからというだけなので無視してください。
ともかくこれで階層構造は次のようになります。
$ tree -L 2
.
├── Cargo.lock
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
├── pkg
│ ├── README.md
│ ├── package.json
│ ├── wasm_lesson.d.ts
│ ├── wasm_lesson.js
│ ├── wasm_lesson_bg.d.ts
│ └── wasm_lesson_bg.wasm
├── src
│ ├── lib.rs
│ └── utils.rs
├── target
│ ├── release
│ └── wasm32-unknown-unknown
└── tests
└── web.rs
pkg/wasm_lesson.*
みたいなファイルがいくつかできています。 *.d.ts
はTypeScript用のコードなので今回は扱いません。.wasm
にコンパイルされたコードが、 .js
にそのインターフェースが入っています。 package.json
はNode.js用のメタデータです。
Webページを作成する
ここでもテンプレートを使用しますが、今回は npm
で取ってきます。 rustwasm/create-wasm-app: npm init template for consuming rustwasm pkgs
今回はフォルダ名として www
を指定します。
$ npm init wasm-app www
npx: 1個のパッケージを0.822秒でインストールしました。
🦀 Rust + 🕸 Wasm = ❤
$
するとプロジェクトに www
フォルダが追加されます。
$ tree www
www
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── bootstrap.js
├── index.html
├── index.js
├── package-lock.json
├── package.json
└── webpack.config.js
いっぱいファイルが見当たりますが、一番目につくのは index.html
でしょう。開いてみます。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello wasm-pack!</title>
</head>
<body>
<noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
<script src="./bootstrap.js"></script>
</body>
</html>
bootstrap.js
を読み込んでいますね。 bootstrap.js
を開けばわかりますが、こちらでは index.js
をインポートしています。 index.js
の中身は次のようになっています。
import * as wasm from "hello-wasm-pack";
wasm.greet();
greet
関数が呼ばれていますが、これはテンプレートの依存先なので注意です。この後で変更しましょう。
ここで npm
で依存関係を解決しておきます。
$ cd www
$ npm install
...
$
Wasmを index.html
が読み込めるようにする
まず依存関係を示すために、 www/package.json
の dependencies
にディレクトリを追加します。
{
//...
"devDependencies": {
"wasm-lesson": "file:../pkg", // 追加
// ...
}
}
index.js
の中身も書き換えましょう。
import * as wasm from "wasm-lesson";
wasm.aisatsu();
再び依存関係を解決します。
$ npm install
いよいよ表示!
これで準備完了です。まずはローカルで表示を確認しましょう。バックグラウンドで実行させておくと色々と楽なので、新しいターミナルを開いて www
まで移動し、サーバーを起動させます!
$ npm run start
この状態でブラウザにて localhost:8080
にアクセスすれば、次のような表示を見られるでしょう。
3. 軽くプログラミング
Hello, World!
のみではあまりにもつまらないので、実用的なものとして行列積を求めるプログラムを作成してみましょう!(ライフゲームだと長すぎるので...)
行列積については 行列の積の定義とその理由 | 高校数学の美しい物語 とかその辺りを参考に調べてみてください。行列を扱うのは、Wasmの配列の扱いは少々面倒くさいため、サンプルとして機能すればという思いからです(実際実装には割と苦労しました...)。
本題ではないのでちまちま書かず完成品を載せます。
Cargo.toml
wasm-bindgen
について JsValue::into_serde()
が扱えるように serde-serialize
を features
に加えています。また、行列積を計算する関数について型を特定させたくなかったので、トレイトを導入する目的で num
を入れています。(あまり意味はなかった)
# ...
[dependencies]
# wasm-bindgen = "0.2"
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } # こちらに変更
num = "0.2.0" # 追加
# ...
src/lib.rs
配列の受け渡しが割と面倒なため、構造体を使用したりしています。配列については後述。
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;
extern crate num;
fn dot_sub<T>(a: &Vec<Vec<T>>, b: &Vec<Vec<T>>) -> Option<Vec<Vec<T>>>
where T: num::Num + Clone + Copy {
if a.len() == 0 || a[0].len() != b.len() {
return None;
}
let mut res = vec![vec![T::zero(); a.len()]; b[0].len()];
for i in 0..a.len() {
for k in 0..a[0].len() {
for j in 0..b[0].len() {
res[i][j] = res[i][j] + a[i][k] * b[k][j];
}
}
}
Some(res)
}
const MAX: usize = 1024;
#[wasm_bindgen]
pub struct Matrix {
result: [f64; MAX],
}
#[wasm_bindgen]
impl Matrix {
pub fn new() -> Matrix {
Matrix {
result: [0.0; MAX],
}
}
pub fn get_result_ptr(&self) -> *const f64 {
self.result.as_ptr()
}
pub fn dot(&mut self, a: JsValue, b: JsValue, di: u8, dk: u8, dj: u8) {
utils::set_panic_hook();
let (di, dk, dj) = (di as usize, dk as usize, dj as usize);
let a: Vec<f64> = a.into_serde().unwrap();
let b: Vec<f64> = b.into_serde().unwrap();
let a = (0..di).map(|i| {
(0..dk).map(|k| a[i*dk+k]).collect::<Vec<_>>()
}).collect::<Vec<_>>();
let b = (0..dk).map(|k| {
(0..dj).map(|j| b[k*dj+j]).collect::<Vec<_>>()
}).collect::<Vec<_>>();
dot_sub(&a, &b).unwrap()
.iter().flatten().enumerate()
.for_each(|(i, &v)| self.result[i] = v);
}
}
index.html
クソ雑魚UIです。変更を加えたら直ちに計算させるので計算ボタンみたいなものは設けていません。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>行列積 計算</title>
<link rel="stylesheet" type="text/css" href="./style.css">
</head>
<body>
<div class="wrapper">
<div class="left-b"></div>
<table id="mat-a"><tbody></tbody></table>
<div class="right-b"></div>
<div class="left-b"></div>
<table id="mat-b"><tbody></tbody></table>
<div class="right-b"></div>
<div style="display: flex;flex-direction: column;justify-content: center;">
<div> = </div>
</div>
<div class="left-b"></div>
<table id="mat-c"><tbody></tbody></table>
<div class="right-b"></div>
</div>
<br />
di: <input id="di" class="config" type="number" value="2" /><br />
dk: <input id="dk" class="config" type="number" value="3" /><br />
dj: <input id="dj" class="config" type="number" value="2" /><br />
<script src="./bootstrap.js"></script>
</body>
</html>
index.js
set_matrix
関数で行列のサイズを変更。 calc_matrix
で行列積の計算を行っています。
import { Matrix } from "wasm-lesson";
import { memory } from "wasm-lesson/wasm_lesson_bg";
const set_matrix = () => {
const tb_a = document.querySelector("#mat-a tbody");
const tb_b = document.querySelector("#mat-b tbody");
const tb_c = document.querySelector("#mat-c tbody");
const di = parseInt(document.querySelector("#di").value);
const dk = parseInt(document.querySelector("#dk").value);
const dj = parseInt(document.querySelector("#dj").value);
tb_a.innerHTML = "";
tb_b.innerHTML = "";
tb_c.innerHTML = "";
for (let i = 0; i < di; i++) {
const tr = document.createElement("tr");
for (let k = 0; k < dk; k++) {
const td = document.createElement("td");
td.innerHTML = `<input class="mat-inp" type="number" value="0" />`;
tr.appendChild(td);
}
tb_a.appendChild(tr);
}
for (let k = 0; k < dk; k++) {
const tr = document.createElement("tr");
for (let j = 0; j < dj; j++) {
const td = document.createElement("td");
td.innerHTML = `<input class="mat-inp" type="number" value="0" />`;
tr.appendChild(td);
}
tb_b.appendChild(tr);
}
for (let i = 0; i < di; i++) {
const tr = document.createElement("tr");
for (let j = 0; j < dj; j++) {
const td = document.createElement("td");
td.setAttribute("id", `c${i}-${j}`);
td.innerHTML = "0";
tr.appendChild(td);
}
tb_c.appendChild(tr);
}
};
const calc_matrix = (mat) => {
const tb_a = document.querySelector("#mat-a tbody");
const tb_b = document.querySelector("#mat-b tbody");
const tb_c = document.querySelector("#mat-c tbody");
const di = parseInt(document.querySelector("#di").value);
const dk = parseInt(document.querySelector("#dk").value);
const dj = parseInt(document.querySelector("#dj").value);
if (di < 0 || dk < 0 || dj < 0 || di > 32 || dk > 32 || dj > 32) return;
const a = Array.from(tb_a.querySelectorAll("td input")).map(e => {
const res = parseFloat(e.value);
return res ? res : 0.0;
});
const b = Array.from(tb_b.querySelectorAll("td input")).map(e => {
const res = parseFloat(e.value);
return res ? res : 0.0;
});
mat.dot(a, b, di, dk, dj);
const c = new Float64Array(memory.buffer, mat.get_result_ptr(), di * dj);
tb_c.querySelectorAll("td").forEach((e, i) => e.innerHTML = c[i]);
};
const main = () => {
const matrix = Matrix.new();
set_matrix();
calc_matrix(matrix);
addEventListener("change", e => {
if (e.target.classList.contains("config")) {
set_matrix();
} else {
calc_matrix(matrix);
}
}, false);
};
main();
ここで配列の話をしておきます。Wasmの配列は扱いが面倒です。JavaScript側にて、まるでC言語かのようにバッファ、ポインタ、大きさから配列の位置を特定する必要があります。その処理を new Float64Array(...)
によって行っています。
2次元配列は1次元配列にしておいたほうが扱いが楽です。
style.css
クソ雑魚UIだけども少しは体裁を整えたい...
@charset "utf-8";
.wrapper {
display: flex;
}
.wrapper > * {
margin: 5px;
}
input.mat-inp {
border: none;
width: 30px;
text-align: center;
}
.left-b, .right-b {
border: black solid 2px;
width: 3px;
}
.left-b {
border-right: none;
}
.right-b {
border-left: none;
}
ここまで変更を加えたら、 wasm-lesson
ディレクトリで wasm-pack build
を行い、 www
ディレクトリで npm run start
して実際に動くか確かめてみましょう。
ライフゲームのチュートリアル ではこの後プロファイリングやデバッグの方法などが述べられていますが、本記事では割愛します。
4. Firebaseでデプロイ
はい。ここまではWasmを動かす話でした。本題はここから。いよいよデプロイしていきます!!!!!
別にFirebaseである必要性はないのですが、 npm
を使用していることを考慮すると、気軽にデプロイしたい場合は最も楽な方法でしょう。
とりあえず 公式ドキュメント に従って進めていきます。(ステップ4からですが() )
CLIツールのインストール
firebase
コマンドを使用可能にするために次のコマンドを打ってください。
$ npm install -g firebase-tools
もしパーミッションエラーが出た場合は次のページが参考になりました。 npmでpermission deniedになった時の対処法[mac] - Qiita
ともかくこれでfirebaseが使用可能なはずです。
$ firebase --version
7.11.0
プロジェクトの初期化
次にGoogleアカウント(流石に持ってますよね...?)でログインし、プロジェクトを初期化します。
以下のコマンドを打つとブラウザが開くと思うのでログインし、許可していきます。画像のとおりになったら完了です。
$ firebase login
...
Waiting for authentication...
✔ Success! Logged in as username@gmail.com
$
次に wasm-lesson/www
ディレクトリにて初期化します。
色々聞かれます。
- 使用目的: Hosting
- プロジェクト: Create a new project
- プロジェクト名: (後から変更不可らしいので気をつけましょう。)
- 呼び名: (任意名でデフォルトはプロジェクト名)
$ firebase init
...
? Which Firebase CLI features do you want to set up for this folder? Press Space
to select features, then Enter to confirm your choices.
◯ Database: Deploy Firebase Realtime Database Rules
◯ Firestore: Deploy rules and create indexes for Firestore
◯ Functions: Configure and deploy Cloud Functions
❯◉ Hosting: Configure and deploy Firebase Hosting sites
◯ Storage: Deploy Cloud Storage security rules
◯ Emulators: Set up local emulators for Firebase features
...
? Please select an option:
Use an existing project
❯ Create a new project
Add Firebase to an existing Google Cloud Platform project
Don't set up a default project
...
? Please specify a unique project id (warning: cannot be modified afterward) [6-
30 characters]:
() (任意名)
? What would you like to call your project? (defaults to your project ID) () (任意名)
(とりあえず、筆者はその後の面倒を避けるため wasm-lesson-namnium
にしました。ちなみに私はnamniumと申します。よろしく。)
利用規約に同意していない、といったエラーが出る場合は、利用規約に同意しつつ FIrebase Console にて直接プロジェクトを作成するといいかもしれません。その場合「Please select an option」 では「Use an existing project」を選びます。
その後公開ディレクトリについてと単独ページにするかを聞かれますが public
Yes
で構いません。
デプロイ作業
次にまず www
にてデプロイ用データを作成します。(ここ地味に重要)
$ npm run build
すると dist
というディレクトリができます。これが公開対象です!firebaseの公開設定を変更することで dist
を公開対象にします。
{
"hosting": {
// "public": "public", // 消去
"public": "dist/",
// ...
}
}
またこのままだと style.css
が読み込まれないのでコピーしておきます4。
$ cp ./style.css ./dist/
最後に、次のコマンドを打てばデプロイ完了です、お疲れ様でした!!!
$ firebase deploy
...
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/wasm-lesson-namnium/overview
Hosting URL: https://wasm-lesson-namnium.firebaseapp.com
$
というわけでうまくデプロイできたものを載せておきます。
不明な点、おかしな点等あれば、コメント、編集リクエスト、ぜひよろしくお願いいたしますm(_ _)m
ここまで読んでくださりありがとうございました!