•8 min read

WebAssembly入門

WebAssembly入門

JavaScriptは30年間、ブラウザがネイティブに実行する唯一の言語でした。WebAssemblyはそれを変えます。2017年にすべての主要ブラウザに搭載されて以来、Wasmは「興味深い実験」から、Figma、Google Earth、AutoCAD、FFmpeg.jsなどで使用される本番ツールへと移行しました。ここでは、それが実際にどのように機能するかを説明します。

Audio Briefing
0:00 / 0:00

WebAssemblyとは(そして何ではないか)

WebAssemblyは、スタックベースの仮想マシン用のバイナリ命令フォーマットです。Wasmを手書きすることはありません。C、C++、Rust、Go、その他多くの言語からコンパイルします。ブラウザは.wasmバイナリをダウンロードし、検証(明確に定義されたフォーマットなので高速です)し、実行します。

Wasmがではないこと:

  • JavaScriptの代替ではありません。WasmはDOMに直接触れることはできません。document.getElementByIdなどを呼び出すにはJSが必要です。
  • すべてにおいてJSよりも自動的に高速というわけではありません。Wasmは予測可能で計算量の多い作業で優位に立ちます。DOM操作やイベント処理では通常JSの方が高速です。
  • 安全ではないわけではありません。Wasmはサンドボックス内で実行されます。明示的に与えられたメモリにのみアクセスできます。
Advertisement

メモリモデル:リニアメモリ

これが重要な概念です。Wasmモジュールはリニアメモリ(完全に制御するフラットなバイト配列)にアクセスできます。Wasm側から見ると、ArrayBufferへのi32オフセットのように見えます。JS側から見ると、同じバッファです。

// Loading a .wasm module
const response = await fetch('/my-module.wasm');
const buffer = await response.arrayBuffer();
const module = await WebAssembly.instantiate(buffer, {});
const { memory, add, allocate } = module.instance.exports;

// Read from Wasm memory
const view = new Uint8Array(memory.buffer);
console.log(view[0]);  // byte at offset 0

// Write into Wasm memory
view[100] = 42;

Wasmモジュールはこのバッファの外側には何もアクセスできません。windowもdocumentもファイルシステムもありません。この分離は、セキュリティとポータビリティの両方にとって不可欠です。

RustからWasmへのコンパイル:実践的なパス

Rustは現在、どの言語よりも優れたWasmツールチェーンを持っています。wasm-bindgenクレートはJS/Wasmのグルーコードを自動的に生成します。

# Install target
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
// src/lib.rs
use wasm_bindgen::prelude::*;

// Exported to JavaScript
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => {
            let mut a = 0u64;
            let mut b = 1u64;
            for _ in 2..=n {
                let c = a + b;
                a = b;
                b = c;
            }
            b
        }
    }
}

// Pass and return strings
#[wasm_bindgen]
pub fn process_json(input: &str) -> String {
    // serde_json works in Wasm
    let v: serde_json::Value = serde_json::from_str(input)
        .unwrap_or(serde_json::Value::Null);
    format!("Processed: {} fields", v.as_object().map(|o| o.len()).unwrap_or(0))
}

ビルドと使用:

wasm-pack build --target web

これにより、.wasmファイル、JSラッパー、TypeScript型を含むpkg/ディレクトリが生成されます。アプリでインポートします。

import init, { fibonacci, process_json } from './pkg/my_module.js';

await init(); // loads and instantiates the .wasm
console.log(fibonacci(40));  // 102334155, runs in ~0.1ms
console.log(process_json('{"a":1,"b":2}'));  // "Processed: 2 fields"

EmscriptenによるC/C++のコンパイル

既存のC/C++コードベースの場合、Emscriptenが標準ツールです。

# Install
git clone https://github.com/emscripten-core/emsdk.git
./emsdk/emsdk install latest && ./emsdk/emsdk activate latest
source ./emsdk/emsdk_env.sh
// image_process.c
#include <stdint.h>
#include <stdlib.h>

// Grayscale a pixel buffer in-place
void grayscale(uint8_t* data, int width, int height) {
    int num_pixels = width * height;
    for (int i = 0; i < num_pixels; i++) {
        int idx = i * 4;
        uint8_t r = data[idx], g = data[idx+1], b = data[idx+2];
        uint8_t gray = (uint8_t)(0.299f * r + 0.587f * g + 0.114f * b);
        data[idx] = data[idx+1] = data[idx+2] = gray;
    }
}
emcc image_process.c -o image_process.js \
  -s EXPORTED_FUNCTIONS='["_grayscale", "_malloc", "_free"]' \
  -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \
  -O2

JavaScriptからの使用:

const Module = await import('./image_process.js');
await Module.default();

const grayscale = Module.cwrap('grayscale', null, ['number', 'number', 'number']);

// Get pixel data from canvas
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

// Copy into Wasm memory
const ptr = Module._malloc(imageData.data.length);
Module.HEAPU8.set(imageData.data, ptr);

// Run C function
grayscale(ptr, canvas.width, canvas.height);

// Copy back
imageData.data.set(Module.HEAPU8.subarray(ptr, ptr + imageData.data.length));
ctx.putImageData(imageData, 0, 0);
Module._free(ptr);

このパターン(malloc、コピーイン、計算、コピーアウト、free)は、GC統合のない言語におけるWasmの基本的なデータ交換モデルです。

Advertisement

パフォーマンス:Wasmが実際に勝つとき

タスクJSWasm (Rust)スピードアップ
フィボナッチ(40)0.8ms0.1ms8倍
SHA-256 (1MB)14ms2.4ms5.8倍
JPEGデコード (2MP)120ms22ms5.5倍
文字列連結 (10万回操作)2ms6ms0.3倍
DOM更新 (1000ノード)8ms40ms (JSブリッジ経由)0.2倍

パターンは明らかです。WasmはCPUバウンドな数値計算で優位に立ちます。JS/Wasm境界を頻繁に越える場合(各越境にはオーバーヘッドがあります)やDOM作業を行う場合は不利になります。

Wasmのデバッグ

Chrome DevToolsはWasmの直接デバッグをサポートしています。Rustの場合、デバッグシンボルを保持するためにwasm-pack build --devフラグを追加し、DevTools → Sourcesで.rsソースファイルが表示されます。ブレークポイントを設定し、変数を検査できます。

本番ビルドでは、ソースマップはサポートされていますが、.wasmバイナリは削除されます。バイナリサイズを削減するため(しばしば30〜50%)、Binaryenツールキットのwasm-opt -O3を使用します。

wasm-pack build --release
wasm-opt -O3 -o pkg/my_module_opt.wasm pkg/my_module_bg.wasm

次に来るもの:WASI

WebAssemblyはブラウザで始まりましたが、WebAssembly System Interface (WASI) はそれをサーバーに拡張します。WASIはWasmモジュールに、ファイル、ネットワーキング、環境変数にアクセスするための標準化された方法を提供します。これは同じサンドボックスモデルを維持しながら行われます。WASI用にコンパイルされたWasmモジュールは、Cloudflare Workers、Fermyon Spin、またはローカルのwasmtimeプロセスで再コンパイルなしで実行できます。

サーバーサイドWasmはブラウザWasmとは異なるトピックですが、リニアメモリ、サンドボックス分離、バイナリのポータビリティといった基本は同じです。

こちらもおすすめ

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement