wasmに入門してみる【5】

にあえん

August 11, 2023

シリーズ化しているwasm入門です。

(wasmのチュートリアルをやったという実績残しと備忘録を兼ねてやっています。真新しいことはないので悪しからず。。)

テストの実装

これまでで実装してきたライフゲームのテストを実装します。

Testing Life - Rust and WebAssembly

まずはUniverseの幅と高さのセッターを実装します。

#[wasm_bindgen]
impl Universe {
    // ...

    /// Universeの幅を設定します。
    ///
    /// すべてのセルの状態を死んだ状態にします。
    pub fn set_width(&mut self, width: u32) {
        self.width = width;
        self.cells = (0..width * self.height).map(|_i| Cell::Dead).collect();
    }

    /// Universeの高さを設定します。
    ///
    /// すべてのセルの状態を死んだ状態にします。
    pub fn set_height(&mut self, height: u32) {
        self.height = height;
        self.cells = (0..self.width * height).map(|_i| Cell::Dead).collect();
    }
    // ...
}

次に、セルのゲッターとセッターを実装します。

ここで、セルのゲッターを#[wasm_bindgen]マクロのあるUniverseに実装するとエラーになります。

#[wasm_bindgen]
impl Universe {
    // ...

    /// Get the dead and alive values of the entire universe.
    pub fn get_cells(&self) -> &[Cell] {
        &self.cells
    }

    // ...
}

この状態でビルドします。

% wasm-pack build
[INFO]: 🎯  Checking for the Wasm target...
[INFO]: 🌀  Compiling to Wasm...
   Compiling wasm-game-of-life v0.1.0 (/home/username/Project/personal/wasm-game-of-life)
error: cannot return a borrowed ref with #[wasm_bindgen]
  --> src/lib.rs:70:32
   |
