Contents
Wasm(WebAssembly)とは:ブラウザでの利点と代表的ユースケース
Wasm はブラウザで動作するバイナリ命令形式です。サンドボックス化された環境で効率良く計算を実行でき、数値演算や既存ネイティブ資産の再利用に向いています。ここでは代表的ユースケースと注意点を整理します。
代表的ユースケース
代表的なユースケースを簡潔に示します。
- CPU 集約処理:数値計算、物理シミュレーション、画像・音声処理
- 暗号処理や圧縮:クライアント側での暗号化/復号や圧縮処理
- 軽量な機械学習推論:ブラウザ上での推論ワークロード
- ゲームのコア処理:物理演算やパスファインディングなど
制約と考慮点
導入前に把握すべき主な制約を示します。
- JS↔Wasm の境界コストがあり短い関数呼び出しが多数あるケースでは不利です。
- DOM 操作は基本的に JS 側で行う方が効率的です。
- スレッドや SharedArrayBuffer、SIMD を使う場合はブラウザの対応とホスティング側のヘッダが必要です。
Rust + Wasm の採用判断チェックリストと代替案
ここでは Rust + Wasm を実務で採用するかの判断軸と代替案を示します。要件に応じてメリットとコストを比較してください。
採用チェックリスト
判断に使える主要ポイントを列挙します。
- 処理が明確にバッチ処理や大きな数値演算であるか
- JS↔Wasm の呼び出し回数が少なくまとまった処理を渡せるか
- 既存の Rust/C/C++ ライブラリを再利用したいか
- ビルドやデバッグの運用コストを許容できるか
- ブラウザ互換性やホスティング面の制約を受け入れられるか
代替案と推奨ワークフロー
目的別に推奨ワークフローを示します。
- 開発スピード重視:TypeScript/JavaScript を推奨します。
- クライアントで並列計算:Web Worker+JS、または Rust + Wasm(ただし SharedArrayBuffer 要件を確認)。
- サーバ移譲が可能:サーバサイド Rust / Go に処理を移す選択肢。
- 既存のネイティブ資産を活かす:C/C++→Wasm、または Rust ライブラリの移植。
- 静的配布(Node 不要):wasm-pack build --target web を利用。
- npm 配布やツリーシェイク:--target bundler を使い bundler と統合。
開発環境と最小プロジェクト設定(Rust + Wasm 向け)
ここでは必須ツールと最低限のプロジェクト構成を示します。動作に必要な依存と実行可能な最小構成を提供します。
必須ツールと推奨バージョン
導入時に用意すべき主要ツールを列挙します。
- Rust:rustup で最新の stable toolchain を使用することを推奨します。機能や最適化が改善されるためです。
- wasm32 ターゲット:wasm32-unknown-unknown を追加します。
- wasm-pack:ビルドとパッケージングに便利です(インストール方法は後述)。
- wasm-bindgen-cli:低レベルのバインディング生成や手動ワークフローで使用します。
- Binaryen(wasm-opt):最終的な .wasm の最適化に使用します。
- 簡易サーバ:basic-http-server や Python の http.server(ただし MIME を確認すること)。
- Node.js:必須ではありません。bundler や Node ターゲットを使う場合は最新の LTS(例: v18 / v20)を使用してください。
インストール手順の安全性
インストール時の安全対策や企業ポリシーへの配慮を示します。
- curl | sh のワンライナーは即実行せず、まずスクリプト内容を確認してください。
- 企業では内部ミラーやパッケージマネージャを推奨します。必要に応じてセキュリティチームの承認を得てください。
- wasm-pack は公式インストーラのほか、Homebrew や各 OS のパッケージを使う方法があります。
例(安全に実行する場合は手順を確認してから):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# rustup とターゲット rustup toolchain install stable rustup target add wasm32-unknown-unknown # wasm-pack は公式ページの手順かパッケージマネージャを利用 # 公式スクリプトを使う場合は事前に内容を確認してください # curl -sSfL https://rustwasm.github.io/wasm-pack/installer/init.sh | sh # wasm-bindgen-cli(必要な場合) cargo install -f wasm-bindgen-cli # 簡易サーバ(ローカル確認用) cargo install basic-http-server |
最小 Cargo.toml と依存例
実行可能な最小構成の例です。バージョンは例示です。各クレートの最新版を確認してください。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[package] name = "hello-wasm" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" js-sys = "0.3" web-sys = { version = "0.3", features = ["Window", "Request", "Response", "Fetch", "Console"] } console_error_panic_hook = "0.1" serde = { version = "1.0", features = ["derive"] } serde_wasm_bindgen = "0.5" [profile.release] opt-level = "z" lto = true codegen-units = 1 panic = "abort" |
必要な import と最小の Rust コード例
インポートと最小動作例です。fetch や非同期のブリッジで必要な use を含めています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen_futures::{future_to_promise, JsFuture}; use web_sys::window; // パニック時にブラウザのコンソールでスタックトレースを表示 #[wasm_bindgen(start)] pub fn start() { console_error_panic_hook::set_once(); } #[wasm_bindgen] pub fn greet(name: &str) -> String { format!("Hello, {}!", name) } // Promise を返す非同期ラッパーの例(future_to_promise を明示) #[wasm_bindgen] pub fn fetch_text_promise(url: String) -> js_sys::Promise { future_to_promise(async move { let w = window().ok_or_else(|| JsValue::from_str("no window"))?; let resp_value = JsFuture::from(w.fetch_with_str(&url)).await?; let resp: web_sys::Response = resp_value.dyn_into().map_err(|_| JsValue::from_str("not response"))?; let text_value = JsFuture::from(resp.text().unwrap()).await?; Ok(text_value) }) } |
最小ハンズオン:ビルドからブラウザでの実行まで
まずはローカルで最小の「Hello Wasm」を動かす流れを示します。Node が不要な静的配布ワークフローを中心に説明します。
クローンとビルド
リポジトリをクローンしてビルドする手順例です。実際のリポジトリ URL は適宜置き換えてください。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
git clone <サンプルリポジトリのURL> cd hello-wasm # ターゲット追加(既に追加済みの場合は不要) rustup target add wasm32-unknown-unknown # wasm-pack を使う場合(web ターゲットは静的配布向け) wasm-pack build --release --target web --out-dir pkg # 手動ワークフローの例 # cargo build --release --target wasm32-unknown-unknown # wasm-bindgen target/wasm32-unknown-unknown/release/hello-wasm.wasm --out-dir pkg --target web |
サーブと MIME チェック
サーブ時に .wasm が正しい MIME で配信されているか確認します。MIME が application/wasm でないと WebAssembly.instantiateStreaming は失敗します。まずはローカルサーバで確認してください。
|
1 2 3 4 5 |
# ローカル確認例 basic-http-server ./ -a 127.0.0.1:4000 # または python -m http.server 8080 |
.content-type を確認する方法(例):
|
1 2 3 |
curl -I http://127.0.0.1:4000/pkg/hello_wasm_bg.wasm # Expect: Content-Type: application/wasm |
サーバが正しく返さない場合は、WebAssembly.instantiateStreaming のフォールバックとして arrayBuffer を使って初期化してください(次節のコード参照)。
最小の HTML / JS による初期化
wasm-pack が生成する JS ラッパーを使う最小例です。
|
1 2 3 4 5 6 7 |
<script type="module"> import init, { greet } from './pkg/hello_wasm.js'; await init(); console.log(greet('Wasm')); </script> |
直接 WebAssembly.instantiate を呼ぶ場合は MIME を確認してフォールバック実装を用意してください。
JS連携・非同期処理とブラウザAPIの実務パターン
Rust と JS 間のデータ受け渡しや非同期処理のパターン、ブラウザ API 利用上の注意を示します。設計上のトレードオフも併記します。
データ受け渡し(JsValue / serde_wasm_bindgen)
構造化データを扱う際は serde_wasm_bindgen が便利です。小さな配列はコピーで良いことが多く、大きなバイナリはメモリ共有を検討します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; #[derive(Serialize, Deserialize)] pub struct Item { id: u32, name: String, } #[wasm_bindgen] pub fn process_item(val: JsValue) -> Result<JsValue, JsValue> { let item: Item = serde_wasm_bindgen::from_value(val) .map_err(|e| JsValue::from_str(&e.to_string()))?; serde_wasm_bindgen::to_value(&item).map_err(|e| JsValue::from_str(&e.to_string())) } |
非同期関数のエクスポートと future_to_promise
[wasm_bindgen] pub async fn のエクスポートは wasm-bindgen のバージョンやツールチェーンにより挙動が変わるため注意が必要です。互換性を重視する場合は明示的に future_to_promise を使う方法が安定します。一般的には wasm-bindgen の 0.2 系と wasm-bindgen-futures の 0.4 系で非同期ブリッジが整備されていますが、詳細は公式のリリースノートを確認してください。
async fn をそのまま使う例(ツールチェーン依存):
|
1 2 3 4 5 6 7 8 |
use wasm_bindgen::prelude::*; #[wasm_bindgen] pub async fn fetch_text_async(url: String) -> Result<String, JsValue> { // 実装は前述の fetch と同様 Ok("...".to_string()) } |
future_to_promise を使う例(互換性優先):
|
1 2 3 4 5 6 7 8 9 10 11 12 |
use wasm_bindgen::prelude::*; use wasm_bindgen_futures::future_to_promise; use wasm_bindgen_futures::JsFuture; #[wasm_bindgen] pub fn fetch_text_promise(url: String) -> js_sys::Promise { future_to_promise(async move { // 非同期処理を実行して JsValue を返す Ok(JsValue::from_str("text")) }) } |
ブラウザ API との橋渡し(設計上の注意)
web-sys を通じてブラウザ API にアクセスできますが、以下に注意してください。
- DOM 操作は JS 側に寄せると生産性が高いです。
- Closure::wrap を使う場合は不要になったら drop してリークを防いでください。
- dyn_into()/JsCast を用いて型変換します。例外処理は JsValue で行います。
デバッグ・最適化・デプロイ(実務ノウハウ)
開発中に遭遇しやすい問題の対処、最適化手順、実運用時のホスティング設定について実践的にまとめます。
デバッグとよくあるエラー
よくある障害と対処法を示します。
- パニックのトレース:console_error_panic_hook を導入するとブラウザで Rust のスタックトレースが見えます。
- ターゲット未追加:wasm32-unknown-unknown を追加しているか確認してください。
- wasm-pack 未インストール:必要に応じてインストールしてください。
- instantiateStreaming の失敗:MIME が application/wasm でないことが主原因です。フォールバックを用意してください。
- COOP/COEP 未設定:SharedArrayBuffer やスレッド利用時はヘッダ設定を確認してください。
例(スタートフックで panic hook を設定):
|
1 2 3 4 5 |
#[wasm_bindgen(start)] pub fn start() { console_error_panic_hook::set_once(); } |
instantiateStreaming と MIME 確認方法
instantiateStreaming は高速ですが Content-Type が正しく設定されている必要があります。ブラウザ互換性は高いものの、サーバ側設定ミスでエラーになります。開発時はヘッダを必ず確認してください。
- MIME 確認コマンド:
|
1 2 3 |
curl -I https://example.com/pkg/your_pkg_bg.wasm # Expect: Content-Type: application/wasm |
- JavaScript 側での安全な初期化パターン:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
async function loadWasm(url, importObject) { const resp = await fetch(url); const ct = resp.headers.get('content-type') || ''; if (WebAssembly.instantiateStreaming && ct.includes('application/wasm')) { try { return await WebAssembly.instantiateStreaming(resp, importObject); } catch { const bytes = await resp.arrayBuffer(); return await WebAssembly.instantiate(bytes, importObject); } } else { const bytes = await resp.arrayBuffer(); return await WebAssembly.instantiate(bytes, importObject); } } |
最適化(サイズと性能)
実務的な最適化手順を示します。
- Cargo の release ビルドに LTO/codegen-units/panic="abort" を設定する。
- wasm-pack build --release を実行する。
- Binaryen の wasm-opt を用いてさらに縮小する。
- 例: wasm-opt -Oz pkg/your_pkg_bg.wasm -o pkg/your_pkg_bg_opt.wasm
- 境界コストを考慮したベンチ設計を行う。短いコールが多数あるケースでは効果が薄いです。
デプロイとホスティング実務
ホスティングごとの実務上の注意と具体例を示します。特に COOP/COEP や MIME、CORP/CORS に注意してください。
- S3 + CloudFront:
- S3 オブジェクトの Content-Type を application/wasm に設定する。
- CloudFront の Response Headers Policy で COOP/COEP を付与する。
- Netlify:
- publish ルートに _headers を置いてヘッダを付与できます。ワイルドカードもサポートされます。
- 例(publish ディレクトリ直下に _headers を置く):
|
1 2 3 4 5 6 7 |
/pkg/*.wasm Content-Type: application/wasm /* Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp |
- Cloudflare Pages:
- Cloudflare Pages はビルド出力に _headers を置く形でカスタムヘッダを設定できます。
- ただし設定方法は Pages のバージョンや UI によるため、公式ドキュメントで確認してください。
- GitHub Pages:
- GitHub Pages では任意のレスポンスヘッダを設定できない場合があります。COOP/COEP が必須の機能は動作しない可能性が高いです。
- 回避策として Cloudflare や Netlify をフロントに置いてヘッダを付与する方法があります。
ホスティング上で外部リソースを利用する場合は Cross-Origin-Resource-Policy(CORP)や CORS の整合性も確認してください。COEP: require-corp を設定すると、外部リソースは CORP ヘッダを持っているか同一オリジンである必要があります。
よくある実務質問(FAQ)
-
Q: Wasm は常に JS より高速ですか?
A: 常に高速とは限りません。大きなバッチ処理で有利ですが、短い呼び出しが多数ある場合は境界コストで遅くなる場合があります。 -
Q: 非同期関数を export する最良の方法は?
A: wasm-bindgen の async サポートはバージョン依存です。互換性を優先するなら future_to_promise を明示的に使うことを推奨します。 -
Q: SharedArrayBuffer を使いたいが動かない。原因は?
A: COOP/COEP によるクロスオリジン隔離が必要です。ホスティングでヘッダが正しく付与されているか確認してください。
参考(公式ドキュメントとサンプル)
主要ドキュメントやツールの公式ページを参照してください。バージョンや手順は各公式の最新版を確認することをおすすめします。
-
MDN: Rust から WebAssembly にコンパイル(日本語)
https://developer.mozilla.org/ja/docs/WebAssembly/Guides/Rust_to_Wasm -
rustwasm book(公式ガイド) — https://rustwasm.github.io/book/
- wasm-bindgen(公式) — https://rustwasm.github.io/wasm-bindgen/
- wasm-pack(公式) — https://rustwasm.github.io/wasm-pack/
- Binaryen(wasm-opt) — https://github.com/WebAssembly/binaryen
まとめ
- Rust + Wasm は計算集約処理や既存ネイティブ資産の再利用に適しています。
- 採用判断は処理特性(バッチかコール頻度か)と運用コストで行ってください。
- 開発環境は rustup による最新 stable、wasm32 ターゲット、wasm-pack/wasm-bindgen、wasm-opt を揃えると実務運用がしやすくなります。
- 非同期は future_to_promise を明示する方法が互換性で安全です。
- デプロイでは .wasm の Content-Type、COOP/COEP、CORP/CORS の設定を必ず確認してください。