アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

Rust による WASM サンプルを試す

October 27, 2021開発WASM, Rust

Rust による WASM サンプルを試す。

環境構築

はじめに公式サイト Install Rust へ提示されてる手順で rustup をインストール。私の環境は macOS なので Terminal から curl によりシェル スクリプトをダウンロードしてから実行。続けて rustup update で最新版へ更新後、バージョンを確認しておく。

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
$ rustup update
$ rustup -V
rustup 1.24.3 (ce5817a94 2021-05-31)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.55.0 (c8dfcfe04 2021-09-06)`

rustup に含まれる cargo コマンド経由で WASM 開発用の wasm-pack をインストール。

$ cargo install wasm-pack

wasm-pack 公式の解説では rustup 同様にシェルス クリプトを推奨しているようだ。cargo も紹介されているものの 2021/10 時点だと "display all supported installers." をクリックするまでは表示されない。しかし更新や削除などを考慮するとパッケージ マネージャーのほうがよさそうなため、こちらでゆくことにした。

パッケージ マネージャー管理という意味では npm 版 wasm-pack もよさそうだ。しかし Rust に属するものはそのエコ システムでまとめておくほうが分かりやすいだろうから cargo にしておく。

Rust のコード編集には VS Code を利用する。そのため以下のプラグインを追加した。

プラグイン 用途
Rust Rust の全般的な開発機能サポート。
rust-analyzer コードの入力補完。Rust プラグインに比べて効きがよい。
Better TOML Rust (cargo) プロジェクト設定ファイル Cargo.toml の閲覧と編集。

サンプル プロジェクト生成その 1

前述の環境構築により Quickstart - Hello wasm-pack! の 2 まで済ませているため wasm-pack を利用してプロジェクトを生成。そのディレクトリーへ移動してからビルドを実行してみた。

$ wasm-pack new hello-wasm
$ cd hello-wasm
$ wasm-pack build

ファイル構成をまとめる。WASM の動作検証や npm 公開用のファイルなどが含まれるため、内容物はかなり多い。

項目 内容
src/ Rust のソース コード置き場となるディレクトリー。lib.rs がライブラリー用、utils.rs は名前のとおり便利機能を定義するもの。
tests/ Rust のテスト コード置き場となるディレクトリー。Rust には実装とテストを同一ファイルへ記述する慣習があると聞く。しかし wasm-pack によるものは独立ディレクトリー派のようだ。
.appveyor.yml CI サービス AppVeyor 設定ファイル。動作環境に Windows を持つのが特徴とのこと。私は利用したことがない。2021/10 現在は GitHub Actions に Windows 環境があるから代替できるかもしれない。
.cargo-ok Cargo 用ファイル。What exactly is .cargo-ok and why is Cargo.toml modified? を読むに Cargo のパッケージ処理が中断された場合の再開に利用するファイルらしい。この目的ならローカルに存在すれば十分だから .gitignore へ定義すべきと思うがそうなっていない。
.gitignore Git 管理の除外設定ファイル。
.travis.yml CI サービス Travis CI 設定ファイル。
Cargo.toml Rust のビルドとパッケージ管理を担当する Cargo の設定ファイル。npm でいう package.json 的なもの。
LICENSE_APACHE プロジェクトのライセンス ファイル。"Apache License, Version 2.0" 用。
LICENSE_MIT プロジェクトのライセンス ファイル。"MIT License" 用。
README.md README ファイル。生成されたプロジェクトの運用方法について解説されている。
pkg ビルドにより生成。WASM とそれを npm として参照、公開するための構成ファイル置き場となるディレクトリー。package.json のプロジェクト情報は Cargo.tomlpackage テーブルへ定義した内容が参照される。
target ビルドにより生成。Rust のビルド成果物が出力されるディレクトリー。
Cargo.lock ビルドにより生成。Cargo によりビルドされた際の依存パッケージのバージョンが記録されたファイル。他の環境でもバージョンを系ではなく厳密に参照して同一のビルド結果を得るために共有される。npm でいう package-lock.jsonyarn.lock 的なもの。

テストは README に記載されている wasm-pack test --headless --firefox または cargo test で実行される。前者は引数からして Firefox をヘッドレス (画面表示なし) 実行した環境でテストを走らせるようだ。しかし事前設定を済ませずに実行すると以下のように警告された。

Running headless tests in Firefox on `http://127.0.0.1:60110/`
Try find `webdriver.json` for configure browser's capabilities:
Not found

