Rust: WebAssembly プログラミング

Takami Torao Rust 1.70 wasmer 4.0 #Rust #WebAssembly #WASM #wasmer
  • このエントリーをはてなブックマークに追加

概要

WebAssembly は可搬性の高い仮想マシン用バイナリ命令フォーマットです。Rust はターゲットバイナリに WebAssembly を選択することができます。

Table of Contents

  1. 概要
  2. hello, world (CLI 環境 / wasmer)
  3. hello, world (Web ブラウザ環境 / wasm-pack)
  4. 参考文献
C:\Users\torao\git>rustc --version
rustc 1.70.0 (90c541806 2023-05-31)

hello, world (CLI 環境 / wasmer)

この章では Rust で実装した "hello, world" コマンドを WebAssembly で実行して WASM 開発に必要な手順を確認します。

wasmer は WebAssembly ランタイムの一つで WASI (WebAssembly System Interface) をターゲットにした WASM バイナリを実行することができます。WASI は WASM アプリケーションからシステムリソースにアクセスするための標準的なインターフェースです。したがって一般的な CLI アプリケーションとして動作する Rust ソースコードは、WASI をターゲットとする WebAssembly アプリケーションとして (ほぼ) そのままビルドすることができます。一方で Web ブラウザなどの制約のある環境では WASI がサポートされていません。

wasmer と wasm32-wasi ターゲットのインストール

Rust では rustccargo のビルドターゲットに wasm32-wasi を指定するだけで wasmer で実行できる WebAssembly バイナリを作成することができます。wasm32-wasi ターゲットはデフォルトではインストールされていないため rustup で追加します。

C:\Users\torao\git>rustup target add wasm32-wasi
info: downloading component 'rust-std' for 'wasm32-wasi'
info: installing component 'rust-std' for 'wasm32-wasi'

wasmer コマンド自体はいくつかの方法でインストールできますが、ここでは Rust を前提としているため cargo 経由でインストールする方法が簡単です。

C:\Users\torao\git>cargo install wasmer-cli --features singlepass,cranelift
    Updating crates.io index
  Downloaded wasmer-cli v4.0.0
  Downloaded 1 crate (136.1 KB) in 1.53s
  Installing wasmer-cli v4.0.0
    Updating crates.io index
  Downloaded equivalent v1.0.0
  ...
  Compiling wasmer-wast v4.0.0
    Finished release [optimized] target(s) in 3m 20s
  Installing C:\Users\torao\.cargo\bin\wasmer.exe
   Installed package `wasmer-cli v4.0.0` (executable `wasmer.exe`)
C:\Users\torao\git>wasmer --version
wasmer 4.0.0

singlepasscranelift は wasmer で使用するネイティブバックエンドです。llvm を追加することもできますが、大量のソースダウンロードとビルドでインストールに時間がかかるため試すだけであれば不要です。

hello, world プログラムの作成

cargo init で新しいプロジェクトを作成し "hello, world" と表示するだけの Rust プログラムとそのテストケースを記述します。

fn main() {
    println!("{}", greet());
}

fn greet() -> &'static str {
    return "hello, world";
}

#[cfg(test)]
mod test {
    #[test]
    fn test_greet() {
        assert_eq!(super::greet(), "hello, world");
    }
}

このプロジェクトをビルドして実行すると "hello, world" と表示されます。

C:\Users\torao\git\hello-world>cargo run
hello, world
C:\Users\torao\git\hello-world>cargo test
Compiling hello-world v0.1.0 (C:\Users\torao\git\hello-world)
    Finished test [unoptimized + debuginfo] target(s) in 0.24s
     Running unittests src\main.rs (target\debug\deps\hello_world-52f4bcb99bd5a8c9.exe)

running 1 test
test test::test_greet ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
WebAssembly での実行とテスト

この hello, world プログラムを WebAssembly にコンパイルするには --target wasm32-wasi を付けるだけです。出力された WebAssembly バイナリは wasmer で実行できます。

C:\Users\torao\git\hello-world>cargo build --target wasm32-wasi
   Compiling hello-world v0.1.0 (C:\Users\torao\git\hello-world)
    Finished dev [unoptimized + debuginfo] target(s) in 0.15s
C:\Users\torao\git\hello-world>wasmer run target\wasm32-wasi\debug\hello-world.wasm
hello, world

