スキップしてメイン コンテンツに移動

WASM(Rust) チュートリアル

Rust + WASM チュートリアル
チュートリアル
⏱ 1〜2 時間
Claude に作ってもらったチュートリアルがとても分かりやすかったので、共有します。 1~2 時間とありますが、実際には 15 分くらいで終わります。

Rust + WebAssembly
実践チュートリアル

Rust のコードをブラウザで動かす WASM の基本を、ゼロから実装しながら学ぶチュートリアルです。
wasm-bindgen を使った JS ↔ Rust のデータ受け渡しから DOM 操作、ミニプロジェクトまで一気に体験できます。

難易度:初〜中級
所要時間:1〜2 時間
前提:Rust の基礎知識
00

概要・仕組み

WebAssembly(WASM)はブラウザ上で動くバイナリフォーマットの仮想命令セットです。 Rust は WASM への公式サポートが充実しており、wasm-bindgen というクレートを使うことで JavaScript と Rust のコードを簡単に連携できます。

全体のデータフロー

Rust コード
src/lib.rs
wasm-pack build
コンパイル
.wasm ファイル
バイナリ
JavaScript
import して呼ぶ

登場する主要ツール・クレート

名前役割
wasm-packRust → WASM のビルドツール。npm パッケージとして出力もできる
wasm-bindgenJS と Rust の型変換・バインディングを自動生成するクレート
web-sysWeb API(DOM、fetch、canvas 等)の Rust バインディング集
js-sysJS の組み込みオブジェクト(Array、Promise 等)の Rust バインディング
ℹ️
Rust 経験者へ

このチュートリアルでは Rust の基礎構文の説明は省略し、WASM 固有の概念・ツールの使い方に集中します。 #[wasm_bindgen] マクロや web-sys の使い方など WASM ならではの部分を重点的に解説します。


Step 01

環境構築

まず必要なツールをインストールします。rustup は既にある前提です。

① wasm-pack のインストール

Rust を WASM にビルドして npm パッケージとして出力してくれる公式ツールです。

shell
$ cargo install wasm-pack

# インストール確認
$ wasm-pack --version
wasm-pack 0.13.1

② WASM ターゲットの追加

wasm-pack が内部的に使いますが、念のため手動でも追加しておきましょう。

shell
$ rustup target add wasm32-unknown-unknown

③ Node.js と http-server

ビルドした WASM を動かすローカルサーバーが必要です。

shell
# Node.js が入っているか確認(18 以上推奨)
$ node --version

# 軽量な静的サーバーをグローバルインストール
$ npm install -g http-server
⚠️
なぜサーバーが必要?

WASM ファイルは file:// プロトコルでは CORS エラーで読み込めません。 必ず http://localhost から配信する必要があります。

✓ 環境構築完了!次は実際にプロジェクトを作ります

Step 02

プロジェクト作成

① プロジェクトの雛形を生成

shell
$ wasm-pack new hello-wasm
$ cd hello-wasm

生成されるディレクトリ構造:

hello-wasm/ ├── Cargo.toml # クレートの設定・依存関係 ├── src/ │ └── lib.rs # Rust のメインソース(lib クレートとして作る) ├── tests/ │ └── web.rs # ブラウザ上でのテスト └── .gitignore

② Cargo.toml の確認と編集

生成された Cargo.toml を確認し、後で使う web-sys を追加しておきます。

toml Cargo.toml
[package]
name    = "hello-wasm"
version = "0.1.0"
edition = "2021"