npm として公開する際は npmjs.com へアカウント登録してから wasm-pack publish を実行するのだが、今回はお試しなのでやめておく。

Cargo.toml

プロジェクト設定ファイル Cargo.toml を読む。

[package]
name = "hello-wasm"
version = "0.1.0"
authors = ["akabeko <sample@example.com>"]
edition = "2018"

[lib]
crate-type = ["cdylib", "rlib"]

[features]
default = ["console_error_panic_hook"]

[dependencies]
wasm-bindgen = "0.2.63"

# 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.6", 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.5", optional = true }

[dev-dependencies]
wasm-bindgen-test = "0.3.13"

[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"

各項目の役割について公式資料 The Manifest Format - The Cargo Book を参考にまとめる。表中に登場するクレート (crate) とは Rust の実行単位でアプリケーションやライブラリーを指す。

[package]

パッケージ設定を定義する。npm の package.json へ指定されるものもある。

設定 役割
name パッケージ名。
version パッケージのバージョン。
authors パッケージの作者。
edition クレートも含むパッケージ内の実行環境に影響する Rust のエディション (版)。本記事では 2018 だがまもなく 2021 リリースも控えているので移行することになりそう。

[lib]

ライブラリーとしての設定。

設定 役割
crate-type クレート種別。Rust 以外の言語から動的ライブラリーとして参照されるための cdylib と Rust 用の参照 rlib が指定されている。

[dependencies]

依存クレート設定。npm の package.json でいう dependencies に相当する。

設定 役割
wasm-bindgen WASM 化のために必要なクレート。
console_error_panic_hook デバッグ用クレート。utils.rs で参照されている。コメントによれば依存の関係で組み込むとサイズが肥大化するため、デバッグ時のみ参照することを想定しているようだ。
wee_alloc WASM 上で動作するメモリー アロケーター用クレート。標準アロケーターより低速だがコード サイズは 10K から 1K に縮小されているとのこと。

[dev-dependencies]

開発用の依存クレート設定。公式リファレンスの Specifying Dependencies - The Cargo Book には

Dev-dependencies are not used when compiling a package for building, but are used for compiling tests, examples, and benchmarks.

とある。パッケージ用のビルドでは使用されないとのこと。npm の package.json でいう devDependencies みたいなものか。

設定 役割
wasm-bindgen-test WASM にコンパイルされた Rust をテスト実行するためのクレート。web.rs で参照されている。

[profile.release]

profile はコンパイラー設定を定義するためのテーブル。最適化やデバッグ シンボルなどを定義する。名前に .release を付与そた場合はリリース用の設定となる。

設定 役割
opt-level 最適化オプション。s はバイナリー サイズを最適化するフラグ。

lib.rs

軽い Rust 入門としてサンプル実装の 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, hello-wasm!");
}

部分単位で考察。

mod utils;

プロジェクト内のモジュール utils.rs を参照してる。ただし参照だけで以降のコードでは利用されず。その旨、コンパイラーに警告される。

use wasm_bindgen::prelude::*;

クレート参照。WASM のホストと相互連携するため 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;

#[...] は Attributes という機能。C# のそれとか Java の Annotation みたいに対象へ宣言的なメタデータを付与する。ここでは wee_alloc をグローバルなアロケーターとして利用する旨、宣言している。

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

ホストから公開された関数やクラスを WASM から参照するための実装。#[wasm_bindgen] で連携を宣言して alert 関数の参照を指定。ホスト側から同名のインターフェースを公開すれば連携される。サンプルとしては Web ブラウザーにおける JavaScript の window.alert 想定なのだろう。

