WithCodeMedia-1-pc
previous arrowprevious arrow
next arrownext arrow

WithCodeMedia-1-sp
previous arrowprevious arrow
next arrownext arrow

WebAssemblyとJavaScript間のデータやりとり完全ガイド|線形メモリ・ArrayBuffer・文字列・serde-wasm-bindgen・SharedArrayBuffer・コールバック・メモリ管理まで徹底解説

生徒

WebAssemblyをJavaScriptから使いたいんですが、数値は渡せても文字列や配列の渡し方が分からなくて詰まっています……

ペン博士

WebAssemblyとJavaScript間のデータやりとりにはいくつかのパターンがあるんじゃ!プリミティブ型は直接渡せるが、文字列や複雑なオブジェクトはメモリ(ArrayBuffer)を通じて渡す必要があるんじゃ。wasm-bindgenを使えばRustで書いたWasmはこの複雑さを抽象化してくれる!さらにSharedArrayBufferで「ゼロコピー共有」も実現できるし、JSのコールバックをWasmに渡す方法も解説するぞ!

目次

この記事でわかること

  • WebAssemblyの線形メモリモデルとArrayBufferの基本的な仕組み
  • プリミティブ型・文字列・バイナリデータの安全な受け渡し方法
  • wasm-bindgenとserde-wasm-bindgenで構造化データを型安全に扱う手順
  • SharedArrayBufferでJSとWasm間のゼロコピー共有を実現する方法
  • JSコールバックのWasmへの渡し方・メモリリーク防止・TypeScript型の自動生成

WebAssemblyの基本は分かったものの、「文字列や配列・オブジェクトをどうやってJavaScriptとWasm間でやりとりするか」で詰まることが多いです。WasmはJavaScriptのValueオブジェクトを直接扱えず、メモリ(線形メモリ)を通じてデータを渡す必要があります。

本記事では、Wasm-JavaScript間のデータ受け渡しの基本原則・線形メモリモデルの詳細・プリミティブ型・文字列(TextEncoder/Decoder手動実装)・wasm-bindgenによる自動変換・serde-wasm-bindgenによる構造化データ・SharedArrayBufferゼロコピー共有・JSコールバックのWasmへの渡し方・C++(Emscripten)アプローチ・メモリ管理とリーク防止・TypeScript型の自動生成を完全解説します。


WebAssemblyのメモリモデルを理解する

WebAssemblyの線形メモリとJavaScript間のデータ受け渡しの図
>【WebAssemblyとJavaScript間のデータ受け渡しの基本原則】

✅ 直接渡せるもの(プリミティブ型):
→ 整数(i32, i64):JavaScriptの number と直接やりとり可能
→ 浮動小数点(f32, f64):JavaScriptの number と直接やりとり可能
→ bigint (i64 の大きな値):JavaScriptの BigInt と対応

❌ 直接渡せないもの(参照型):
→ 文字列(string)
→ 配列(Array, TypedArray の高レベルAPI)
→ オブジェクト({ key: value })
→ これらは「WebAssembly.Memory(線形メモリ)を通じた間接的な渡し方」が必要

【データ受け渡しの3つの手段】
1. エクスポート関数の引数/戻り値(数値のみ直接)
2. WebAssembly.Memory(共有線形メモリ)を介したバッファ渡し
3. wasm-bindgen(Rust)/ Emscripten(C/C++): 複雑な型を自動変換

【Wasmの線形メモリとは】
→ Wasmモジュールは「線形メモリ」という連続したバイト配列を持つ
→ JavaScriptからは ArrayBuffer としてアクセス可能
→ Uint8Array, Int32Array 等のTypedArrayで読み書きできる
→ 初期サイズは page(64KB)単位で指定、memory.grow() で拡張可能
>// WebAssembly.Memory の基本操作
const memory = new WebAssembly.Memory({
  initial: 1,   // 1 page = 64KB
  maximum: 100, // 最大100 page = 6.4MB
  shared: false, // trueにするとSharedArrayBufferになる
})

// JavaScript側でメモリにアクセス
const buffer = memory.buffer  // ArrayBuffer
const view = new Uint8Array(buffer)  // バイト単位で読み書き
const int32View = new Int32Array(buffer)  // 4バイト整数として読み書き