# ★ WASM 用には必ず crate-type = ["cdylib"] を指定する
[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2"

# web-sys: DOM・ブラウザ API にアクセスするためのクレート
# features に使いたい API を列挙する(tree shaking のため)
[dependencies.web-sys]
version  = "0.3"
features = [
  "console",       # console.log を使うため
  "Window",         # window オブジェクト
  "Document",       # document オブジェクト
  "Element",         # DOM 要素
  "HtmlElement",    # HTML 要素の型
  "HtmlButtonElement",
  "HtmlInputElement",
  "EventTarget",     # イベントリスナーの登録
  "MouseEvent",      # クリックイベント
  "Node",            # appendChild 等
]

[dev-dependencies]
wasm-bindgen-test = "0.3"
💡
web-sys の features は明示的に指定が必要

web-sys は Web API が非常に多いため、使うものだけを features に列挙する仕組みになっています。 使いたい API の型名を features に追加しないとコンパイルエラーになります。 どんな API が使えるかは docs.rs/web-sys で確認できます。

✓ プロジェクト準備完了!いよいよ Rust コードを書きます

Step 03

Hello WASM

まずはブラウザのコンソールに文字を出力するところから始めましょう。

① lib.rs を書く

rust src/lib.rs
// wasm_bindgen::prelude は必ずインポートする
use wasm_bindgen::prelude::*;

// console.log を Rust から呼び出すためのマクロを定義
#[macro_export]
macro_rules! console_log {
    ($($t:tt)*) => (
        web_sys::console::log_1(&format!($($t)*).into())
    )
}

// ★ #[wasm_bindgen] を付けた関数は JS から呼び出せるようになる
#[wasm_bindgen]
pub fn greet(name: &str) {
    // console.log を呼び出す
    console_log!("こんにちは、{} さん!Rust から WASM 経由で送ります 🦀", name);
}

// JS に文字列を返す関数の例
#[wasm_bindgen]
pub fn get_greeting(name: &str) -> String {
    format!("こんにちは、{} さん!", name)
}

② ビルドする

shell
# --target web = ブラウザで ES Modules として使う形式でビルド
$ wasm-pack build --target web

# 成功すると pkg/ ディレクトリに以下が生成される
pkg/
├── hello_wasm_bg.wasm    # 本体バイナリ
├── hello_wasm.js         # wasm-bindgen が自動生成したグルーコード
├── hello_wasm.d.ts       # TypeScript の型定義
└── package.json

③ HTML から使う

プロジェクトルートに index.html を作成します。

html index.html
<!-- ES Modules で .wasm ファイルを読み込む -->
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Hello WASM</title>
</head>
<body>
  <h1>Rust + WASM</h1>

  <script type="module">
    // pkg/hello_wasm.js から default export(init 関数)と greet をインポート
    import init, { greet, get_greeting } from './pkg/hello_wasm.js';

    async function main() {
      // ★ init() を必ず最初に呼ぶ(.wasm ファイルを fetch して初期化する)
      await init();

      // Rust 関数を呼ぶ(コンソールに出力される)
      greet('Rust WASM 入門者');

      // 戻り値を受け取る
      const msg = get_greeting('世界');
      document.body.innerHTML += `<p>${msg}</p>`;
    }

    main();
  </script>
</body>
</html>

④ 確認する

shell
# http-server でローカルに配信(デフォルトは 8080 ポート)
$ http-server . -c-1

# ブラウザで http://localhost:8080 を開いて
# DevTools の Console に以下が表示されれば成功!
# → こんにちは、Rust WASM 入門者 さん!Rust から WASM 経由で送ります 🦀
💡
wasm_bindgen が自動でやってくれること

&strString などの型変換は wasm-bindgen が自動で行います。 JS の文字列 ↔ Rust の UTF-8 文字列の変換コードを自分で書く必要はありません。 生成された pkg/hello_wasm.js を覗いてみると、メモリ操作のグルーコードが自動生成されているのが分かって面白いです。

✓ Hello WASM 完了!コンソールに文字が出たら OK です 🎉

Step 04

JS ↔ Rust のデータ受け渡し

wasm-bindgen でサポートされている型の範囲と、より複雑な値を扱うときの方法を学びます。

基本的な型マッピング

Rust 型JS 型備考
i32 / u32 / f64numberそのまま使える
boolbooleanそのまま使える
&str / Stringstring自動変換(コピーが発生)
Vec<u8>Uint8Arrayバイト列のやり取りに使う
JsValueany型安全でない万能型
Option<T>T | undefinedNone → undefined に変換
Result<T, JsValue>T (エラー時は JS 例外)エラーハンドリングに使う

数値・演算の例

rust src/lib.rs(追記)
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

// Option を返すと JS 側では undefined になる
#[wasm_bindgen]
pub fn safe_divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 {
        None  // JS 側では undefined
    } else {
        Some(a / b)
    }
}

// Result を返すと JS 側で try/catch できる
#[wasm_bindgen]
pub fn parse_number(s: &str) -> Result<f64, JsValue> {
    s.parse::<f64>()
        .map_err(|e| JsValue::from_str(&e.to_string()))
}

JS 側からの呼び出し

javascript
await init();

// 数値の足し算
const sum = add(3, 4);
console.log(sum); // → 7

// Option: 0 除算は undefined が返る
const r1 = safe_divide(10, 2);  // → 5
const r2 = safe_divide(10, 0);  // → undefined

// Result: エラー時は JS 例外として飛んでくる
try {
    const n = parse_number('abc');
} catch (e) {
    console.log('エラー:', e); // → "invalid float literal"
}

構造体を JS に公開する

#[wasm_bindgen] は構造体にも付けられます。JS からはクラスのように扱えます。