70 |     pub fn get_cells(&self) -> &[Cell] {
   |                                ^^^^^^^

error: could not compile `wasm-game-of-life` (lib) due to previous error
Error: Compiling your crate to WebAssembly failed
Caused by: Compiling your crate to WebAssembly failed
Caused by: failed to execute `cargo build`: exited with exit status: 101
  full command: cd "/home/username/Project/personal/wasm-game-of-life" && "cargo" "build" "--lib" "--release" "--target" "wasm32-unknown-unknown"

エラーが発生しました。

#[wasm_bindgen]マクロを使用してJSに公開する場合、借用した参照は返すことはできないという制約があるためです。

今回はテスト実装なのでいいですが、wasmの実装を行う際は気をつけたほうがいいでしょう。

では気を取り直して、Cellのセッターとゲッターをwasm内部で使用するUniverseトレイトに実装しましょう。

impl Universe {
    // ...

    /// すべてのセルの参照を取得するゲッター
    pub fn get_cells(&self) -> &[Cell] {
        &self.cells
    }

    /// セルの行列座標のリストを受け取り、それらのセルを生きている状態にする
    pub fn set_cells(&mut self, cells: &[(u32, u32)]) {
        for (row, col) in cells.iter().cloned() {
            let idx = self.get_index(row, col);
            self.cells[idx] = Cell::Alive;
        }
    }

    // ...
}

これでテストを作成する下地ができましたが、既にテストコマンドは使えるようなので、一度実行してみます。

% wasm-pack test --chrome --headless
[INFO]: 🎯  Checking for the Wasm target...
   Compiling proc-macro2 v1.0.66
   Compiling unicode-ident v1.0.11
   Compiling wasm-bindgen-shared v0.2.87
   Compiling once_cell v1.18.0
   Compiling bumpalo v3.13.0
   Compiling log v0.4.19
   Compiling wasm-bindgen v0.2.87
   Compiling cfg-if v1.0.0
   Compiling scoped-tls v1.0.1
   Compiling quote v1.0.32
   Compiling syn v2.0.28
   Compiling wasm-bindgen-test-macro v0.3.37
   Compiling wasm-bindgen-backend v0.2.87
   Compiling wasm-bindgen-macro-support v0.2.87
   Compiling wasm-bindgen-macro v0.2.87
   Compiling js-sys v0.3.64
   Compiling console_error_panic_hook v0.1.7
   Compiling wasm-game-of-life v0.1.0 (/home/username/Project/personal/wasm-game-of-life)
   Compiling wasm-bindgen-futures v0.4.37
   Compiling wasm-bindgen-test v0.3.37
    Finished dev [unoptimized + debuginfo] target(s) in 14.98s
[INFO]: ⬇️  Installing wasm-bindgen...
[INFO]: Getting chromedriver...
    Finished test [unoptimized + debuginfo] target(s) in 0.03s
     Running unittests src/lib.rs (target/wasm32-unknown-unknown/debug/deps/wasm_game_of_life-5f9f2f4d1774b96a.wasm)
no tests to run!
     Running tests/web.rs (target/wasm32-unknown-unknown/debug/deps/web-98641d5e2e4ced2f.wasm)
Set timeout to 20 seconds...
Running headless tests in Chrome on `http://127.0.0.1:45799/`
Try find `webdriver.json` for configure browser's capabilities:
Not found
driver status: signal: 9 (SIGKILL)                
driver stdout:
    Starting ChromeDriver 114.0.5735.90 (386bc09e8f4f2e025eddae123f36f6263096ae49-refs/branch-heads/5735@{#1052}) on port 45799
    Only local connections are allowed.
    Please see https://chromedriver.chromium.org/security-considerations for suggestions on keeping ChromeDriver safe.
    ChromeDriver was started successfully.

Error: non-200 response code: 404                 
{"value":{"error":"invalid session id","message":"invalid session id","stacktrace":"#0 0x55ab67a524e3 \u003Cunknown>\n#1 0x55ab67781b00 \u003Cunknown>\n#2 0x55ab677b1919 \u003Cunknown>\n#3 0x55ab677dcf16 \u003Cunknown>\n#4 0x55ab677d917a \u003Cunknown>\n#5 0x55ab677d88a6 \u003Cunknown>\n#6 0x55ab67751263 \u003Cunknown>\n#7 0x55ab67a123e4 \u003Cunknown>\n#8 0x55ab67a163d7 \u003Cunknown>\n#9 0x55ab67a20b20 \u003Cunknown>\n#10 0x55ab67a17023 \u003Cunknown>\n#11 0x55ab679e51aa \u003Cunknown>\n#12 0x55ab6774fa43 \u003Cunknown>\n#13 0x7f6d00629d90 \u003Cunknown>\n"}}
error: test failed, to rerun pass `--test web`

Caused by:
  process didn't exit successfully: `/home/username/.cache/.wasm-pack/wasm-bindgen-5c77a27a44722aba/wasm-bindgen-test-runner /home/username/Project/personal/wasm-game-of-life/target/wasm32-unknown-unknown/debug/deps/web-98641d5e2e4ced2f.wasm` (exit status: 1)
Error: Running Wasm tests with wasm-bindgen-test failed
Caused by: Running Wasm tests with wasm-bindgen-test failed
Caused by: failed to execute `cargo test`: exited with exit status: 1
  full command: cd "/home/username/Project/personal/wasm-game-of-life" && CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER="/home/username/.cache/.wasm-pack/wasm-bindgen-5c77a27a44722aba/wasm-bindgen-test-runner" CHROMEDRIVER="/home/username/.cache/.wasm-pack/chromedriver-365490088b2eefa3/chromedriver" WASM_BINDGEN_TEST_ONLY_WEB="1" "cargo" "test" "--target" "wasm32-unknown-unknown"

あらら、いろいろエラーが出てしまいました。。

最近は専らBraveというブラウザを使っていたので、Chromeのバージョンが古くなってしまったのが原因かもしれません。

(この辺のIssueを見て思った)

Error while running `wasm-pack test --headless --chrome` · Issue #21 · input-output-hk/js-chain-libs · GitHub

ということでChromeを最新版にして再実行しました。

% wasm-pack test --chrome --headless  
[INFO]: 🎯  Checking for the Wasm target...
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
[INFO]: ⬇️  Installing wasm-bindgen...
    Finished test [unoptimized + debuginfo] target(s) in 0.02s
     Running unittests src/lib.rs (target/wasm32-unknown-unknown/debug/deps/wasm_game_of_life-5f9f2f4d1774b96a.wasm)
no tests to run!
     Running tests/web.rs (target/wasm32-unknown-unknown/debug/deps/web-98641d5e2e4ced2f.wasm)
Set timeout to 20 seconds...
Running headless tests in Chrome on `http://127.0.0.1:38635/`
Try find `webdriver.json` for configure browser's capabilities:
Not found
running 1 test                                    

test web::pass ... ok

test result: ok. 1 passed; 0 failed; 0 ignored

無事実行されました!

web::passというテストが一件実行されていますね。

実装は既にtests/web.rsに格納されています。

//! Test suite for the Web and headless browsers.

#![cfg(target_arch = "wasm32")]

extern crate wasm_bindgen_test;
use wasm_bindgen_test::*;

wasm_bindgen_test_configure!(run_in_browser);

#[wasm_bindgen_test]
fn pass() {
    assert_eq!(1 + 1, 2);
}

このテストは必ず通るようになっており、今回のようにChromeのバージョンが間違っていたりドライバの問題がある場合はエラーになるようです。

まずは、テストを行うためにtests/web.rsからライフゲームの実装を実行できるようにクレートとUniverseを取得します。

extern crate wasm_game_of_life;
use wasm_game_of_life::Universe;

…とチュートリアルではなっていますが、Rust2018以降extern crateは一部の特殊な状況以外では必要なくなりました。

今や、プロジェクトに新しくクレートを追加したかったら、Cargo.toml に追記して、それで終わりです。

パスとモジュールシステムへの変更 - エディションガイド

なのでextern crateは消してしまって問題ありません。

(あっても後方互換性があるので動きます)

次に、初期状態のUniverseとその次のフレームでのUniverseの状態を定義します。

#[cfg(test)]
pub fn input_spaceship() -> Universe {
    /// 初期のUniverseのセル状態を定義
    let mut universe = Universe::new();
    universe.set_width(6);
    universe.set_height(6);
    universe.set_cells(&[(1,2), (2,3), (3,1), (3,2), (3,3)]);
    universe
}

#[cfg(test)]
pub fn expected_spaceship() -> Universe {
    /// input_spaceshipの次のフレームのUniverseのセル状態を定義
    let mut universe = Universe::new();
    universe.set_width(6);
    universe.set_height(6);
    universe.set_cells(&[(2,1), (2,3), (3,2), (3,3), (4,2)]);
    universe
}

これでテストの準備ができたので、テスト関数を実装します。

wasmのテストにはwasm_bindgen_testを使用します。

#[wasm_bindgen_test]
pub fn test_tick() {
    // 初期状態のUniverseを呼び出す
    let mut input_universe = input_spaceship();

    // 初期状態から予想される次の状態のUniverseを呼び出す
    let expected_universe = expected_spaceship();

    // 初期状態のUniverseのtick関数を呼び出して、予想した次の状態のUniverseと一致するか確認します
    input_universe.tick();
    assert_eq!(&input_universe.get_cells(), &expected_universe.get_cells());
}

これを実行します。

% wasm-pack test --firefox --headless
[INFO]: 🎯  Checking for the Wasm target...
   Compiling wasm-game-of-life v0.1.0 (/home/username/Project/personal/wasm-game-of-life)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
[INFO]: ⬇️  Installing wasm-bindgen...
    Finished test [unoptimized + debuginfo] target(s) in 0.02s
     Running unittests src/lib.rs (target/wasm32-unknown-unknown/debug/deps/wasm_game_of_life-5f9f2f4d1774b96a.wasm)
no tests to run!
     Running tests/web.rs (target/wasm32-unknown-unknown/debug/deps/web-98641d5e2e4ced2f.wasm)
Set timeout to 20 seconds...
Running headless tests in Firefox on `http://127.0.0.1:45759/`
Try find `webdriver.json` for configure browser's capabilities:
Not found
running 2 tests                                   

test web::test_tick ... ok
test web::pass ... ok

test result: ok. 2 passed; 0 failed; 0 ignored

無事実行できました!

さりげなくfirefoxで実行してますが、問題なさそうですね。

chromeでもテストしましたが問題なく動きました。

デバッギング

個人的にこれが一番楽しみだったかもしれません。

wasmって結構RustとJSを言ったり来たりするので、どうやってデバッグすればいいのか皆目見当がつきませんでした。

Debugging - Rust and WebAssembly

ここではデバッギングについて学んで行きます。

まずデバッグするために必要なクレートを追加します。

cargo add web-sys -F "console"

もしくはCargo.tomlに[dependencies.web-sys]セクションを追加します。

[dependencies.web-sys]
version = "0.3.64"
features = [
    "console",
]

これを追加すると、Rust上からブラウザ上のコンソール出力が可能になります!

Universeに追加してみましょう。

#[wasm_bindgen]
impl Universe {

    pub fn new() -> Universe {
        // コンソール出力を追加
        web_sys::console::log_1(&"Hello, world!".into());
        //...
    }
    //...
}

ビルドしてブラウザから確認してみましょう。

wasm-pack build && (cd www && npm start)

ちなみに、ここで()を付けているのは、サブシェルという別プロセスで実行させるためです。

これによりディレクトリの移動をしなくても指定ディレクトリでコマンドを実行できます。

カレントディレクトリを変更せずに、特定のディレクトリでコマンドを実行する - Qiita

出力されていることが分かります。

ではこのログをprintln!マクロと同じように扱えるlog!マクロを実装してみます。

use web_sys;

// Rustからprintln!と同じようにJSのconsole.log出力を行うことのできるマクロ
macro_rules! log {
    ( $( $t:tt )* ) => {
        web_sys::console::log_1(&format!( $( $t )* ).into());
    }
}

このログをtick関数から呼び出すように修正してみます。


    pub fn tick(&mut self) {
        //...
                let live_neighbors = self.live_neighbor_count(row, col);

                // ログに出力する
                log!(
                    "cell[{}, {}] is initially {:?} and has {} live neighbors",
                    row,
                    col,
                    cell,
                    live_neighbors
                );

                let next_cell = match (cell, live_neighbors) {
                    //...
                };
                
                // セルの状態を出力
                log!("    it becomes {:?}", next_cell);

                next[idx] = next_cell;
            }
        }

        self.cells = next;
    }

これをまたブラウザで確認してみましょう。と言いたいところですが、これをこのまま実行して確認すると膨大なコンソール出力によってめちゃくちゃ重くなります。

なので、www/index.jsdebugger;という文言を追加して、止まるようにしてあげます。

const renderLoop = () => {
  universe.tick();

  drawGrid();
  drawCells();
  debugger; // これを追加

  requestAnimationFrame(renderLoop);
};

これでブラウザでアクセスしてみます。

アクセスの際、F12で開発者ツールを開いておくのを忘れずに!(開いておかないとデバッグできず死にます)

止まってくれました。

コンソールには既に先ほど実装したログがいくつも出力されていました。

残念ながら、Rustコードに直接ブレークポイントを置くといったことはできないっぽいですね。。

まとめ

デバッグに便利なweb-sysクレートですが、featuresがかなりの量用意されているので、いろいろ見てみるのも楽しいですね。

web_sys - Rust

まだチュートリアルは続くので、近いうちに上げます。

2023-08-13 追記

wasmのエラーはフロントエンドでは通常「RuntimeError: Unreachable executed」と、なにが原因でエラーになったのか分かりづらいですが

「console_error_panic_hook」を使えばフロントエンドからエラーの内容が推測しやすくなります。

GitHub - rustwasm/console_error_panic_hook: A panic hook for wasm32-unknown-unknown that logs panics with console.error

使い方も簡単で、以下の関数を一度実行するだけでいいです。

    console_error_panic_hook::set_once();

ただし、このパッケージの実行にはすべてのstd::fmtstd::panickingの実装が含まれるため、あくまでデバッグのためだけに使用するようにしましょう。