デフォルトではバックエンドに singlepass が使用されますが --cranelift オプションを付けることで cranelift を選択することもできます。

さて、--target wasm32-wasi オプションを付けた cargo runcargo test はビルドした WebAssembly バイナリをネイティブ実行しようとして失敗してしまいます。これを wasmer 経由で実行させるために .cargo/config ファイルを利用します。

この設定ファイルでは cargo の実行に使用するインタープリタやエミュレータを runner に指定できます。.cargo/config を編集して cargo runcargo testwasm32-wasi をターゲットにしたときに wasmer コマンドが使用されるように設定します。

[target.wasm32-wasi]
runner = ["wasmer"]    # 絶対パス/相対パスで明示的に指定しても良い; such as ["/home/torao/.cargo/bin/wasmer"]

上記のような .cargo/config ファイルを作成して cargo runcargo test を実行すると WebAssembly バイナリにビルドしたコマンドやテストを wasmer 上で実行することができます。

C:\Users\torao\git\hello-world>cargo run --target wasm32-wasi
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `wasmer target\wasm32-wasi\debug\hello-world.wasm`
hello, world
C:\Users\torao\git\hello-world>cargo test --target wasm32-wasi
   Compiling hello-world v0.1.0 (C:\Users\torao\git\hello-world)
    Finished test [unoptimized + debuginfo] target(s) in 0.18s
     Running unittests src\main.rs (target\wasm32-wasi\debug\deps\hello_world-735f5832ea9e63c1.wasm)

running 1 test
test test::test_greet ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

このように、WASI をターゲットとして wasmer で動作させることによって、通常の CLI アプリケーションとして実装した Rust プロジェクトから WebAssembly バイナリを生成し実行やテストを行うことができます。

hello, world (Web ブラウザ環境 / wasm-pack)

wasmer で実行する WASM アプリケーションは WASI によって自由度の高い動作ができますが、そのような WebAssembly バイナリは Web ブラウザなどのより制約の厳しい環境では利用できません。Web ブラウザ向けには JavaScript から呼び出すことのできる関数のライブラリを実装することになります。この章では Web ブラウザの JavaScript から呼び出し可能な WASM プログラムを Rust で実装する方法を示します。

wasm-pack と wasm32-unknown-unknown ターゲットのインストール

Web ブラウザ向けの WebAssembly バイナリは wasm32-unknown-unknown をターゲットにします。rustup でこのターゲットを追加します。

C:\Users\torao\git>rustup target add wasm32-unknown-unknown
info: downloading component 'rust-std' for 'wasm32-unknown-unknown'
info: installing component 'rust-std' for 'wasm32-unknown-unknown'

また cargo の代わりにビルドを行う wasm-pack もインストールします。wasm-pack は Rust で記述された WebAssembly プロジェクトをビルドしパッケージ化して公開するためのツールです。cargo install wasm-pack を実行して wasm-pack をインストールできます (ソースのダウンロードとビルドに時間をかけたくなければバイナリを Install wasm-pack からダウンロードすることもできます)。

    Updating crates.io index
  Installing wasm-pack v0.12.0
  Downloaded time-core v0.1.1
  ...
   Compiling wasm-pack v0.12.0
    Finished release [optimized] target(s) in 36.69s
  Installing C:\Users\torao\.cargo\bin\wasm-pack.exe
  Installed package `wasm-pack v0.12.0` (executable `wasm-pack.exe`)
C:\Users\torao\git\hello-world>wasm-pack -h
📦 ✨  pack and publish your wasm!

Usage: wasm-pack [OPTIONS] <COMMAND>

Commands:
  build    🏗️  build your npm package!
  pack     🍱  create a tar of your npm package but don't publish!
  new      🐑  create a new project with a template
  publish  🎆  pack up your npm package and publish!
  login    👤  Add an npm registry user account! (aliases: adduser, add-user)
  test     👩‍🔬  test your wasm!
  help     Print this message or the help of the given subcommand(s)

Options:
  -v, --verbose...             Log verbosity is based off the number of v used
  -q, --quiet                  No output printed to stdout
      --log-level <LOG_LEVEL>  The maximum level of messages that should be logged by wasm-pack. [possible values: info, warn, error] [default: info]
      -h, --help                   Print help
hello, world プロジェクトの作成

cargo new でライブラリを指定して新しい hello, world プロジェクトを作成します。

C:\Users\torao\git>cargo new --lib hello-world
     Created library `hello-world` package
