NodeJS 速習チュートリアル

Node.js WebAssembly

1. WebAssembly とは?

WebAssembly (Wasm) は、C、C++、Rust などの高レベル言語向けのポータブルなコンパイルターゲットとして設計された、バイナリ命令形式(Binary Instruction Format)です。

WebAssembly の主な特徴は以下の通りです:

  • バイナリ形式 (Binary format) - JavaScript よりも読み込みと実行が速いコンパクトなサイズ。
  • ニアネイティブなパフォーマンス (Near-native performance) - マシンコードに近い速度で実行可能。
  • プラットフォームに依存しない (Platform independent) - ブラウザ、Node.js、その他の環境で動作。
  • 安全性 (Safety) - 強固なセキュリティモデルを持つサンドボックス環境で実行。

JavaScript とは異なり、WebAssembly は低レベルのバイナリ形式であるため、手動で記述することを目的としていません。代わりに、他の言語からコンパイルして作成します。

2. Node.js における WebAssembly サポート

Node.js は、ブラウザと同様にグローバルな WebAssembly オブジェクトを通じて WebAssembly を標準サポートしています。

使用している Node.js バージョンが WebAssembly をサポートしているか確認する方法:

console.log(typeof WebAssembly === 'object'); // true であればサポート済み
console.log(WebAssembly);

       注意: WebAssembly のサポートは Node.js v8.0.0 から始まり、それ以降のバージョンで継続的に改善されています。

3. Node.js での WebAssembly の使用方法

Node.js の WebAssembly API は、モジュールを操作するためのいくつかのメソッドを提供しています。

メソッド説明
WebAssembly.compile()WebAssembly バイナリコードを WebAssembly モジュールにコンパイルする
WebAssembly.instantiate()WebAssembly コードをコンパイルし、インスタンス化する
WebAssembly.validate()WebAssembly バイナリ形式が有効かどうかを検証する
WebAssembly.Moduleコンパイル済みの WebAssembly モジュールを表す
WebAssembly.Instanceインスタンス化された WebAssembly モジュールを表す
WebAssembly.MemoryWebAssembly のメモリを表す

以下は、WebAssembly モジュールを読み込んで実行する基本的な例です:

const fs = require('fs');

// WebAssembly バイナリファイルを読み込む
const wasmBuffer = fs.readFileSync('./simple.wasm');

// モジュールをコンパイルしてインスタンス化する
WebAssembly.instantiate(wasmBuffer).then(result => {
  const instance = result.instance;
  
  // エクスポートされた 'add' 関数を呼び出す
  const sum = instance.exports.add(2, 3);
  console.log('2 + 3 =', sum); // 出力: 2 + 3 = 5
});

       注意: この例の simple.wasm ファイルは、add 関数をエクスポートするコンパイル済みの WebAssembly モジュールである必要があります。通常、C、C++、または Rust のコードからコンパイルして作成します。

4. さまざまな言語での開発

Node.js で使用するために、さまざまな言語を WebAssembly にコンパイルできます。

4.1 C/C++ と Emscripten

Emscripten は、WebAssembly を出力する C/C++ 用のコンパイラツールチェーンです。

C コードの例 (add.c):

#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
  return a + b;
}

WebAssembly へのコンパイル:

emcc add.c -s WASM=1 -s EXPORTED_FUNCTIONS='["_add"]' -o add.js

4.2 Rust と wasm-pack

wasm-pack は、Rust で生成された WebAssembly をビルドするためのツールです。

Rust コードの例 (src/lib.rs):

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
   a + b
}

wasm-pack でビルド:

wasm-pack build --target nodejs

5. 高度な WebAssembly の活用

5.1 複雑なデータ構造の操作

JavaScript と WebAssembly の間で複雑なデータをやり取りするには、慎重なメモリ管理が必要です。

例:WebAssembly への配列の受け渡し

// JavaScript コード
const wasmModule = await WebAssembly.instantiate(wasmBuffer, {
  env: {
    // 1ページ (64KB) のメモリを割り当て
    memory: new WebAssembly.Memory({ initial: 1 })
  }
});