// メモリにデータを書き込む
view[0] = 72    // 'H'
view[1] = 101   // 'e'
view[2] = 108   // 'l'
view[3] = 108   // 'l'
view[4] = 111   // 'o'

// メモリを拡張する
const prevPages = memory.grow(1)  // 1ページ(64KB)拡張
// grow()後は buffer が新しいArrayBufferになるため再取得が必要

プリミティブ型の受け渡し(最もシンプル)

>// Rust で数値関数を定義してJSから呼び出す例

// src/lib.rs
use wasm_bindgen::prelude::*;

// i32(32ビット整数)
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

// f64(64ビット浮動小数点)
#[wasm_bindgen]
pub fn square(x: f64) -> f64 {
    x * x
}

// bool(i32の0/1として変換)
#[wasm_bindgen]
pub fn is_even(n: i32) -> bool {
    n % 2 == 0
}

// 複数の数値を処理
#[wasm_bindgen]
pub fn calculate_stats(values: &[f64]) -> f64 {
    if values.is_empty() { return 0.0; }
    values.iter().sum::() / values.len() as f64  // 平均値
}

// i64 と BigInt(JavaScript側でBigIntを使う必要がある)
#[wasm_bindgen]
pub fn factorial(n: u32) -> u64 {
    (1..=n as u64).product()
}
>// JavaScript 側
import init, { add, square, is_even, calculate_stats, factorial } from './pkg/my_wasm.js'
await init()

console.log(add(3, 4))             // → 7
console.log(square(2.5))           // → 6.25
console.log(is_even(42))           // → true
console.log(calculate_stats([1, 2, 3, 4, 5])) // → 3(平均値)

// i64はBigIntとして返ってくる
const result = factorial(20)
console.log(result)  // → 2432902008176640000n(BigInt)

文字列の受け渡し|TextEncoderとメモリ操作

方法A:wasm-bindgenで自動変換(推奨)

>// Rust + wasm-bindgen: &str を直接受け取れる(wasm-bindgenが変換を行う)
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("こんにちは、{}さん!", name)
}

#[wasm_bindgen]
pub fn reverse_string(s: &str) -> String {
    s.chars().rev().collect()
}

// 文字列の配列を受け取る(wasm-bindgenがjs_sys::Array経由で変換)
#[wasm_bindgen]
pub fn join_strings(separator: &str, parts: js_sys::Array) -> String {
    let strs: Vec = parts
        .iter()
        .filter_map(|v| v.as_string())
        .collect();
    strs.join(separator)
}
>// JavaScript 側 - wasm-bindgenが文字列変換を自動で行う
import init, { greet, reverse_string, join_strings } from './pkg/my_wasm.js'
await init()

const message = greet('田中')
console.log(message)  // → "こんにちは、田中さん!"

const reversed = reverse_string('Hello, World!')
console.log(reversed)  // → "!dlroW ,olleH"

// wasm-bindgenの内部処理(透過的に行われる):
// 1. JSのstring → TextEncoder でUTF-8バイト列に変換
// 2. Wasmのメモリ(alloc確保)に書き込む
// 3. ポインタと長さをWasm関数に渡す
// 4. Wasm関数が戻り値のポインタと長さを返す
// 5. JSがメモリからUTF-8デコードしてstringに変換
// 6. Wasmのメモリを解放

方法B:手動でTextEncoderを使う(wasm-bindgenなしの生のWasm)

>// WebAssembly.instantiate でWasmを直接読み込む場合
const wasmModule = await WebAssembly.instantiateStreaming(
  fetch('./module.wasm'),
  {
    env: {
      abort: () => console.error('Wasm abort called'),
    }
  }
)
const { instance } = wasmModule
const wasmMemory = new Uint8Array(instance.exports.memory.buffer)

// ===== JSからWasmへ文字列を渡す =====
function writeStringToWasm(str, ptr) {
  const encoder = new TextEncoder()
  const bytes = encoder.encode(str)  // UTF-8エンコード
  wasmMemory.set(bytes, ptr)
  wasmMemory[ptr + bytes.length] = 0  // null終端(Cスタイル文字列)
  return bytes.length
}

