Chapter 2 / 3 ― 中級編

SharedArrayBuffer + COOP/COEP + Rust→WASM ビルド

cross-origin isolation を有効化し、shared-memory 版 WASM を Rust ソースから自前でビルド、WebWorker から SAB 経由で同一 linear memory に同時アクセスする。「SaaS PaaS の既定」で詰まる典型 3 点 (header / build flag / module 共有) を順番に潰す。

2.1 cross-origin isolation の有効化 (COOP / COEP)

SharedArrayBuffer は Spectre 対策から cross-origin isolation された context でしか使えません。判定は以下:

// ブラウザの DevTools コンソールで確認
console.log(crossOriginIsolated);  // true なら SAB 使用可
console.log(typeof SharedArrayBuffer);  // 'function' なら OK

crossOriginIsolated === true になるには、HTML を返す HTTP server が以下 2 ヘッダを返す必要があります:

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

2.1.1 nginx 設定

server {
    listen 443 ssl http2;
    server_name rlm-app.example.jp;

    ssl_certificate     /etc/letsencrypt/live/rlm-app.example.jp/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/rlm-app.example.jp/privkey.pem;

    root /var/www/rlm-app;
    index index.html;

    # === cross-origin isolation 必須 2 ヘッダ ===
    add_header Cross-Origin-Opener-Policy   "same-origin" always;
    add_header Cross-Origin-Embedder-Policy "require-corp" always;

    # WASM は明示 MIME
    location ~ \.wasm$ {
        add_header Cross-Origin-Opener-Policy   "same-origin" always;
        add_header Cross-Origin-Embedder-Policy "require-corp" always;
        add_header Cross-Origin-Resource-Policy "same-origin" always;
        types { application/wasm wasm; }
    }

    # 静的アセットも CORP を要求
    location / {
        add_header Cross-Origin-Resource-Policy "same-origin" always;
    }
}

2.1.2 Apache 設定 (.htaccess)

<IfModule mod_headers.c>
    Header set Cross-Origin-Opener-Policy   "same-origin"
    Header set Cross-Origin-Embedder-Policy "require-corp"
    Header set Cross-Origin-Resource-Policy "same-origin"
</IfModule>

AddType application/wasm .wasm

2.1.3 Express (Node.js) ミドルウェア

// server.mjs
import express from 'express';
const app = express();

app.use((req, res, next) => {
  res.setHeader('Cross-Origin-Opener-Policy',   'same-origin');
  res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
  res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
  next();
});

app.use(express.static('./public', {
  setHeaders: (res, path) => {
    if (path.endsWith('.wasm')) {
      res.setHeader('Content-Type', 'application/wasm');
    }
  },
}));

app.listen(8443, () => console.log('https://localhost:8443/'));
SaaS ホスティングの罠: Vercel / Netlify / Cloudflare Pages 等の既定では COOP/COEP は付きません。各プラットフォームの "headers" 設定 (例: Vercel の vercel.jsonheaders) で明示的に追加する必要があります。3rd-party CDN リソースを参照していると CORP 違反で読込失敗するので、全アセットを同一オリジン化するか Cross-Origin-Resource-Policy: cross-origin を 3rd party 側で返す必要があります。

2.2 Rust ソースから shared-memory 版 WASM をビルドする

配布された WASM (Chapter 1) は single-thread 版です。WebWorker 間で linear memory を共有するには、Rust 側で WASM threads feature (+atomics +bulk-memory +mutable-globals) を有効にして再ビルドします。

2.2.1 Rust toolchain の準備

rustup install nightly
rustup component add rust-src --toolchain nightly

# wasm-pack
cargo install wasm-pack

# wasm32 target
rustup target add wasm32-unknown-unknown --toolchain nightly

2.2.2 Cargo.toml (RLM source crate 側)

[package]
name    = "slimetree-rlm-wasm"
version = "0.x.y"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
slimetree-rlm = { path = "../slimetree-rlm-core", default-features = false }
wasm-bindgen  = { version = "0.2", features = ["enable-interning"] }
js-sys        = "0.3"
web-sys       = { version = "0.3", features = ["console"] }

[profile.release]
opt-level   = "z"
lto         = true
codegen-units = 1
panic       = "abort"
strip       = "symbols"

2.2.3 ビルドコマンド (shared memory 有効化)

RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals" \
  rustup run nightly \
  wasm-pack build \
    --release \
    --target web \
    --out-dir ../public/wasm \
    -- -Z build-std=panic_abort,std

# 出力:
# ../public/wasm/slimetree_rlm.js          (ES module loader)
# ../public/wasm/slimetree_rlm_bg.wasm     (shared-memory WASM)
# ../public/wasm/slimetree_rlm.d.ts        (型定義)
ポイント: RUSTFLAGS+atomicsi32.atomic.* 等の WebAssembly atomic 命令を有効化し、+bulk-memorymemory.copy / memory.fill を有効化、+mutable-globals は WASM globals を可変にします。3 つ揃って初めて WebAssembly.Memory({ shared: true }) が生成可能。-Z build-std=panic_abort,std は libstd 自体を上記フラグで再ビルドするため nightly Rust 必須です。

2.3 main thread + Worker で SAB-backed RLM を共有する

shared-memory WASM をロードしたあと、WebAssembly.Memoryshared: true で生成し、それを main + Worker 双方の WASM インスタンスに渡します。

2.3.1 ディレクトリ構成

my-rlm-app/
├── index.html
├── main.js
├── audit-worker.js
└── wasm/
    ├── slimetree_rlm.js
    └── slimetree_rlm_bg.wasm

2.3.2 index.html

<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>SlimeTree-RLM SAB 共有</title>
</head>
<body>
  <h1>SlimeTree-RLM SAB-backed shared memory</h1>
  <pre id="log"></pre>
  <script type="module" src="./main.js"></script>
</body>
</html>

2.3.3 main.js (main thread)

import init, {
  SlimeTreeRLM,
  __wbindgen_thread_destroy,
} from './wasm/slimetree_rlm.js';

const log = (m) => { document.getElementById('log').textContent += m + '\n'; };

// --- pre-flight check ---
if (!crossOriginIsolated) {
  log('[FATAL] crossOriginIsolated=false. COOP/COEP ヘッダを確認してください');
  throw new Error('not cross-origin isolated');
}
if (typeof SharedArrayBuffer !== 'function') {
  log('[FATAL] SharedArrayBuffer 未対応');
  throw new Error('no SAB');
}

// --- shared WebAssembly.Memory を main で生成 ---
//   initial=256 page (16 MiB)、maximum=4096 page (256 MiB)
const sharedMemory = new WebAssembly.Memory({
  initial: 256,
  maximum: 4096,
  shared:  true,
});
log('[ok] WebAssembly.Memory (shared) を main で生成');

// --- WASM module を共有 memory で初期化 ---
const wasmModule = await init({
  module_or_path: './wasm/slimetree_rlm_bg.wasm',
  memory:         sharedMemory,
});
log('[ok] WASM module 初期化 (main)');

// --- RLM を SAB 上に構築 ---
const rlm = new SlimeTreeRLM({
  capacity: 16 * 1024 * 1024,
  audit:    true,
  mode:     'shared',         // SAB 共有モード
  memory:   sharedMemory,
});
log('[ok] RLM (shared mode) 構築完了');

// --- 監査 Worker を起動 ---
const auditWorker = new Worker('./audit-worker.js', { type: 'module' });

//   Worker に WASM module + shared memory + RLM の base ptr を渡す
auditWorker.postMessage({
  cmd:     'init',
  module:  wasmModule,         // 既コンパイル WebAssembly.Module
  memory:  sharedMemory,       // shared memory への参照
  rlm_ptr: rlm.shared_base_ptr(), // RLM の SAB 上の base ポインタ
});

auditWorker.onmessage = (ev) => {
  log('[worker] ' + JSON.stringify(ev.data));
};

// --- main から RLM に write を続け、Worker が並行で audit ---
let counter = 0;
setInterval(() => {
  const id = rlm.write({
    semantic_key: 'event:tick:' + counter,
    payload:      'tick at ' + new Date().toISOString(),
    source:       'main_thread',
  });
  log('[main write] id=' + id);
  counter++;
}, 500);

2.3.4 audit-worker.js (Worker)

import initWorker, {
  SlimeTreeRLM,
} from './wasm/slimetree_rlm.js';

let rlm = null;