// 10個の整数(各4バイト)の配列用にメモリを確保
const arraySize = 10;
const ptr = wasmModule.exports.alloc(arraySize * 4);
const intArray = new Int32Array(wasmModule.exports.memory.buffer, ptr, arraySize);

// 配列に値を詰める
for (let i = 0; i < arraySize; i++) {
  intArray[i] = i * 2;
}

// 配列を処理する WebAssembly 関数を呼び出す
const sum = wasmModule.exports.processArray(ptr, arraySize);
console.log('配列の合計:', sum);

// メモリの解放を忘れないこと
wasmModule.exports.dealloc(ptr, arraySize * 4);

対応する C コード(WebAssembly にコンパイル):

#include <stdlib.h>

int* alloc(int size) {
  return (int*)malloc(size);
}

void dealloc(int* ptr, int size) {
  free(ptr);
}

int processArray(int* array, int length) {
  int sum = 0;
  for (int i = 0; i < length; i++) {
    sum += array[i];
  }
  return sum;
}

5.2 WebAssembly によるマルチスレッド処理

WebAssembly は、Web Workers と SharedArrayBuffer を通じてマルチスレッドをサポートしています。

例:WebAssembly による並列処理

// main.js
const workerCode = `
  const wasmModule = await WebAssembly.instantiate(wasmBuffer, {
    env: { memory: new WebAssembly.Memory({ initial: 1, shared: true }) }
  });

  self.onmessage = (e) => {
    const { data, start, end } = e.data;
    const result = wasmModule.exports.processChunk(data, start, end);
    self.postMessage({ result });
  };
`;

// ワーカープールの作成
const workerCount = navigator.hardwareConcurrency || 4;
const workers = Array(workerCount).fill().map(() => {
  const blob = new Blob([workerCode], { type: 'application/javascript' });
  return new Worker(URL.createObjectURL(blob));
});

// データの並列処理
async function processInParallel(data, chunkSize) {
  const results = [];
  let completed = 0;

  return new Promise((resolve) => {
    workers.forEach((worker, i) => {
      const start = i * chunkSize;
      const end = Math.min(start + chunkSize, data.length);

      worker.onmessage = (e) => {
        results[i] = e.data.result;
        completed++;

        if (completed === workerCount) {
          resolve(results);
        }
      };

      worker.postMessage({ data, start, end });
    });
  });
}

5.3 WebAssembly のデバッグ

WebAssembly のデバッグは困難な場合がありますが、最新のツールが役立ちます。

Emscripten でソースマップを使用する

# デバッグ情報とソースマップを含めてコンパイル
emcc -g4 --source-map-base http://localhost:8080/ -s WASM=1 -s EXPORTED_FUNCTIONS='["_main","_my_function"]' -o output.html source.c

Chrome デベロッパーツールでのデバッグ

  1. Chrome デベロッパーツール (F12) を開く
  2. "Sources" タブに移動
  3. ファイルツリーから WebAssembly ファイルを見つける
  4. JavaScript と同様にブレークポイントを設定し、変数を検査する

6. 実践的な WebAssembly の活用例

6.1 WebAssembly による画像処理

WebAssembly は、画像処理のような CPU インテンシブなタスクに非常に適しています。

// 画像処理用の JavaScript ラッパー
async function applyFilter(imageData, filterType) {
  const { instance } = await WebAssembly.instantiate(wasmBuffer, {
    env: { memory: new WebAssembly.Memory({ initial: 1 }) }
  });

  const { width, height, data } = imageData;

  // 画像データ用のメモリ割り当て
  const imageDataSize = width * height * 4; // RGBA
  const imageDataPtr = instance.exports.alloc(imageDataSize);

  // 画像データを WebAssembly メモリにコピー
  const wasmMemory = new Uint8Array(instance.exports.memory.buffer);
  wasmMemory.set(new Uint8Array(data.buffer), imageDataPtr);

  // フィルタの適用
  instance.exports.applyFilter(imageDataPtr, width, height, filterType);

  // 結果を ImageData にコピーして戻す
  const resultData = new Uint8ClampedArray(
    wasmMemory.slice(imageDataPtr, imageDataPtr + imageDataSize)
  );

  // 割り当てたメモリを解放
  instance.exports.dealloc(imageDataPtr, imageDataSize);

  return new ImageData(resultData, width, height);
}