// ===== Wasmのメモリから文字列を読み取る =====
function readStringFromWasm(ptr) {
  // null終端まで読み進める
  let end = ptr
  while (wasmMemory[end] !== 0) end++
  const slice = wasmMemory.slice(ptr, end)
  return new TextDecoder('utf-8').decode(slice)
}

// 使い方例
const inputPtr = instance.exports.alloc(100)  // 100バイト確保
const byteLen = writeStringToWasm('Hello, Wasm!', inputPtr)

const outputPtr = instance.exports.process_string(inputPtr, byteLen)
const result = readStringFromWasm(outputPtr)
console.log(result)

instance.exports.dealloc(inputPtr, 100)  // メモリ解放(重要!)

バイナリデータ(ArrayBuffer・TypedArray)の受け渡し

>// Rust: Vec / &[u8] を受け取ってバイト操作
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn process_bytes(data: &[u8]) -> Vec {
    // バイト列を逆順にする例
    data.iter().rev().cloned().collect()
}

// 画像処理(グレースケール変換)
#[wasm_bindgen]
pub fn grayscale(rgba_data: &[u8]) -> Vec {
    let mut output = rgba_data.to_vec()

    // RGBA形式: 4バイトごとにRGBAが並ぶ
    for chunk in output.chunks_mut(4) {
        let r = chunk[0] as f64
        let g = chunk[1] as f64
        let b = chunk[2] as f64
        // 輝度の重み付け平均(標準的な変換式)
        let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8
        chunk[0] = gray
        chunk[1] = gray
        chunk[2] = gray
        // chunk[3]はα値なのでそのまま
    }

    output
}
>// JavaScript 側 - Uint8Array で受け渡し
import init, { process_bytes, grayscale } from './pkg/my_wasm.js'
await init()

// バイト配列の処理
const input = new Uint8Array([1, 2, 3, 4, 5])
const reversed = process_bytes(input)
console.log(Array.from(reversed))  // → [5, 4, 3, 2, 1]

// Canvas の ImageData をWasmで処理
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

// 画像を Canvas に描画
const img = document.getElementById('source-image')
ctx.drawImage(img, 0, 0)

// ImageData を取得してWasmに渡す
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)

// Uint8Array として wasm-bindgen に渡す
const processed = grayscale(imageData.data)

// 処理結果を Canvas に戻す
const outputImageData = new ImageData(processed, canvas.width, canvas.height)
ctx.putImageData(outputImageData, 0, 0)

// パフォーマンス比較
const start = performance.now()
// Wasmで処理(典型的に3〜10倍高速)
const wasmResult = grayscale(imageData.data)
console.log(`Wasm: ${performance.now() - start}ms`)

構造化データの受け渡し|serde-wasm-bindgen vs JSON

