39
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rustその2Advent Calendar 2019

Day 19

ゼロからRust+WasmをFirebaseでデプロイするまでを簡単に

Last updated at Posted at 2019-12-19

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 )。省略を ... とします。

bash
$ 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関連の操作をもろもろやってくれます。リンク先にあるコマンドを実行してインストールします。

bash
$ 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

bash
$ 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 が参考になりました。

bash
$ 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 にでもしましょう。

どこかワークスペースに移動したら以下を実行します。

bash
$ 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

bash
$ 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にするためのもろもろが書かれていたりします。

Cargo.toml一部抜粋
[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の関数を使用可能にしています。

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;

#[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 を実行します。

bash
$ 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 の関数を呼び出していないからというだけなので無視してください。

ともかくこれで階層構造は次のようになります。

bash
$ 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 を指定します。

bash
$ npm init wasm-app www
npx: 1個のパッケージを0.822秒でインストールしました。
🦀 Rust + 🕸 Wasm =$

するとプロジェクトに www フォルダが追加されます。

bash
$ 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 でしょう。開いてみます。

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 の中身は次のようになっています。

index.js
import * as wasm from "hello-wasm-pack";

wasm.greet();

greet 関数が呼ばれていますが、これはテンプレートの依存先なので注意です。この後で変更しましょう。

ここで npm で依存関係を解決しておきます。

bash
$ cd www
$ npm install
...
$

Wasmを index.html が読み込めるようにする

まず依存関係を示すために、 www/package.jsondependencies にディレクトリを追加します。

www/package.json
{
  //...
  "devDependencies": {
      "wasm-lesson": "file:../pkg", // 追加
      // ...
  }
}

index.js の中身も書き換えましょう。

index.js
import * as wasm from "wasm-lesson";

wasm.aisatsu();

再び依存関係を解決します。

bash
$ npm install

いよいよ表示!

これで準備完了です。まずはローカルで表示を確認しましょう。バックグラウンドで実行させておくと色々と楽なので、新しいターミナルを開いて www まで移動し、サーバーを起動させます!

bash
$ npm run start

この状態でブラウザにて localhost:8080 にアクセスすれば、次のような表示を見られるでしょう。

アド_image1.png

3. 軽くプログラミング

Hello, World! のみではあまりにもつまらないので、実用的なものとして行列積を求めるプログラムを作成してみましょう!(ライフゲームだと長すぎるので...)

行列積については 行列の積の定義とその理由 | 高校数学の美しい物語 とかその辺りを参考に調べてみてください。行列を扱うのは、Wasmの配列の扱いは少々面倒くさいため、サンプルとして機能すればという思いからです(実際実装には割と苦労しました...)。

本題ではないのでちまちま書かず完成品を載せます。

Cargo.toml

wasm-bindgen について JsValue::into_serde() が扱えるように serde-serializefeatures に加えています。また、行列積を計算する関数について型を特定させたくなかったので、トレイトを導入する目的で num を入れています。(あまり意味はなかった)

Cargo.toml
# ...

[dependencies]
# wasm-bindgen = "0.2"
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } # こちらに変更
num = "0.2.0" # 追加

# ...

src/lib.rs

配列の受け渡しが割と面倒なため、構造体を使用したりしています。配列については後述。

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です。変更を加えたら直ちに計算させるので計算ボタンみたいなものは設けていません。

index.html
<!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 で行列積の計算を行っています。

index.js
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だけども少しは体裁を整えたい...

style.css
@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 して実際に動くか確かめてみましょう。

アド_image2.png

ライフゲームのチュートリアル ではこの後プロファイリングやデバッグの方法などが述べられていますが、本記事では割愛します。

4. Firebaseでデプロイ

はい。ここまではWasmを動かす話でした。本題はここから。いよいよデプロイしていきます!!!!!

別にFirebaseである必要性はないのですが、 npm を使用していることを考慮すると、気軽にデプロイしたい場合は最も楽な方法でしょう。

とりあえず 公式ドキュメント に従って進めていきます。(ステップ4からですが() )

CLIツールのインストール

firebase コマンドを使用可能にするために次のコマンドを打ってください。

bash
$ npm install -g firebase-tools

もしパーミッションエラーが出た場合は次のページが参考になりました。 npmでpermission deniedになった時の対処法[mac] - Qiita

ともかくこれでfirebaseが使用可能なはずです。

bash
$ firebase --version
7.11.0

プロジェクトの初期化

次にGoogleアカウント(流石に持ってますよね...?)でログインし、プロジェクトを初期化します。

以下のコマンドを打つとブラウザが開くと思うのでログインし、許可していきます。画像のとおりになったら完了です。

bash
$ firebase login
...
Waiting for authentication...

✔  Success! Logged in as username@gmail.com
$

アド_image4.png

次に wasm-lesson/www ディレクトリにて初期化します。

色々聞かれます。

  • 使用目的: Hosting
  • プロジェクト: Create a new project
  • プロジェクト名: (後から変更不可らしいので気をつけましょう。)
  • 呼び名: (任意名でデフォルトはプロジェクト名)
bash(@wasm-lesson/www)
$ 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 にてデプロイ用データを作成します。(ここ地味に重要)

bash
$ npm run build

すると dist というディレクトリができます。これが公開対象です!firebaseの公開設定を変更することで dist を公開対象にします。

firebase.json
{
  "hosting": {
    // "public": "public", // 消去
    "public": "dist/",
    // ...
  }
}

またこのままだと style.css が読み込まれないのでコピーしておきます4

bash(@www)
$ cp ./style.css ./dist/

最後に、次のコマンドを打てばデプロイ完了です、お疲れ様でした!!!

bash
$ 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

ここまで読んでくださりありがとうございました!

  1. 筆者のほうが理解できていない場合もあるかもです。誤り等あればコメントお待ちしております。

  2. tree コマンドを使用したければaptかなんかで取ってきてください。なくても全く問題ありません。

  3. ここでクロスコンパイルのターゲットに wasm32-unknown-unknown が加わります。

  4. 筆者はnpmに詳しくないためにこのような原始的な手段を取ってしまいました...package.jsonをうまく書けばこのような操作は必要ない...? (2020/6/2追記: webpack.config.jsにてコピーするように記述すれば自動化が可能です。本記事では省略します。)

39
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
39
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?