rust src/lib.rs(追記)
// ★ #[wasm_bindgen] を構造体に付けると JS クラスとして公開できる
#[wasm_bindgen]
pub struct Counter {
    count: i32,
}

#[wasm_bindgen]
impl Counter {
    // コンストラクタは new() という名前の関数で定義する
    #[wasm_bindgen(constructor)]
    pub fn new() -> Counter {
        Counter { count: 0 }
    }

    pub fn increment(&mut self) {
        self.count += 1;
    }

    pub fn value(&self) -> i32 {
        self.count
    }

    // JS 側での free() を自動呼び出しにしたい場合は
    // drop() を使うか、wasm-bindgen の自動 GC に任せる
}
javascript
import init, { Counter } from './pkg/hello_wasm.js';
await init();

// JS クラスのように扱える
const counter = new Counter();
counter.increment();
counter.increment();
console.log(counter.value()); // → 2

// ★ WASM 側のメモリを解放する(重要!)
counter.free();
⚠️
メモリ管理に注意

WASM の構造体インスタンスは JS の GC 管理外のメモリに確保されます。 使い終わったら必ず .free() を呼ぶか、try-finally で解放してください。 解放しないと WASM のメモリリークになります。

✓ データの受け渡し完了!次は DOM を Rust から操作します

Step 05

DOM 操作

web-sys クレートを使って Rust からブラウザの DOM を操作します。 これが一番「WASM らしい」使い方になります。

document を取得して要素を操作する

rust src/lib.rs(追記)
use wasm_bindgen::prelude::*;
use web_sys::{window, Document, Element};

// window → document を取得するヘルパー(よく使うパターン)
fn get_document() -> Document {
    window()
        .expect("window が取得できない")
        .document()
        .expect("document が取得できない")
}

#[wasm_bindgen]
pub fn add_list_item(text: &str) ->  Result<(), JsValue> {
    let doc = get_document();

    // id="list" の要素を取得(JS の getElementById と同じ)
    let list = doc
        .get_element_by_id("list")
        .ok_or_else(|| JsValue::from_str("#list が見つからない"))?;

    // <li> 要素を作成してテキストを設定
    let item = doc.create_element("li")?;
    item.set_text_content(Some(text));

    // リストに追加
    list.append_child(&item)?;

    Ok(())
}

イベントリスナーを Rust で登録する

イベントリスナーは少し複雑で、Closure という型を使います。

rust src/lib.rs(追記)
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{HtmlButtonElement, MouseEvent};

#[wasm_bindgen]
pub fn setup_button() ->  Result<(), JsValue> {
    let doc = get_document();

    // id="my-btn" のボタンを取得して HtmlButtonElement にキャスト
    let btn = doc
        .get_element_by_id("my-btn")
        .ok_or(JsValue::from_str("ボタンが見つからない"))?
        // ★ dyn_into でダウンキャスト(JS の型システムの都合)
        .dyn_into::<HtmlButtonElement>()?;

    // ★ Closure::wrap でクロージャを JS が扱える形に変換
    let closure = Closure::wrap(
        Box::new(move |_e: MouseEvent| {
            console_log!("ボタンがクリックされました!🦀");
        }) as Box<dyn FnMut(MouseEvent)>
    );

    // イベントリスナーに登録
    btn.add_event_listener_with_callback(
        "click",
        closure.as_ref().unchecked_ref(),
    )?;

    // ★ forget() しないとクロージャが即座にドロップされてしまう
    // (メモリリークになるが、ページ存続中ずっと使う場合はこれでOK)
    closure.forget();

    Ok(())
}
ℹ️
Closure::forget() について

Closure は Rust の所有権管理に従うため、スコープを抜けると解放されます。 イベントリスナー等「ページが存続する間ずっと生かしておきたい」クロージャは .forget() でリークさせて JS 側に生存期間を委ねます。 逆にリスナーを解除したい場合は forget() せずに参照を保持してください。

✓ DOM 操作完了!いよいよ実践プロジェクトへ 🚀

Step 06

ミニプロジェクト:フィボナッチ計算機

今まで学んだことを組み合わせて、JS 版と WASM 版の速度を比較できる フィボナッチ計算機を作ります。重い再帰計算で WASM の威力を体感しましょう。

① Rust 側の実装

rust src/lib.rs(完成版)
use wasm_bindgen::prelude::*;
use web_sys::{window, Document};

#[macro_export]
macro_rules! console_log {
    ($($t:tt)*) => (
        web_sys::console::log_1(&format!($($t)*).into())
    )
}