>// Cargo.toml の依存関係
[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-wasm-bindgen = "0.6"  # 高速なシリアライズ

方法1:JSON経由(シンプル・広く使われる)

>// Rust - JSON文字列として受け渡し
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct User {
    pub id: u32,
    pub name: String,
    pub email: String,
    pub scores: Vec,
}

#[derive(Serialize, Deserialize)]
pub struct UserStats {
    pub name: String,
    pub average_score: f64,
    pub max_score: f64,
    pub grade: String,
}

#[wasm_bindgen]
pub fn analyze_user(json: &str) -> String {
    let user: User = match serde_json::from_str(json) {
        Ok(u) => u,
        Err(e) => return format!("{{\"error\": \"{}\"}}", e),
    };

    let avg = if user.scores.is_empty() {
        0.0
    } else {
        user.scores.iter().sum::() / user.scores.len() as f64
    };

    let max = user.scores.iter().cloned().fold(f64::NEG_INFINITY, f64::max);

    let grade = match avg as u32 {
        90..=100 => "S",
        80..=89  => "A",
        70..=79  => "B",
        60..=69  => "C",
        _        => "D",
    };

    let stats = UserStats {
        name: user.name,
        average_score: avg,
        max_score: max,
        grade: grade.to_string(),
    };

    serde_json::to_string(&stats).unwrap()
}
>// JavaScript 側 - JSON経由
import init, { analyze_user } from './pkg/my_wasm.js'
await init()

const user = {
  id: 1,
  name: '山田太郎',
  email: 'yamada@example.com',
  scores: [85, 92, 78, 95, 88],
}

const resultJson = analyze_user(JSON.stringify(user))
const stats = JSON.parse(resultJson)
console.log(stats)
// → { name: "山田太郎", average_score: 87.6, max_score: 95, grade: "A" }

方法2:serde-wasm-bindgen(高速・型安全)

>// Rust - serde-wasm-bindgen使用(JSON変換オーバーヘッドなし)
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct AnalysisResult {
    pub total: f64,
    pub mean: f64,
    pub std_dev: f64,
    pub percentile_90: f64,
}

#[wasm_bindgen]
pub fn analyze_data(data: JsValue) -> Result {
    // JsValue → Rust型に変換(serde-wasm-bindgen)
    let numbers: Vec = serde_wasm_bindgen::from_value(data)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;

    let n = numbers.len() as f64;
    let total: f64 = numbers.iter().sum();
    let mean = total / n;

    let variance = numbers.iter()
        .map(|x| (x - mean).powi(2))
        .sum::() / n;
    let std_dev = variance.sqrt();

    let mut sorted = numbers.clone();
    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
    let p90_idx = (n * 0.9) as usize;
    let percentile_90 = sorted[p90_idx.min(sorted.len() - 1)];

    let result = AnalysisResult { total, mean, std_dev, percentile_90 };

    // Rust型 → JsValue に変換(serde-wasm-bindgen)
    serde_wasm_bindgen::to_value(&result)
        .map_err(|e| JsValue::from_str(&e.to_string()))
}

// JavaScript 側
// const data = [1, 5, 3, 8, 2, 9, 4, 7, 6]
// const result = analyze_data(data)
// → { total: 45, mean: 5, std_dev: ..., percentile_90: ... }

SharedArrayBufferでゼロコピー共有

>// SharedArrayBuffer を使ったゼロコピー共有
// 注意: COOP/COEP ヘッダーが必要(cross-origin isolation)

// JavaScriptとWasmがSharedArrayBufferを共有してコピーなしでアクセス

// まず SharedArrayBuffer 対応のメモリでWasmを初期化
const memory = new WebAssembly.Memory({
  initial: 10,   // 640KB
  maximum: 100,  // 6.4MB
  shared: true,  // SharedArrayBuffer を使用
})

const wasmModule = await WebAssembly.instantiateStreaming(
  fetch('./wasm_module.wasm'),
  { env: { memory } }
)

const { instance } = wasmModule

// 共有メモリ(Int32Array)の作成
const sharedArray = new Int32Array(memory.buffer)

// 大量データをゼロコピーでWasmに渡す
const dataOffset = 0
const dataLength = 1000000  // 100万要素

// データを書き込む(コピーなし、直接書き込み)
for (let i = 0; i < dataLength; i++) {
  sharedArray[dataOffset + i] = Math.random() * 1000 | 0
}

// Wasmで処理(データのコピーは発生しない)
const sum = instance.exports.sum_array(dataOffset, dataLength)
console.log('合計:', sum)

// Web Worker との連携でメインスレッドをブロックしない
const worker = new Worker('./wasm_worker.js')
worker.postMessage({ memory, dataOffset, dataLength })
worker.onmessage = (e) => {
  console.log('処理完了:', e.data.result)
}

JavaScriptのコールバックをWasmに渡す

>// Rust + wasm-bindgen: JSの関数をコールバックとして受け取る

use wasm_bindgen::prelude::*;
use js_sys::Function;

// JS関数をコールバックとして受け取る
#[wasm_bindgen]
pub fn process_with_callback(
    data: &[i32],
    callback: &Function,  // JSの関数
) {
    for (i, &value) in data.iter().enumerate() {
        let this = JsValue::null()
        let result = JsValue::from(value * 2)
        let index = JsValue::from(i as u32)
        // JSのコールバックを呼び出す
        callback.call2(&this, &result, &index).unwrap()
    }
}

// 進捗コールバックつきの重い処理
#[wasm_bindgen]
pub fn heavy_computation(
    n: u32,
    on_progress: Option,  // オプショナルなコールバック
) -> f64 {
    let mut result = 0.0
    for i in 0..n {
        // 重い計算...
        result += (i as f64).sqrt()

        // 100回に1回プログレス報告
        if i % (n / 100) == 0 {
            if let Some(ref cb) = on_progress {
                let progress = JsValue::from((i as f64 / n as f64) * 100.0)
                let _ = cb.call1(&JsValue::null(), &progress)
            }
        }
    }
    result
}
>// JavaScript 側 - コールバック関数を渡す
import init, { process_with_callback, heavy_computation } from './pkg/my_wasm.js'
await init()

// コールバックを渡す
const results = []
process_with_callback(
  new Int32Array([1, 2, 3, 4, 5]),
  (doubled, index) => {
    results.push({ index, doubled })
    console.log(`index=${index}: ${doubled}`)
  }
)
console.log(results)
// → [{ index: 0, doubled: 2 }, { index: 1, doubled: 4 }, ...]

// 進捗コールバック付きの重い計算
const progressBar = document.getElementById('progress')
const result = heavy_computation(1_000_000, (progress) => {
  progressBar.style.width = `${progress}%`
  progressBar.textContent = `${progress.toFixed(0)}%`
})
console.log('計算結果:', result)

C/C++(Emscripten)でのデータ受け渡し

>// C++ のコードを Emscripten でWasmにコンパイル

// module.cpp
#include 
#include 
#include 
#include 

// 文字列を受け取る関数
std::string greet(const std::string& name) {
    return "Hello, " + name + "!";
}

// ベクターを受け取る関数
float calculate_average(const std::vector& values) {
    if (values.empty()) return 0.0f;
    float sum = 0;
    for (float v : values) sum += v;
    return sum / values.size();
}

// embind でJSにバインディングを生成
EMSCRIPTEN_BINDINGS(my_module) {
    emscripten::function("greet", &greet);
    emscripten::function("calculate_average", &calculate_average);
    emscripten::register_vector("FloatVector");
}

// コンパイルコマンド
// emcc module.cpp -o module.js \
//   -lembind \
//   -s WASM=1 \
//   -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \
//   -s ALLOW_MEMORY_GROWTH=1
>// Emscripten で生成されたJSモジュールを使う
import Module from './module.js'

const instance = await Module()

// 文字列を渡す(embind が自動変換)
const greeting = instance.greet('World')
console.log(greeting)  // → "Hello, World!"

// FloatVector(C++のvector)を使う
const vec = new instance.FloatVector()
vec.push_back(1.5)
vec.push_back(2.5)
vec.push_back(3.5)

const avg = instance.calculate_average(vec)
console.log(avg)  // → 2.5

// メモリ解放(重要!)
vec.delete()

// ccall/cwrap を使う低レベルな方法
const add = instance.cwrap('add', 'number', ['number', 'number'])
console.log(add(3, 4))  // → 7

メモリ管理とリーク防止

>// Rust + wasm-bindgen: 所有権とメモリ管理

// wasm-bindgenを使うと Rust の所有権システムにより
// メモリ管理は自動化される(ガベージコレクション不要)
// → VecはWasm内で生成・破棄される(自動)

// ただし、Rustで#[wasm_bindgen]を付けたstructは注意が必要
#[wasm_bindgen]
pub struct DataProcessor {
    data: Vec,
    result: Option,
}

#[wasm_bindgen]
impl DataProcessor {
    // コンストラクター
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        DataProcessor { data: Vec::new(), result: None }
    }

    pub fn add_data(&mut self, value: f64) {
        self.data.push(value)
    }

    pub fn compute(&mut self) -> f64 {
        let mean = self.data.iter().sum::() / self.data.len() as f64;
        self.result = Some(mean);
        mean
    }

    // 明示的なクリーンアップ(free()メソッド)
    pub fn clear(&mut self) {
        self.data.clear();
        self.result = None;
    }
}

