雑記帳

整理しない情報集

更新情報

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