Rust: WebAssembly プログラミング
概要
WebAssembly は可搬性の高い仮想マシン用バイナリ命令フォーマットです。Rust はターゲットバイナリに WebAssembly を選択することができます。
Table of Contents
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 では
rustc
やcargo
のビルドターゲットに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
singlepass
とcranelift
は 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 run
やcargo test
はビルドした WebAssembly バイナリをネイティブ実行しようとして失敗してしまいます。これを wasmer 経由で実行させるために.cargo/config
ファイルを利用します。この設定ファイルでは cargo の実行に使用するインタープリタやエミュレータを
runner
に指定できます。.cargo/config
を編集してcargo run
やcargo test
でwasm32-wasi
をターゲットにしたときに wasmer コマンドが使用されるように設定します。[target.wasm32-wasi] runner = ["wasmer"] # 絶対パス/相対パスで明示的に指定しても良い; such as ["/home/torao/.cargo/bin/wasmer"]
上記のような
.cargo/config
ファイルを作成してcargo run
やcargo 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 をインストールできます (ソースのダウンロードとビルドに時間をかけたくなければバイナリを Installwasm-pack
からダウンロードすることもできます)。C:\Users\torao\git\hello-world>cargo 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>
C:\Users\torao\git\hello-world>cargo install miniserve
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()
機能が呼び出せていることを確認できます。この章で作成した実際の greeting WebAssembly プログラムは次のボタンで試すことができます。
Web ブラウザ向けの WebAssembly 開発は CLI の場合より手順が多く煩雑です。しかし高度な演算や暗号処理などのより複雑な処理を JavaScript より高速で安全な方法で実装することができます。