6.2 クリプトグラフィー(暗号化)

WebAssembly を使用した高性能な暗号化操作:

// 例:WebAssembly を使用した暗号化
async function encryptData(data, keyMaterial) {
  const { instance } = await WebAssembly.instantiateStreaming(
    fetch('crypto.wasm'),
    { env: { memory: new WebAssembly.Memory({ initial: 1 }) } }
  );

  // IV(初期化ベクトル)の生成
  const iv = window.crypto.getRandomValues(new Uint8Array(12));

  // データの準備
  const dataBytes = new TextEncoder().encode(JSON.stringify(data));
  const dataPtr = instance.exports.alloc(dataBytes.length);
  new Uint8Array(instance.exports.memory.buffer, dataPtr, dataBytes.length)
    .set(dataBytes);

  // WebAssembly を使用してデータを暗号化
  const encryptedDataPtr = instance.exports.encrypt(dataPtr, dataBytes.length);

  // WebAssembly メモリから暗号化データ取得
  const encryptedData = new Uint8Array(
    instance.exports.memory.buffer,
    encryptedDataPtr,
    dataBytes.length 
  );

  // クリーンアップ
  instance.exports.dealloc(dataPtr, dataBytes.length);

  return {
    iv: Array.from(iv),
    encryptedData: Array.from(encryptedData)
  };
}

7. パフォーマンス比較

パフォーマンス上の利点を示すために、再帰的なフィボナッチ関数の JavaScript 実装と WebAssembly 実装を比較してみましょう。

JavaScript 実装:

function fibonacciJS(n) {
  if (n <= 1) return n;
  return fibonacciJS(n - 1) + fibonacciJS(n - 2);
}

C 実装(WebAssembly にコンパイル):

#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int fibonacci_wasm(int n) {
  if (n <= 1) return n;
  
  int a = 0, b = 1, temp;
  for (int i = 2; i <= n; i++) {
    temp = a + b;
    a = b;
    b = temp;
  }
  
  return b;
}

パフォーマンス比較コード:

const fs = require('fs');
const { performance } = require('perf_hooks');

const wasmBuffer = fs.readFileSync('./fibonacci.wasm');

function fibonacciJS(n) {
  if (n <= 1) return n;
  return fibonacciJS(n - 1) + fibonacciJS(n - 2);
}

WebAssembly.instantiate(wasmBuffer).then(result => {
  const { fibonacci_wasm } = result.instance.exports;
  
  const n = 40;
  
  // WebAssembly のパフォーマンス計測
  const wasmStart = performance.now();
  const wasmResult = fibonacci_wasm(n);
  const wasmEnd = performance.now();
  
  // JavaScript のパフォーマンス計測
  const jsStart = performance.now();
  const jsResult = fibonacciJS(n);
  const jsEnd = performance.now();
  
  console.log(`Fibonacci(${n})`);
  console.log(`WebAssembly: ${wasmResult} (${(wasmEnd - wasmStart).toFixed(2)} ms)`);
  console.log(`JavaScript: ${jsResult} (${(jsEnd - jsStart).toFixed(2)} ms)`);
});

WebAssembly バージョンは、再帰的なアプローチよりもはるかに高速な反復アルゴリズムを使用しています。アルゴリズムが同一であっても、コンパイル済みである WebAssembly は CPU 集中的な操作において通常 JavaScript よりも高いパフォーマンスを発揮します。

8. 実世界でのアプリケーション

Node.js で WebAssembly を使用している有名なライブラリ:

  • Sharp: 高性能な画像処理 (C++)
  • ffmpeg.wasm: ビデオおよびオーディオ処理 (C)
  • sql.js: JavaScript 用の SQLite (C)
  • zxing-wasm: バーコードスキャン (C++)
  • TensorFlow.js: 機械学習 (C++)

9. メモリ管理

WebAssembly モジュールは、線形メモリ(Linear Memory)上で動作します。これは JavaScript と WebAssembly の両方からアクセス可能な、連続した可変長バイト配列です。

9.1 WebAssembly メモリの理解