// JavaScript 側 - WasmのStructを使う場合はfree()を呼ぶ必要がある
import init, { DataProcessor } from './pkg/my_wasm.js'
await init()

const processor = new DataProcessor()

try {
  processor.add_data(1.5)
  processor.add_data(2.5)
  processor.add_data(3.5)
  const mean = processor.compute()
  console.log('平均:', mean)  // → 2.5
} finally {
  // Wasmのメモリを解放(重要!)
  // wasm-bindgenが生成するstructはfree()が必要
  processor.free()
}

// ===== 一般的なメモリリーク防止パターン =====
// 1. wasm-bindgenのstructは必ず .free() を呼ぶ
// 2. Emscriptenのvectorは必ず .delete() を呼ぶ
// 3. 手動メモリ管理の場合は alloc/dealloc を対で使う
// 4. 重い処理はWeb Workerで実行(メインスレッドのメモリ節約)

TypeScript型定義の自動生成

>// wasm-bindgenはTypeScriptの型定義(.d.ts)を自動生成する

// wasm-pack ビルド後に生成される my_wasm.d.ts(例)
/* tslint:disable */
/* eslint-disable */

/**
 * WASMモジュール初期化
 * @param {RequestInfo | URL | BufferSource | WebAssembly.Module} input
 */
export default function init(input?: RequestInfo | URL | BufferSource | WebAssembly.Module): Promise