関数ではなくクラス配下のメソッドを連携したい場合は以下の記事を参照のこと。

ただし私は状態と副作用の観点から WASM とホストはともに関数のみで連携するのが好ましいと考えている。そのため便利でもクラスはなるべく避けるだろう。言語によっては関数で状態を持つことも可能だが、それは意図的な強い逸脱としておこなわれるものだと認識している。経験上、異なるプラットフォーム間の境界は簡素にしておくことが無難だ。

#[wasm_bindgen]
pub fn greet() {
    alert("Hello, hello-wasm!");
}

WASM からホストに関数を公開するための実装。前述の alert を呼び出している。

ここまでで wasm-pack new hello-wasm により生成されたプロジェクト構成をひととおり調べたが、やはり実際に動くところを見たい。というわけで次項はもうひとつのサンプルを試す。

サンプル プロジェクト生成その 2

Web ブラウザー上の動作確認コードも含まれる Getting started - Hello wasm-pack! でプロジェクトを生成。

$ npm init rust-webpack my-app

ファイル構成をチェック。

項目 内容
js/ JavaScript のソース コード置き場となるディレクトリー。初期状態は webpack でエントリー ポイント指定されている index.js が格納されている。
node_modules/ npm の依存パッケージ置き場となるディレクトリー。
src/ Rust のソース コード置き場となるディレクトリー。初期状態は lib.rs が格納されている。
static/ Web アプリケーションの静的リソース置き場となるディレクトリー。初期状態は index.html が格納されている。
target Rust のビルド成果物が出力されるディレクトリー。
tests/ Rust のテスト コード置き場となるディレクトリー。初期状態は app.rs が格納されている。
.gitignore Git 管理の除外設定ファイル。
Cargo.lock Cargo によりビルドされた際の依存パッケージのバージョンが記録されたファイル。他の環境でもバージョンを系ではなく厳密に参照して同一のビルド結果を得るために共有される。npm でいう package-lock.jsonyarn.lock 的なもの。
Cargo.toml Rust のビルドとパッケージ管理を担当する Cargo の設定ファイル。npm でいう package.json 的なもの。
package-lock.json 厳密な npm 参照バージョンを記録するためのファイル。
package.json npm プロジェクト設定ファイル。
README.md README ファイル。生成されたプロジェクトの運用方法について解説されている。
webpack.config.js webpack 設定ファイル。Web アプリとしての WASM 動作検証は webpack により実行される。

プロジェクト生成の時点で npm install などが実行されるらしく node_modulespackage-lock.jsonCargo.lock が最初から出力されている。自分でセットアップ用のコマンドを実行することなく開発を始められるので便利だ。

サンプルは webpack によりローカルで HTTP サーバーを起動し、そこで動作する Web アプリから WASM が呼び出されるらしい。試しに実行してみる。

$ npm start

Web ブラウザーで http://localhost:8080/ が開かれて真っ白なページが表示された。開発者ツール (Firefox) でコンソール出力を確認すると以下のメッセージが出力されていた。

Hello world!
[WDS] Live Reloading enabled.

この状態で src/lib.rsstatic/index.htmljs/index.js などを編集すると webpack が変更を検出して再コンパイルと Web ブラウザーの再読み込みを処理してくれる。実に便利だ。HTTP サーバーとファイル変更監視などの停止は npm start した Terminal で Ctrl + C を押下すればよい。

Cargo.toml

Cargo.toml を読む。Quickstart に比べて設定がかなり増えた。

# You must change these to your own details.
[package]
name = "rust-webpack-template"
description = "My super awesome Rust, WebAssembly, and Webpack project!"
version = "0.1.0"
authors = ["You <you@example.com>"]
categories = ["wasm"]
readme = "README.md"
edition = "2018"

[lib]
crate-type = ["cdylib"]

[profile.release]
# This makes the compiled code faster and smaller, but it makes compiling slower,
# so it's only enabled in release mode.
lto = true

[features]
# If you uncomment this line, it will enable `wee_alloc`:
#default = ["wee_alloc"]