WebAssembly メモリは「ページ」単位で構成され、各ページは 64KB (65,536 バイト) です。

  • initial: ページの初期数(最小サイズ)
  • maximum: メモリが拡張できる最大ページ数(任意)
  • shared: スレッド間でメモリを共有するかどうか

9.2 メモリの作成とアクセス

// 1ページ(64KB)から始まり、最大10ページ(640KB)のメモリを作成
const memory = new WebAssembly.Memory({
  initial: 1,
  maximum: 10
});

// JavaScript で型付き配列としてメモリにアクセス
let bytes = new Uint8Array(memory.buffer);

// メモリにデータを書き込む
for (let i = 0; i < 10; i++) {
  bytes[i] = i * 10; // 0, 10, 20, ..., 90 を書き込む
}

// メモリからデータを読み込む
console.log('メモリ内容:', bytes.slice(0, 10));

// メモリを1ページ増やす(以前のサイズをページ単位で返す)
const previousPages = memory.grow(1);
console.log(`メモリが ${previousPages} ページから ${memory.buffer.byteLength / 65536} ページに拡張されました`);

// 重要:メモリ拡張後、元の ArrayBuffer は切り離されるため、新しいビューを作成する必要があります
bytes = new Uint8Array(memory.buffer);
console.log('現在のメモリサイズ:', bytes.length, 'バイト');

       警告: WebAssembly メモリが拡張されると、基盤となる ArrayBuffer は切り離され(detached)、新しいものが作成されます。そのため、メモリ拡張後はすべての型付き配列ビューを再作成する必要があります。

9.3 異なる型付き配列ビューの使用

同じメモリに対して異なるビューを作成し、データをさまざまな方法で解釈できます。

const memory = new WebAssembly.Memory({ initial: 1 });

// 同じメモリに対する異なるビュー
const bytes = new Uint8Array(memory.buffer);  // 8ビット符号なし整数
const ints = new Int32Array(memory.buffer);   // 32ビット符号付き整数
const floats = new Float32Array(memory.buffer); // 32ビット浮動小数点

// メモリの先頭に整数を書き込む
ints[0] = 42;

// 同じメモリ位置をバイトとして見る
console.log('42 のバイト表現:', Array.from(bytes.slice(0, 4)));

// 浮動小数点を書き込む
floats[1] = 3.14159;

// 浮動小数点をバイトおよび整数として表示
const floatByteOffset = 1 * Float32Array.BYTES_PER_ELEMENT;
const floatIntValue = ints[floatByteOffset / Int32Array.BYTES_PER_ELEMENT];
console.log('3.14159 のバイト表現:', Array.from(bytes.slice(floatByteOffset, floatByteOffset + 4)));
console.log('3.14159 の int32 表現:', floatIntValue);

10. JavaScript との統合

Node.js では、WebAssembly と JavaScript をシームレスに連携させることができます。

const fs = require('fs');
const wasmBuffer = fs.readFileSync('./math.wasm');

// WebAssembly を使用する JavaScript 関数
async function calculateFactorial(n) {
  const result = await WebAssembly.instantiate(wasmBuffer);
  const wasm = result.instance.exports;
  
  // WebAssembly の階乗関数を使用
  return wasm.factorial(n);
}

// 統合関数の使用
async function main() {
  console.log('階乗の計算:');
  for (let i = 1; i <= 10; i++) {
    const result = await calculateFactorial(i);
    console.log(`${i}! = ${result}`);
  }
}

main().catch(console.error);

       ベストプラクティス: アプリケーションのロジックは開発効率の高い JavaScript で記述し、パフォーマンスが重要な計算部分にのみ WebAssembly を使用するのが理想的です。

11. まとめ

WebAssembly は、以下の機能を提供することで Node.js の可能性を広げます:

  • C、C++、Rust などの言語からコンパイルされたコードの実行。
  • 計算負荷の高いタスクにおけるニアネイティブなパフォーマンスの実現。
  • 他言語の既存ライブラリやコードベースの再利用。
  • ブラウザとサーバー間でのコード共有(アイソモーフィックなコード)。

これにより、Node.js は単なる I/O 特化型のプラットフォームを超え、高性能計算が要求される幅広いアプリケーションに最適なプラットフォームへと進化しています。