C:\Users\torao\git>cd hello-world

Web ブラウザ向けの WebAssembly を開発するために Cargo.toml を次のように修正します。必要な変更は 1) 依存関係に wasm-bindgen を追加することと、2) ライブラリのタイプを cdylib にすることです。

[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"

[dependencies]
wasm-bindgen = "0.2"      # ✅

rand = "0.8"
getrandom = { version = "0.2", features = ["js"] }
tinymt = "1.0"

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

ここでは hello, world の例で乱数を使用したいため rand, genrandom, tinymt の依存を追加しています。実際の hello, world プログラムは次のように実装しました。

use wasm_bindgen::prelude::*;
use tinymt::TinyMT32;
use rand::RngCore;

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

#[wasm_bindgen]
pub fn greeting(seed: &str) -> String {
  let seed = seed.chars().fold(0u32, |xor, ch| xor ^ (ch as u32));
  let mut random = TinyMT32::from_seed_u32(seed);
  let message = format!("hello, world: {:04X}", random.next_u32());
  alert(&message);
  return message;
}

この greeting() 関数は、指定されたシードに基づく乱数をメッセージに追加し、Web ブラウザの alert() を使って表示してメッセージを返します。

プロジェクトのビルド

wasm-pack build でビルドします。

C:\Users\torao\git\hello-world>wasm-pack build --target web -d docroot\js
[INFO]: 🎯  Checking for the Wasm target...
[INFO]: 🌀  Compiling to Wasm...
   Compiling proc-macro2 v1.0.63
   ...
   Compiling tinymt v1.0.9
   Compiling hello-world v0.1.0 (C:\Users\torao\git\hello-world)
    Finished release [optimized] target(s) in 7.33s
[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 7.75s
[INFO]: 📦   Your wasm pkg is ready to publish at C:\Users\torao\git\hello-world\docroot\js.
Web ブラウザでの実行

Web ブラウザの JavaScript で greeting() 関数を呼び出すために、次のような簡単な HTML を docroot/ ディレクトリの下に作成して簡易 Web サーバの miniserve で表示します。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>

<script type="module">
  import init, { greeting } from "./js/hello_world.js";
  (async () => {
    await init();
    const msg = greeting(new Date().toString());
    document.write(msg);
  })();
</script>

</body>
</html>
    Updating crates.io index
  Downloaded miniserve v0.23.2
  Downloaded 1 crate (151.1 KB) in 1.54s
  Installing miniserve v0.23.2
    Updating crates.io index
  Downloaded actix-utils v3.0.1
  ...
  Compiling actix-multipart v0.5.0
    Finished release [optimized] target(s) in 1m 15s
  Installing C:\Users\torao\.cargo\bin\miniserve.exe
   Installed package `miniserve v0.23.2` (executable `miniserve.exe`)
C:\Users\torao\git\hello-world>miniserve docroot
miniserve v0.23.2
Bound to [::]:8080, 0.0.0.0:8080
Serving path \\?\C:\Users\torao\git\hello-world\hello-world\docroot
Available at (non-exhaustive list):
    http://127.0.0.1:8080
    http://172.25.240.1:8080
    http://192.168.9.172:8080
    http://192.168.56.1:8080
    http://[::1]:8080
    http://[2409:10:a220:5300:4f31:e1cb:c2c:1dd9]:8080
    http://[2409:10:a220:5300:7deb:c791:2d53:3b4a]:8080

上記のスクリプトは hello_world.js をインポートしていますが、このファイルは呼び出しを簡単にするために wasm-pack によって生成されたラッパーです。greeting() 処理の実体は WebAssembly として実行されています。

Fig 1 はこの index.html を Web ブラウザで表示した結果です。リロードするたびにメッセージに付属する16進数がランダムに変わり、また Rust コード内で使用した alert() 機能が呼び出せていることを確認できます。

Fig 1. hello, world WebAssembly をブラウザで実行した結果。Rust で記述した alert() が実行されていることが分かる。

この章で作成した実際の greeting WebAssembly プログラムは次のボタンで試すことができます。

。oO( ... )

Web ブラウザ向けの WebAssembly 開発は CLI の場合より手順が多く煩雑です。しかし高度な演算や暗号処理などのより複雑な処理を JavaScript より高速で安全な方法で実装することができます。

参考文献

  1. Rust 🦀 and WebAssembly 🕸