fn get_document() -> Document {
    window().unwrap().document().unwrap()
}

// 再帰版フィボナッチ(わざと遅くしてる)
fn fib_recursive(n: u64) -> u64 {
    if n <= 1 {
        return n;
    }
    fib_recursive(n - 1) + fib_recursive(n - 2)
}

// JS から呼ぶエントリーポイント:計算時間も一緒に返す
#[wasm_bindgen]
pub fn calc_fib(n: u32) ->  String {
    // performance.now() で計測(web-sys で window.performance を使う)
    let perf = window().unwrap().performance().unwrap();
    let start = perf.now();

    let result = fib_recursive(n as u64);

    let elapsed = perf.now() - start;
    format!("結果: {} (計算時間: {:.2} ms)", result, elapsed)
}

// ページの初期化:ボタンにイベントリスナーを設定
#[wasm_bindgen(start)]  // ← WASM 初期化時に自動実行される
pub fn on_load() ->  Result<(), JsValue> {
    use wasm_bindgen::JsCast;
    use web_sys::{HtmlButtonElement, HtmlInputElement, MouseEvent};

    let doc = get_document();

    // Rust ボタンのクリック処理
    let btn = doc
        .get_element_by_id("rust-btn")
        .unwrap()
        .dyn_into::<HtmlButtonElement>()?;

    let closure = Closure::wrap(Box::new(move |_: MouseEvent| {
        let d = get_document();

        // input から n の値を読む
        let n_str = d
            .get_element_by_id("n-input")
            .unwrap()
            .dyn_into::<HtmlInputElement>()
            .unwrap()
            .value();

        let n: u32 = n_str.parse().unwrap_or(40);
        let result = calc_fib(n);

        // 結果を表示エリアに書き込む
        d.get_element_by_id("rust-result")
            .unwrap()
            .set_text_content(Some(&result));
    }) as Box<dyn FnMut(MouseEvent)>);

    btn.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())?;
    closure.forget();

    Ok(())
}

② HTML / JS 側の実装

html index.html(完成版)
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>フィボナッチ比較</title>
  <style>
    body { font-family: sans-serif; max-width: 500px; margin: 2rem auto; }
    .row { display: flex; gap: 1rem; margin: 1rem 0; }
    button { padding: 0.5rem 1rem; cursor: pointer; }
    .result { margin-top: 0.5rem; font-weight: bold; min-height: 1.5rem; }
    .rust-result { color: #e07b39; }
    .js-result   { color: #4a9eff; }
  </style>
</head>
<body>
  <h1>🦀 Rust WASM vs JavaScript</h1>
  <p>フィボナッチ数列の n 番目を再帰で計算して速度比較します。</p>

  <div class="row">
    <label>n = <input id="n-input" type="number" value="42" min="1" max="50"></label>
  </div>

  <div class="row">
    <button id="rust-btn">🦀 Rust で計算</button>
    <button id="js-btn">📜 JS で計算</button>
  </div>

  <div class="result rust-result" id="rust-result">(未計算)</div>
  <div class="result js-result"   id="js-result"  >(未計算)</div>

  <script type="module">
    import init from './pkg/hello_wasm.js';

    // WASM を初期化(#[wasm_bindgen(start)] が自動実行される)
    await init();

    // JS 版のフィボナッチ(比較用)
    function fibJs(n) {
      if (n <= 1) return n;
      return fibJs(n - 1) + fibJs(n - 2);
    }

    document.getElementById('js-btn')
      .addEventListener('click', () => {
        const n = +document.getElementById('n-input').value;
        const t0 = performance.now();
        const result = fibJs(n);
        const ms = (performance.now() - t0).toFixed(2);
        document.getElementById('js-result').textContent =
          `結果: ${result} (計算時間: ${ms} ms)`;
      });
  </script>
</body>
</html>

③ ビルドして動かす

shell
$ wasm-pack build --target web
$ http-server . -c-1

# http://localhost:8080 を開いて n=42 あたりで両方計算してみよう
# Rust WASM の方が 2〜10 倍速いことが体感できるはず!
🎯
速度差が出るポイント

n = 42〜48 くらいで顕著に差が出ます。WASM は整数演算・ループ・再帰が特に速く、 JS エンジンの JIT コンパイルとの速度差が出やすいです。 DOM 操作は JS の方が得意なので、「計算処理は Rust、UI 操作は JS」というのが WASM の典型的な使い方です。

✓ ミニプロジェクト完成!お疲れさまでした 🎉🦀

Rust + WASM チュートリアル — wasm-bindgen 0.2 / wasm-pack 0.13 系

コメント