[dependencies]
# The `wasm-bindgen` crate provides the bare minimum functionality needed
# to interact with JavaScript.
wasm-bindgen = "0.2.45"

# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. However, it is slower than the default
# allocator, so it's not enabled by default.
wee_alloc = { version = "0.4.2", optional = true }

# The `web-sys` crate allows you to interact with the various browser APIs,
# like the DOM.
[dependencies.web-sys]
version = "0.3.22"
features = ["console"]

# 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 it's only enabled
# in debug mode.
[target."cfg(debug_assertions)".dependencies]
console_error_panic_hook = "0.1.5"

# These crates are used for running unit tests.
[dev-dependencies]
wasm-bindgen-test = "0.2.45"
futures = "0.1.27"
js-sys = "0.3.22"
wasm-bindgen-futures = "0.3.22"

気になった部分だけ考察してゆく。

[profile.release]

ltoLLVM Link Time Optimization に関連する設定で true を指定すると自身だけでなく依存クレートすべてに対してコンパイル時の最適化を実行するとのこと。

[dependencies.web-sys]

Web API 用のクレートである web-sys の依存バージョンと関連する設定をまとめたテーブル。

この定義は Specifying Dependencies の Choosing features に解説されている。宣言的な設定だけであれば依存と一緒に定義されているほうがわかりやすくてよい。npm の package.json にもほしい機能だ。

features へ利用したい Web API を列挙する。設定項目の名前は Using web-sys の解説を読むに API かそれが属するものの名前を指定するようだ。console をそのまま指定しているのは Window.consoleconsole のどちらでも参照可能だから、ということなのだろうか?

WASM を Web ブラウザー限定にするなら自前で API を連携させるよりも web-sys へ任せるのが安全そうだ。

[target."cfg(debug_assertions)".dependencies]

これは Platform specific dependencies と呼ばれるもので cfg 属性に指定された特定のターゲット向け依存と設定を定義できる。これも面白い仕組みだ。

例えば cfg(windows) なら Windows (OS)、本サンプルの cfg(debug_assertions) はデバッグ用アサーションのみを対象とする。console_error_panic_hook は WASM 上の異常終了を console.error として出力するものなので開発時へ限定するためこのように設定しているのだろう。

lib.rs

全体像。

use wasm_bindgen::prelude::*;
use web_sys::console;


// When the `wee_alloc` feature is enabled, this uses `wee_alloc` as the global
// allocator.
//
// If you don't want to use `wee_alloc`, you can safely delete this.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;


// This is like the `main` function, except for JavaScript.
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
    // This provides better error messages in debug mode.
    // It's disabled in release mode so it doesn't bloat up the file size.
    #[cfg(debug_assertions)]
    console_error_panic_hook::set_once();


    // Your code goes here!
    console::log_1(&JsValue::from_str("Hello world! Sample!"));

    Ok(())
}

Quickstart と異なり公開する関数へ #[wasm_bindgen(start)] を指定。これはコメントに解説されているとおり C 言語などの main 関数的なエントリー ポイントとして WASM を参照した時点でそのまま実行される。

実際 pkg/index.js の処理は

import * as wasm from "./index_bg.wasm";
export * from "./index_bg.js";
wasm.__wbindgen_start();

となっており main_js を呼び出していない。再利用する関数として参照するなら Quickstart、処理がすべて WASM 完結するなら本サンプルのようにする。

ただし WASM はプラットフォーム中立な汎用ライブラリーを目的として選ばれることが多いだろうから、Quickstart のほうがサンプルとしては参考になるだろう。

まとめ

ざっくり 2 種類の WASM サンプルを試してみた。開発に必要な基本要素を把握したので、次は実用的な題材でなにか作ってみたい。

例えば過去に開発した npm icon-gen について PNG からプラットフォームごとのアイコン生成する部分だけ WASM 化するとか面白そうだ。これを Node.js だけでなく Swift や Kotlin からも呼べたら WASM のクロスプラットフォームな利点を味わうサンプルとして好ましい。

Copyright © 2009 - 2021 akabeko.me All Rights Reserved.