VitePressでWebAssemblyを使う
VitePressのコンテンツ内でWebAssembly(WASM)を利用する際の,汎用的な設計をまとめます。ここではRay Tracingに依存しない共通論点(配置,読み込み,制約)を扱います。
配置方針
WASMの生成物は,通常,グルーコード(.js / .mjs)とバイナリ(.wasm)の2ファイルで構成されます。
- wasm-bindgen(Rust):
<name>.jsと<name>_bg.wasm - Emscripten(C/C++):
<name>.mjsと<name>.wasm
このプロジェクトでは docs/public/wasm/ 配下に置きます。public 配下のファイルはビルド時にそのまま dist へコピーされるため,URLを安定させやすく,バイナリを srcDir 外に分離できます。
Vite開発サーバーの制約
public 配下のJSファイルは,開発サーバー上で import() できません。
// dev serverでは失敗する
const wasm = await import('/wasm/my_module/my_module.js')Vite開発サーバーは,publicディレクトリ内のJSファイルへのES module import()リクエストをインターセプトして以下のエラーを返します。
Failed to load url /wasm/my_module/my_module.js ...
This file is in ../public and will be copied as-is during build without going through
the plugin transforms, and therefore should not be imported from source code.
It can only be referenced via HTML tags.これはViteの仕様です。/* @vite-ignore */ を付けても,この制約は回避できません。なお,静的サイトとして生成後はこの制約はなく,fetch()でアクセスできます。開発サーバー固有の問題です。
解決策:fetch + Blob URL
fetch() でグルーコードのテキストを取得し,Blob URLにして import() します。
const response = await fetch(jsPath)
if (!response.ok) throw new Error(`HTTP ${response.status} fetching ${jsPath}`)
const jsText = await response.text()
const blob = new Blob([jsText], { type: 'text/javascript' })
const blobUrl = URL.createObjectURL(blob)
let wasmModule
try {
wasmModule = await import(/* @vite-ignore */ blobUrl)
} finally {
URL.revokeObjectURL(blobUrl)
}import.meta.url問題と対処
Blob URL経由で読み込んだモジュールでは,import.meta.url が blob: URLになります。wasm-bindgen/Emscriptenは内部でWASMバイナリの相対パス解決に import.meta.url を使うため,バイナリURLを明示する必要があります。
VueコンポーネントのWASM読み込みパターン
wasm-bindgen(Rust)
wasm-bindgenが生成するJSは,デフォルトエクスポートの初期化関数にWASMバイナリのURLを引数として渡せます。wasm-bindgenの慣例では .js と同じディレクトリに _bg.wasm という名前でバイナリが置かれます。
onMounted(async () => {
const jsPath = withBase('/wasm/my_module/my_module.js')
// fetch + Blob URLでimport(Vite開発サーバーのimport制限を回避)
const response = await fetch(jsPath)
if (!response.ok) throw new Error(`HTTP ${response.status} fetching ${jsPath}`)
const jsText = await response.text()
const blob = new Blob([jsText], { type: 'text/javascript' })
const blobUrl = URL.createObjectURL(blob)
let wasm
try {
wasm = await import(/* @vite-ignore */ blobUrl)
} finally {
URL.revokeObjectURL(blobUrl)
}
// import.meta.url問題の回避:WASMバイナリURLを明示的に渡す
const wasmBinaryUrl = new URL(
jsPath.replace(/\.js$/, '_bg.wasm'),
window.location.origin
)
await wasm.default(wasmBinaryUrl)
// 以降はモジュールのexportを呼ぶ
const result = wasm.my_function()
})Emscripten(C/C++)
Emscriptenが生成するJSは,初期化関数に渡すオブジェクトのlocateFileコールバックでWASMバイナリのパスを上書きできます。
onMounted(async () => {
const mjsPath = withBase('/wasm/another/another.mjs')
// fetch + Blob URLでimport(Vite開発サーバーのimport制限を回避)
const response = await fetch(mjsPath)
if (!response.ok) throw new Error(`HTTP ${response.status} fetching ${mjsPath}`)
const jsText = await response.text()
const blob = new Blob([jsText], { type: 'text/javascript' })
const blobUrl = URL.createObjectURL(blob)
let wasmModule
try {
wasmModule = await import(/* @vite-ignore */ blobUrl)
} finally {
URL.revokeObjectURL(blobUrl)
}
// import.meta.url問題の回避:locateFileでWASMバイナリのURLを明示的に指定
const module = await wasmModule.default({
locateFile: (path) => withBase('/wasm/another/' + path)
})
// 以降はmoduleのexportを呼ぶ
const result = module._my_function()
})CSP
Content Security Policy(CSP)を明示する環境では,script-src blob: の許可が必要です。
プロジェクト固有の話題
raytracing_demos の構成(common,r1XX-*),PPM文字列の生成,PPMRenderer での描画など,Ray Tracing固有の実装は ja/raytracing/0 を参照してください。