RustとWebAssembly
WebAssembly(よくWasmと略されます)を使うと、Rust などの言語で書いたコードを、Web ブラウザの中で JavaScript と並べて動かせます。ブラウザに積まれた2つ目のエンジンだと考えてください。ページを動かすのは今までどおり JavaScript ですが、画像フィルター、物理演算、大きなファイルの解析といった重い処理は、コンパイル済みの速い Rust のコードに任せられます。
このページでは、小さな Rust のライブラリを書いて Wasm にコンパイルし、Web ページから呼び出します。ここにあるものは Rust Playground では動きません(ブラウザと追加のツールが必要です)。なので、ほとんどの例は自分のマシンで試すためのふつうのコードブロックです。ただし、純粋な Rust の部分はすぐに試せます。
WebAssembly とは何か、なぜ Rust と相性がいいのか
Section titled “WebAssembly とは何か、なぜ Rust と相性がいいのか”WebAssembly は、プログラムのためのコンパクトなバイナリ形式です。主要なブラウザはどれもこれを実行でき、JavaScript と同じセキュリティのサンドボックスの中で、ネイティブに近い速さで動きます。ブラウザ専用というわけではなく、サーバーやエッジのプラットフォームでも Wasm が動いています。
Rust は Wasm でとても人気のある言語の1つです。理由はいくつかあります。
- ガベージコレクタや大きなランタイムがない。 Rust はコードと一緒に余計なしくみを配布する必要がないので、
.wasmファイルを小さくできます。 - ツールがすばらしい。
wasm-bindgenが Rust と JavaScript をつなぐ「のり」のコードを書いてくれて、wasm-packが1つのコマンドですべてをビルドしてくれます。 - 安全で速い。 デスクトップで得られるのと同じメモリ安全性とパフォーマンスが手に入ります。
ツールを準備する
Section titled “ツールを準備する”まず、Rust に Wasm のターゲット向けにコンパイルできるよう伝えます。ターゲットとは「コードが動くマシンの種類」のことです。wasm32-unknown-unknown は「32ビットの Wasm で、特定の OS はなし」という意味です。
rustup target add wasm32-unknown-unknown次に wasm-pack をインストールします。wasm-pack はクレートをコンパイルし、wasm-bindgen を実行して、必要なものをすべて1つのフォルダにまとめてくれます。公式のインストーラーを使うか、Cargo でビルドしてください。
# wasm-pack のサイトにある Linux / macOS 向けインストーラーcurl https://wasm-bindgen.github.io/wasm-pack/installer/init.sh -sSf | sh
# または、Rust が入っていればどのシステムでもcargo install wasm-pack --lockedWasm 用の Rust ライブラリを書く
Section titled “Wasm 用の Rust ライブラリを書く”Wasm のモジュールは、main を持つプログラムではなくライブラリです。ライブラリを作りましょう。
cargo new --lib hello-wasmcd hello-wasm次に Cargo.toml を編集します。crate-type の行は、.wasm に向いたライブラリ(cdylib)を作るよう Cargo に伝えます。wasm-bindgen は Rust と JavaScript をつなぐクレートです。
[package]name = "hello-wasm"version = "0.1.0"edition = "2021"
[lib]crate-type = ["cdylib", "rlib"]
[dependencies]wasm-bindgen = "0.2"それでは、src/lib.rs に関数を2つ書きます。#[wasm_bindgen] 属性は、それぞれの関数に「JavaScript から呼び出せる」という印を付けます。
use wasm_bindgen::prelude::*;
#[wasm_bindgen]pub fn add(a: i32, b: i32) -> i32 { a + b}
#[wasm_bindgen]pub fn greet(name: &str) -> String { format!("こんにちは、{name}さん!この文字列はRustで作られました。")}必要な Rust のコードはこれだけです。どちらもふつうの関数だという点に注目してください。JavaScript の文字列を &str に変換したり、返した String を JavaScript の文字列に戻したりするのは、wasm-bindgen がやってくれます。
まずはネイティブでロジックをテストする
Section titled “まずはネイティブでロジックをテストする”ここで、時間を大きく節約できる習慣を紹介します。実際の処理はふつうの Rust の関数に書いておき、いつものコンピュータで cargo test やかんたんな main を使ってテストするのです。ブラウザでのデバッグは、ターミナルでのデバッグより時間がかかります。属性を外した同じロジックは、どこでも動きます。
fn add(a: i32, b: i32) -> i32 { a + b}
fn greet(name: &str) -> String { format!("こんにちは、{name}さん!この文字列はRustで作られました。")}
fn main() { println!("{}", greet("Ferris")); println!("2 + 3 = {}", add(2, 3));}実行結果:
こんにちは、Ferrisさん!この文字列はRustで作られました。2 + 3 = 5ロジックが正しく動けば、#[wasm_bindgen] のラッパーは、ブラウザへの薄い扉にすぎません。(#[test] を付けたテストも、cargo test でネイティブに実行できます。テストを見てください。)
wasm-pack でビルドする
Section titled “wasm-pack でビルドする”クレートのフォルダで、次を実行します。
wasm-pack build --target web--target web は「webpack のようなバンドラーなしで、ブラウザが <script type="module"> タグで直接読み込めるファイルを作る」という意味です。私たちが実行したときは、新しくできた pkg/ フォルダに次のファイルが入っていました。
pkg/├── hello_wasm_bg.wasm コンパイルされた Rust のコード├── hello_wasm.js それを読み込み、各関数を包む JavaScript の「のり」├── hello_wasm.d.ts 関数の TypeScript の型├── hello_wasm_bg.wasm.d.ts└── package.jsonhello-wasm のダッシュは、ファイル名ではアンダースコアになっています。wasm-pack build はデフォルトでリリース(最適化された)ビルドを作ります。試している間は、--dev を付けると、最適化なしでより速くビルドできます。
JavaScript から呼び出す
Section titled “JavaScript から呼び出す”Cargo.toml と同じ場所に index.html を作ります(pkg/ がその隣に来るようにします)。
<!doctype html><html lang="ja"><head> <meta charset="utf-8"> <title>Rust からこんにちは</title></head><body> <p id="message">読み込み中…</p>
<script type="module"> import init, { add, greet } from "./pkg/hello_wasm.js";
await init(); // .wasm ファイルをダウンロードして起動する
const text = greet("Ferris"); const sum = add(2, 3); document.getElementById("message").textContent = `${text} 2 + 3 = ${sum}`; </script></body></html>デフォルトエクスポートの init() が、.wasm ファイルをダウンロードして起動します。その後は、add も greet もふつうの JavaScript の関数として使えます。
ブラウザは file:// のアドレスからはモジュールを読み込まないので、小さなローカル Web サーバーが必要です。Python が入っているなら、これがいちばん手軽です。
python3 -m http.server 8080ブラウザで http://localhost:8080 を開きます。私たちが試したときは、ページにこう表示されました。
こんにちは、Ferrisさん!この文字列はRustで作られました。 2 + 3 = 5この文は、ブラウザの中で動く Rust が作ったものです。🦀
境界を安く越えられるもの、そうでないもの
Section titled “境界を安く越えられるもの、そうでないもの”Rust と JavaScript は、同じやり方でメモリを共有しているわけではありません。Wasm のモジュールは自分専用のメモリのかたまりを持っていて、値は2つの側の間を行き来する必要があります。タダで済む行き来もあれば、コピーのコストがかかるものもあります。
| Rust での型 | JavaScript での型 | コスト |
|---|---|---|
i32、u32、f64、bool |
number、boolean |
安い:そのまま渡される |
&str、String |
string |
コピーに加えて、UTF-16 から UTF-8 への変換(またはその逆) |
&[u8]、Vec<u8> |
Uint8Array |
Wasm のメモリへの(またはメモリからの)コピー |
&mut [u8] |
Uint8Array |
コピーして渡し、呼び出しの後でコピーして戻す |
なので、鉄則は呼び出しは少なく、1回でたくさんの仕事をさせることです。画像全体を処理するために Rust を1回呼ぶのは速いです。ピクセルごとに1回ずつ、100万回 Rust を呼ぶと、時間のほとんどが境界を越えることに使われてしまいます。
実際の例を見てみましょう。次の関数は、ピクセルの値のバッファ全体を、1回の呼び出しで明るくします。
use wasm_bindgen::prelude::*;
/// すべてのピクセルを明るくする。JS からは Uint8Array が渡される#[wasm_bindgen]pub fn brighten(pixels: &mut [u8], amount: u8) { for p in pixels.iter_mut() { *p = p.saturating_add(amount); }}JavaScript からは、await init() の後で Uint8Array を渡して brighten(pixels, 20) と呼ぶと、配列がその場で更新されます。ループ自体は純粋な Rust なので、ネイティブでテストできます。
fn brighten(pixels: &mut [u8], amount: u8) { for p in pixels.iter_mut() { // saturating_add はオーバーフローせず、255 で止まる *p = p.saturating_add(amount); }}
fn main() { let mut pixels = [10, 100, 250]; brighten(&mut pixels, 20); println!("{pixels:?}");}実行結果:
[30, 120, 255]250 + 20 がクラッシュも折り返しもせず、255 になっている点に注目してください。saturating_add は最大値で止まります。色を扱うときには、まさにこれが欲しい動きです。
.wasm ファイルを小さく保つ
Section titled “.wasm ファイルを小さく保つ”ページが .wasm ファイルを使えるようになるには、その全バイトをダウンロードしなければなりません。いくつかコツを紹介します。
- 配布するのは必ずリリースビルドにする。 私たちの小さな例では、
wasm-pack build --devで作った.wasmファイルは 58,591 バイト、デフォルトのリリースビルドは 21,530 バイトでした。(wasm-optをオフにした私たちのマシンでの数字です。あなたの環境では変わります。) - wasm-pack に
wasm-optを実行させる。 リリースモードでは、wasm-pack がデフォルトで Binaryen の最適化ツールwasm-optを出力にかけてくれます。たいていは、さらに小さくなります。 - サーバーで圧縮を有効にする。 21,530 バイトのファイルは、gzip をかけると 8,464 バイトになりました。
- サイズを優先する設定を試して、計測しましょう。
[profile.release]opt-level = "z" # サイズを優先して最適化する("s" はもう少し控えめ)lto = truecodegen-units = 1自分をごまかさずに、ちゃんと計測しましょう。関数2つだけの私たちの例では、この設定は効果がありませんでした。ファイルは 22,817 バイトになり、少し大きくなったのです。こうした設定は、もっと大きなクレートで効いてくることが多いです。また、format! やパニックのメッセージもコードを引き込むので、それらを避けたクレートはずっと小さくなることがあります。
Wasm の実際の使われ方
Section titled “Wasm の実際の使われ方”いくつかの例を、それぞれの会社自身が書いた記事と一緒に紹介します(記事はどれも英語です)。
- Figma は C++ で書かれたデザインエディタを WebAssembly にコンパイルし、読み込み時間が3分の1になったと報告しています。(Rust ではありませんが、大きなアプリで Wasm に何ができるかがわかります。)
- Cloudflare Workers は
workers-rsクレートを通じて Rust をサポートしています。Rust のコードは Wasm にコンパイルされ、世界中にある Cloudflare のサーバーで動きます。 - Shopify Functions はストアをカスタマイズする WebAssembly モジュールで、Shopify はそのための最もパフォーマンスの高い言語として Rust を勧めています。
- 1Password のブラウザ拡張機能は、ページへの入力処理を WebAssembly に移し、少なくとも2倍速くなったと報告しています。
よくある間違い
Section titled “よくある間違い”crate-type = ["cdylib"] を忘れる。 これがないと、wasm-pack はすぐに止まってしまいます。うれしいことに、何を追加すればいいかをエラーがそのまま教えてくれます。
Error: crate-type must be cdylib to compile to WebAssembly. Add the following to your Cargo.toml file:
[lib]crate-type = ["cdylib", "rlib"]「WebAssembly にコンパイルするには crate-type が cdylib でなければならない。Cargo.toml に次を追加してください」という意味です。
index.html をダブルクリックして開く。 ページは「読み込み中…」のまま変わらず、ブラウザのコンソール(Chrome の場合)にはこう表示されます。
Access to script at 'file:///…/pkg/hello_wasm.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: chrome, chrome-extension, chrome-untrusted, data, http, https, isolated-app.これは、file:// のスクリプトの読み込みが CORS ポリシーによってブロックされた、という意味です。直し方は、python3 -m http.server のようなローカル Web サーバーでフォルダを配信し、代わりに http://localhost のアドレスを開くことです。
await init() の前に関数を呼ぶ。 Wasm のモジュールがまだ読み込まれていないので、「のり」のコードには呼び出す相手がありません。私たちが init() の行を消してみたところ、Chrome には Cannot read properties of undefined (reading '__wbindgen_free') と表示されました。必ず最初に await init() しましょう。
次のステップ:Rust で Web アプリ全体を作る
Section titled “次のステップ:Rust で Web アプリ全体を作る”JavaScript から Rust の関数をいくつか呼ぶのは、すばらしい第一歩です。Web アプリ全体を Rust で作りたいなら、次のフレームワークを見てみてください。どちらも Wasm にコンパイルされ、React のような感覚で、コンポーネントを Rust で書けます。
- WebAssembly は、ブラウザ(やサーバー)が実行できる、速くて安全なバイナリ形式です。Rust はガベージコレクタがないので、Wasm にうまくコンパイルできます。
rustup target add wasm32-unknown-unknownでターゲットを追加し、wasm-packをインストールします。- 関数に
#[wasm_bindgen]を付け、crate-type = ["cdylib", "rlib"]を設定して、wasm-pack build --target webでビルドします。 - JavaScript では、まず
await init()してから関数を呼びます。ページはfile://ではなくhttp://で配信しましょう。 - 数値は境界を安く越えられますが、文字列やスライスはコピーされます。だから、呼び出しは少なく、1回で大きな仕事をさせましょう。
- 配布するのは圧縮したリリースビルドにして、サイズの調整は計測してから信じましょう。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. ブラウザ向け WebAssembly の Rust ターゲットを追加するコマンドはどれですか?
2. #[wasm_bindgen] 属性は何をしますか?
3. ページが「読み込み中…」のまま変わらず、コンソールに file:/// のアドレスについての CORS エラーが出ています。どう直しますか?
4. JavaScript から Rust に渡すとき、いちばん安いのはどれですか?
スコア: 0 / 4
さらに詳しく
Section titled “さらに詳しく”- Rust から WebAssembly にコンパイル — MDN:wasm-pack のプロジェクトを作って動かすまでを、1ステップずつ説明するチュートリアルです。
- The wasm-bindgen Guide(英語):対応しているすべての型、ブラウザの API の呼び出し方、たくさんの例があります。
- wasm-pack のドキュメント(英語):
buildのすべてのオプション、ターゲット、プロファイルについて。 - wasm32-unknown-unknown — The rustc book(英語):このターゲットが何に対応しているか、そしてその制限について。
- WebAssembly — MDN:Rust とは関係なく、Wasm とは何か、ブラウザがそれをどう実行するかを説明しています。