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/'));
vercel.json の headers) で明示的に追加する必要があります。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 の +atomics は i32.atomic.* 等の WebAssembly atomic 命令を有効化し、+bulk-memory は memory.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.Memory を shared: 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 の意味がなくなる。postMessage で WebAssembly.Memory は transferable ではなく cloneable (参照共有) なのでコピーされず転送されます。
(5) wasmModule の再コンパイルコスト
Worker ごとに fetch + WebAssembly.compile を繰り返すと遅い。main で 1 度コンパイルした WebAssembly.Module を postMessage で渡せば、Worker 側はインスタンス化のみで済む (上記 main.js の module: 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)
