esbuildでWebAssemblyを無理矢理埋め込む
公開日:
カテゴリ: JavaScript
ちょっと思い立ってやってみたくなったのでやってみた。
TL;DR
- esbuildのloaderでwasmファイルをバイナリとして埋め込むことが可能
- emscripten製のwasmは呼び出し時の引数のwasmBinaryプロパティにwasm自体のArrayBufferを入れることで無理矢理動かせる
- 基本的にメリットは少ない- 1ファイルにバンドルすることでローカル実行時にCORSに引っかからない利点はある
 
- .wasmファイルがBase64形式でバンドルされるため、ファイルサイズは通常よりも大幅に増加する
esbuildでwasmをバンドルする
esbuildではloaderオプションを使用することで、各種ファイルをimportでロードすることができます。
今回は.wasmファイルをbinaryとして読み込むように設定します。binaryとして読み込んだファイルはimportするとUint8Arrayとして扱うことができます。
{
	bundle: true,
	loader: {
		".wasm": "binary"
	}
}読み込む側は以下のようなコードになります。
import wasm_binary from "./example.wasm";
WebAssembly.instantiate(wasm_binary.buffer).then(results => {
	// 処理
});(オプション)型定義
TypeScriptでimportするにはwasmの型定義が必要です。
declare module "*.wasm" {
	export default Uint8Array;
}Emscripten製のwasmをバンドルする
自分で作成したwasmファイルであればここまでの方法で解決しますが、世の中の多くのWebAssemblyはEmscriptenで作成されており、wasmをロードするJavaScriptまでがセットでビルドされます。
このビルド成果物は一般的なwasmと同様にfetch()でwasmファイルをロードするので、そのままではバンドルできません。かといって、このJavaScriptを使用せずに実装するのは難しめです。
ということで、色々いじって無理矢理バンドルさせてみます。
無理矢理ロードさせる
今回は@sqlite.org/sqlite-wasmをバンドルしてみます。
emscriptenでビルドされたwasmは、設定オブジェクトのwasmBinaryプロパティからwasmをロードしています。wasmBinaryプロパティが無い場合はwasmファイルをfetch()しに行くので、ArrayBufferを無理矢理差し込んでしまうという魂胆です。
wasmBinaryは内部用のプロパティで、大抵の場合は型定義ファイルには存在しないため、TypeScriptでコーディングする場合は自力で型定義ファイルを書くか// @ts-ignoreで型チェックを強制的に対象外にする必要があります。
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
import sqlite_wasm from "@sqlite.org/sqlite-wasm/sqlite3.wasm";
sqlite3InitModule({
	// @ts-ignore
	wasmBinary: sqlite_wasm.buffer
}).then(sqlite => {
	// ここに処理
});import.metaの対策
esbuildでバンドルすると、import.metaが空オブジェクトになってしまうので、適当な文字列に置換します。あまりこのような書き換えはしたくないですが、どうにもならなさそうなので今回は目を瞑ります。
{
	bundle: true,
	loader: {
		".wasm": "binary"
	},
	define: {
		"import.meta.url": "location.href"
	}
}結論
非公開のプロパティを使用しているので、将来のアップデートでは使えなくなる可能性が高いです。また既存コードのimport.meta.urlを置き換えたりなど、かなり無理をした対応をしているので、保守性はかなり悪めです。
esbuildのバイナリローダーは、JSファイルにバイナリファイルをバンドルする都合上base64形式でファイルをバンドルするため、ファイルサイズは大きく増大します。小さなwasmファイルであればそれほど気になりませんが、大抵はそれなりのファイルサイズがあることが多いので、転送量は無視できないくらい大きくなりそうです(base64は8bitで1Byte→6bitで1Byteに変換するため約1.3倍)。
メリットとしてはfetch()無しでwasmを使えるため、CORSに引っかからないことでしょうか。ローカルにサーバを立てずにHTMLファイルだけで完結するアプリを作成できそうです。とても限定的ですが、使い方によってはありかも・・・?
カテゴリ: JavaScript