中級 data-structures

Wasmメモリモデル

リニアメモリとは?

WebAssemblyにはリニアメモリと呼ばれる単一の連続メモリブロックがあります。RustとJavaScriptの両方が読み書きできる巨大な Vec<u8> と考えてください。

Address:  0x0000    0x0100    0x0200    ...    0xFFFF
         ┌─────────┬─────────┬─────────┬──────┐
Memory:  │ stack   │ heap    │ data    │ ...  │
         │ data    │ allocs  │ section │      │
         └─────────┴─────────┴─────────┴──────┘
         └────────── 1 page = 64KB ───────────┘

主な特徴:

  • バイトアドレス指定 — すべてのバイトにインデックスがある(0, 1, 2, ...)
  • ページ単位 — メモリは64KBページ単位で拡張(1ページ = 65,536バイト)
  • 共有可能 — RustとJSの両方が同じバイトにアクセスできる
  • 境界チェック付き — 範囲外メモリアクセスはトラップする(ブラウザはクラッシュしない)

Rustがリニアメモリをどう使うか

RustをWasmにコンパイルすると、コンパイラは以下のようにメモリを配置します:

┌──────────────┬──────────────┬──────────────┬──────────────┐
│ Static data  │ Stack        │ Heap         │ Free space   │
│ (strings,    │ (local vars, │ (Vec, String,│ (grows →)    │
│  constants)  │  call frames)│  Box allocs) │              │
└──────────────┴──────────────┴──────────────┴──────────────┘
0x0000                                                  0xFFFF...

JavaScriptからメモリにアクセスする

JavaScriptはWasmメモリを ArrayBuffer として参照します:

// init()の後、wasm.memoryが利用可能になる
const memory = wasm.memory;

// バッファへの型付きビューを作成
const bytes = new Uint8Array(memory.buffer);
const u32s = new Uint32Array(memory.buffer);
const f64s = new Float64Array(memory.buffer);

// アドレス1024のバイトを読み取る
const value = bytes[1024];

// アドレス2048(バイトオフセット)にfloatを書き込む
f64s[2048 / 8] = 3.14;  // Float64Arrayのインデックス = バイトオフセット / 8

RustとJS間のデータ受け渡し

方法1: wasm-bindgenによるコピー(シンプルで安全)

#[wasm_bindgen]
pub fn process(data: &[u8]) -> Vec<u8> {
    // dataはJSからWasmメモリにコピーされる
    let mut result = data.to_vec();
    // ... resultを加工 ...
    result  // WasmからJSにコピーして返される
}

方法2: ポインタ + 長さ(ゼロコピー、上級者向け)

#[wasm_bindgen]
pub struct Buffer {
    data: Vec<u8>,
}

#[wasm_bindgen]
impl Buffer {
    #[wasm_bindgen(constructor)]
    pub fn new(size: usize) -> Self {
        Self { data: vec![0; size] }
    }

    pub fn ptr(&self) -> *const u8 {
        self.data.as_ptr()
    }

    pub fn len(&self) -> usize {
        self.data.len()
    }
}
const buf = new Buffer(1024);
const ptr = buf.ptr();
const len = buf.len();

// Wasmメモリへの直接ビュー — ゼロコピー!
const view = new Uint8Array(wasm.memory.buffer, ptr, len);

// その場で変更 — Rust側にも反映される
view[0] = 42;

メモリの拡張

Wasmメモリは小さく始まり、必要に応じて拡張されます:

// Vec/Stringがより多くの領域を必要とすると、Rustが自動的にメモリを拡張する
let mut big_vec = Vec::with_capacity(1_000_000); // メモリが拡張される可能性がある
// JSから手動でチェック・拡張することも可能
console.log(wasm.memory.buffer.byteLength); // 現在のサイズ
wasm.memory.grow(10); // 10ページ(640KB)拡張

// 重要: grow()の後、すべてのArrayBufferビューは無効になる!
// 拡張後にビューを再作成する:
const freshView = new Uint8Array(wasm.memory.buffer);

よくある落とし穴

落とし穴 原因 対策
メモリ拡張後のArrayBufferビューの失効 memory.grow() が古いバッファを切り離す メモリ拡張の可能性がある操作の後にビューを再作成する
アライメントエラー Float64Arrayは8バイトアライメントが必要 ptr as usize % 8 == 0 チェックを使う
メモリリーク Rustのアロケーションが解放されない 構造体を適切にドロップし、forget() の多用を避ける
解放済みメモリの読み取り 古いJS参照によるuse-after-free Rust構造体を解放する前にデータをコピーしておく

メモリサイズの目安

内容 一般的なサイズ
小さなWasmモジュール 50-200KB
Wasm + web-sysバインディング 200KB-1MB
デフォルトのリニアメモリ 1ページ(64KB)
大規模アプリケーション 10-50MB
ブラウザの上限 約2-4GB

試してみよう

Run をクリックして、リニアメモリの概念を実際に確認しましょう — メモリの作成、オフセットでの読み書き、ページの拡張、型付き値の格納を体験できます。

試してみる

チャプタークイズ

すべての問題に正解してレッスンを完了しましょう