export interface InitOutput {
  readonly memory: WebAssembly.Memory
  readonly add: (a: number, b: number) => number
  readonly square: (a: number) => number
  readonly greet: (a: number, b: number) => [number, number]
  readonly process_bytes: (a: number, b: number) => [number, number, number]
  readonly analyze_user: (a: number, b: number) => [number, number]
  readonly __wbg_dataprocessor_free: (a: number, b: number) => void
  readonly dataprocessor_new: () => number
  readonly dataprocessor_add_data: (a: number, b: number) => void
  readonly dataprocessor_compute: (a: number) => number
  // ...その他のエクスポート
}

// 型安全なインポートと使用
import init, { add, square, greet, DataProcessor } from './pkg/my_wasm'

// 型補完が効く!
const result: number = add(3, 4)
const msg: string = greet('World')

const processor = new DataProcessor()
processor.add_data(1.5)  // TypeScriptが引数の型をチェック
const mean: number = processor.compute()
processor.free()

パフォーマンス計測と最適化のヒント

>// データ受け渡しのパフォーマンスオーバーヘッドを計測

// 各方法のオーバーヘッド比較
const N = 1000000  // 100万要素

// ===== 方法1: プリミティブ(最速)=====
const start1 = performance.now()
for (let i = 0; i < 1000; i++) {
  wasmAdd(i, i + 1)  // 数値のみ: ほぼゼロオーバーヘッド
}
console.log(`プリミティブ: ${performance.now() - start1}ms`)

// ===== 方法2: TypedArray(ほぼ無コスト)=====
const data = new Float64Array(N)
data.fill(1.5)

const start2 = performance.now()
const result = wasmProcessArray(data)  // Uint8Arrayへの参照渡し: 高速
console.log(`TypedArray: ${performance.now() - start2}ms`)

// ===== 方法3: JSON変換(最もオーバーヘッドあり)=====
const obj = { values: Array.from({ length: 1000 }, (_, i) => i) }

const start3 = performance.now()
for (let i = 0; i < 100; i++) {
  const json = JSON.stringify(obj)  // シリアライズオーバーヘッド
  wasmProcessJson(json)
  JSON.parse(wasmGetResult())      // デシリアライズオーバーヘッド
}
console.log(`JSON経由: ${performance.now() - start3}ms`)

// 最適化のヒント:
// 1. 大量データは TypedArray (Uint8Array/Float64Array) で渡す
// 2. 頻繁に呼び出す関数はオブジェクトをWasm側で保持(DataProcessor パターン)
// 3. SharedArrayBuffer でコピーを完全に排除
// 4. 小さなデータは JSON でも問題なし(1KB以下)

よくある質問

WasmからDOMにアクセスできますか?

直接はできません。WasmはDOM APIを直接呼び出す機能を持っていません。wasm-bindgenを使うと、JavaScriptのDOM操作関数をWasmにインポートして、Rustのコードからindirect的にDOMを操作できますweb_sysクレートを使うとdocument.getElementById()element.set_inner_html()などのDOM操作をRustから呼び出せます。ただし頻繁なDOM操作はJSとの境界コストがかかるため、計算処理はWasm・UI更新はJSという分担が効率的です。

wasm-pack と wasm-bindgen の違いは何ですか?

wasm-bindgenはRustのコードとJavaScript間の型変換・バインディング生成を行うライブラリです。wasm-packはwasm-bindgenを活用してWasmプロジェクト全体のビルド・テスト・パッケージング(npm公開)を行うCLIツールです。実際の開発ではwasm-pack buildコマンドでビルドすると、wasm-bindgenのバインディング生成・TypeScript型定義の出力・npm用のpackage.json生成まで一括で行われます。