self.onmessage = async (ev) => {
  if (ev.data.cmd === 'init') {
    // Worker 側で「同じ module + 同じ shared memory」で WASM 初期化
    await initWorker({
      module_or_path: ev.data.module,
      memory:         ev.data.memory,
    });

    // Worker 側でも RLM を attach (新規生成ではなく既存 SAB に attach)
    rlm = SlimeTreeRLM.attach_shared({
      memory:  ev.data.memory,
      base_ptr: ev.data.rlm_ptr,
    });

    self.postMessage({ event: 'attached', ok: true });

    // 0.7 秒ごとに audit chain を verify
    setInterval(() => {
      const r = rlm.verify_audit_chain();
      self.postMessage({
        event: 'audit',
        verified:     r.verified,
        record_count: r.record_count,
        head_hash:    r.head_hash.slice(0, 12) + '...',
      });
    }, 700);
  }
};

2.4 動作確認 ― 並行 write + 並行 audit

main が 500 ms ごとに write を続け、auditWorker が 700 ms ごとに verify_audit_chain を独立に走らせる。SAB が機能していれば、main が write した直後の record を Worker が同じメモリで観測でき、record_count が時間と共に増えながら verified=true が継続します。

[ok] WebAssembly.Memory (shared) を main で生成
[ok] WASM module 初期化 (main)
[ok] RLM (shared mode) 構築完了
[worker] {"event":"attached","ok":true}
[main write] id=0
[main write] id=1
[worker] {"event":"audit","verified":true,"record_count":2,"head_hash":"a3f1d2e8b4c9..."}
[main write] id=2
[main write] id=3
[worker] {"event":"audit","verified":true,"record_count":4,"head_hash":"7c2e8a1b9f4d..."}
...

SAB が機能していない場合 (Worker のメモリが独立してしまっている)、record_count は常に 0 のままになります。これが Chapter 1 の構成では到達できなかった水準です。

2.5 ハマる点 ― 中級で詰まる 6 つ

(1) crossOriginIsolated === false のまま気づかない

DevTools コンソールで毎セッション最初に crossOriginIsolated を必ず確認。false なら Chapter 2 のコードは一切動かない (SharedArrayBuffer コンストラクタが undefined)。

(2) CDN や 3rd-party 画像で COEP 違反

COEP require-corp 下では、3rd-party リソースが Cross-Origin-Resource-Policy を返さないと読込失敗。Google Fonts / 外部画像 / Analytics スクリプトを混在させているサイトは要素ごとに対応 (同一オリジン化 or credentialless モード)。

(3) wasm-pack 既定で shared 版が出ない

RUSTFLAGS + -Z build-std を渡さないと、見た目は WASM が出力されるが atomic 命令が無効。生成 wasm を wasm-objdump -h で確認し、shared memory セクションが存在することを検証。

(4) Worker と main で memory インスタンスを共有し忘れる

init({ memory: ... }) に同じ WebAssembly.Memory オブジェクトを渡さないと、Worker は独自の linear memory を確保し、SAB の意味がなくなる。postMessageWebAssembly.Memorytransferable ではなく cloneable (参照共有) なのでコピーされず転送されます。

(5) wasmModule の再コンパイルコスト

Worker ごとに fetch + WebAssembly.compile を繰り返すと遅い。main で 1 度コンパイルした WebAssembly.ModulepostMessage で渡せば、Worker 側はインスタンス化のみで済む (上記 main.jsmodule: wasmModule 参照)。

(6) SlimeTreeRLM.attach_shared の base_ptr 不整合

Worker 側が attach する際、main が生成した RLM の base ポインタを postMessage で渡し損ねると RlmAttachError。main 側で rlm.shared_base_ptr() の戻り値をログ確認、Worker 側の ev.data.rlm_ptr と一致しているかチェック。

2.6 中級編はここで完了 ― 次章への橋渡し

Chapter 2 で以下が動くようになりました。

  • COOP/COEP による cross-origin isolation の有効化 (nginx / Apache / Express)
  • Rust ソース → shared-memory 版 WASM の自前ビルド (atomics + bulk-memory)
  • WebAssembly.Memory({ shared: true }) を main + Worker で共有
  • main が write、Worker が並行で audit chain 検証

Chapter 3 では以下を扱います:

  • WebWorker pool (3〜N) で並列 write/read、競合制御
  • OPFS / IndexedDB / Node.js fs での audit chain 永続化
  • 実例 1: LLM ハルシネーション抑制ループ (LLM Worker + verify Worker + 抑制 hook)
  • 実例 2: Enterprise audit pipeline (Node.js worker_threads + SAB)