WasmとJSのデータ受け渡しはどのくらい遅いですか?

数値のプリミティブ型はほぼゼロオーバーヘッドです。文字列はUTF-8エンコード・デコードとメモリコピーが発生するため、短い文字列(数十バイト)で数マイクロ秒程度のオーバーヘッドがあります。大きなTypedArray(数MB)は参照渡しで高速ですが、wasm-bindgenを通じると内部でコピーが発生することがあります。パフォーマンスが最重要な場合はSharedArrayBufferを使ったゼロコピー共有が最も効率的です。

SharedArrayBufferを使うために必要なHTTPヘッダーは何ですか?

SharedArrayBufferを使用するにはCross-Origin Isolation(クロスオリジン分離)が必要です。サーバーから以下のHTTPヘッダーを送信してください。

  • Cross-Origin-Opener-Policy: same-origin
  • Cross-Origin-Embedder-Policy: require-corp

これらのヘッダーが設定されていないとSharedArrayBuffer is not definedエラーが発生します。Vercelではvercel.jsonheaders設定で追加できます。

Emscriptenのvec.delete()を忘れるとどうなりますか?

Wasmのヒープメモリがリークします。JavaScriptのガベージコレクターはEmscriptenが管理するWasmヒープを追跡できないため、vec.delete()を呼び出さないと使用済みメモリが解放されません。長時間動作するアプリケーションではメモリ使用量が増加し続け、最終的にクラッシュする可能性があります。必ずtry/finallyブロックでdelete()を確実に呼び出すパターンを使用してください。


まとめ

  • 線形メモリ:Wasmが持つ連続バイト配列。JavaScriptからはArrayBufferとしてアクセスし、TypedArrayで読み書きする
  • プリミティブ型(i32/f64):JavaScriptのnumberと直接やりとり可能。オーバーヘッドほぼゼロ
  • 文字列:wasm-bindgen使用時は自動変換。手動の場合はTextEncoder/DecoderとWasmメモリを使う
  • バイナリデータ:Uint8Array / &[u8] の形で受け渡し。画像処理・音声処理に特に有効
  • 構造化データ:JSON経由(シンプル)かserde-wasm-bindgen(高速・型安全)で受け渡し
  • SharedArrayBuffer:ゼロコピー共有。COOP/COEPヘッダーが必要。大量データの高速処理に最適
  • JSコールバック:js_sys::Functionを使ってJSのコールバック関数をRustに渡せる。進捗報告に活用
  • メモリ管理:wasm-bindgenのstructはfree()を必ず呼ぶ。Emscriptenのvectorはdelete()で解放
  • TypeScript型:wasm-pack buildで.d.tsが自動生成され型安全な開発が可能

WebAssemblyとJavaScriptのデータ受け渡しは「メモリを介した間接的なやりとり」が基本です。wasm-bindgenを使えばこの複雑さがほぼ隠蔽されるため、Rustでの実装から始めることを強く推奨します。特に画像処理・音声解析・暗号化処理のように大量バイナリデータを扱う処理では、TypedArrayを使った高速な受け渡しを活用することでJavaScript単体の3〜10倍の処理速度が達成できます。


WithCodeを体験できる初級コース公開中!

この記事を書いた人

WithCode(ウィズコード)は「目指すなら稼げる人材」をビジョンに、累計400名以上のフリーランスを輩出してきた超実践型プログラミングスクールです。150社以上の実案件支援を特徴にWeb制作・Webデザインなどの役立つ情報を現場のノウハウに基づいて発信していきます。

– service –WithGroupの運営サービス

  • WithCode
    - ウィズコード -

    スクール

    「未経験」から
    現場で通用する
    スキルを身に付けよう!

    詳細はこちら
  • WithFree
    - ウィズフリ -

    実案件サポート

    制作会社のサポート下で
    実務経験を積んでいこう!

    詳細はこちら
  • WithCareer
    - ウィズキャリ -

    就転職サポート

    大手エージェントのサポート下で
    キャリアアップを目指そう!

    詳細はこちら

公式サイト より
今すぐ
無料カウンセリング
予約!

目次