emscriptenビルドを行った独自モジュールと併用するとsqlite3_mallocがないエラーの対処
emscriptenビルドを行った独自のwasmとnpmの@sqlite.org/sqlite-wasmを使用した際にsqlite3_mallocが読み込めないときの原因追及と対処法についての記録です。
結論
独自のWebAssemblyモジュールのビルドに使用しているemscripten/emsdkのバージョンが噛み合わないためエラーが出る。そのため、emscripten/emsdkのバージョンをlatest(当時3.1.58)から3.1.40に下げると動いた(emscripten/emsdkは3.1.44までなら同様にビルド可能)。
経緯
前日譚
モンハンライズを今更プレイしてかなり潤沢にお守りが集まったため、護石スキャナを使用していたが、お守りの削除機能がないなど不便!なので、リポジトリへGOし、早速手順通りにビルドするも護石管理画面のローディングが終わらず、動かない……。
エラー内容
chromeのDevToolsを開き、エラーコンソールを見ると以下のエラーが発生。
sqlite3ApiBootstrap() error: SQLite3Error: Missing required exports[ sqlite3_malloc ] function.
at SQLite3Error.toss (sqlite3-bundler-friendly.mjs:5790:17)
at sqlite3ApiBootstrap (sqlite3-bundler-friendly.mjs:6057:15)
at sqlite3-bundler-friendly.mjs:14365:32
at callRuntimeCallbacks (sqlite3-bundler-friendly.mjs:669:26)
at postRun (sqlite3-bundler-friendly.mjs:448:7)
at doRun (sqlite3-bundler-friendly.mjs:5664:9)
at run (sqlite3-bundler-friendly.mjs:5676:9)
at runCaller (sqlite3-bundler-friendly.mjs:5635:23)
at removeRunDependency (sqlite3-bundler-friendly.mjs:496:11)
at receiveInstance (sqlite3-bundler-friendly.mjs:594:9)
(ちなみに他にもあるけど……。)
何が原因?試したこと
まったくWebAssemblyを触ったことがなく、npmも雰囲気でしか触ってないため、まずはシークレットモードで開いてみると正しく動く!しかし、しばらく試行錯誤すると動かなくなる(シークレットモードで動く理由は不明……。)
その後、
- 護石スキャナ公式のreleseブランチの導入
- WebAssemblyモジュールのビルドを抜いてnpm run dev
- npmや@sqlite.org/sqlite-wasmのバージョンを少し下げる
等々をしたがあまりうまく動かないため、ブレークポイントを護石スキャナ公式サイトとローカル版で同じ場所にブレークポイントを配置し、変数の比較しながらデバッグすることに。
ほとんどサイゼリヤの間違い探し
実際のコードを見ると、エラーを吐く部分は6055行付近の以下。
for (const key of [keyAlloc, keyDealloc, keyRealloc]) {
const f = wasm.exports[key];
if (!(f instanceof Function))
toss3('Missing required exports[', key, '] function.');
}
この部分のブレークポイントでwasm.exports
を確認すると……
ローカルではこう。
一方公式サイトでは……。
~~画像の一部省略~~
比較すると明らかにsqlite3
の関数がない!
しかし肝心なこのwasm.exports
の中身がどこで定義されているのかがよくわからない。
というかスクリプトは一字一句同じなため、もはや違いはなく、外部の要因しか考えられない。
が、どこからその要因が来るのか全くわからないため、大本の呼び出しコードであるconst sqlite3 = await sqlite3InitModule()
からトレースすることに。
何度も見てみるうちに6055行付近で使用されているconfig
の中身が気になり、wasm
を定義している少し手前の5700行付近の以下の部分を発見。
globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
apiConfig = globalThis.sqlite3ApiConfig ||
sqlite3ApiBootstrap.defaultConfig,
) {
if (sqlite3ApiBootstrap.sqlite3) {
console.warn(
'sqlite3ApiBootstrap() called multiple times.',
'Config and external initializers are ignored on calls after the first.',
);
return sqlite3ApiBootstrap.sqlite3;
}
apiConfig
を見てみると……
ローカルではこう、
一方公式サイトでの結果を抜粋すると、
こうなっており、明らかに上のwasm.exports
と様子が一致する。
ここで5700行付近で用いられているsqlite3ApiConfig
の他の呼び出し位置を見ると気になる部分を発見。
('use strict');
if ('undefined' !== typeof Module) {
const SABC = Object.assign(
Object.create(null),
{
exports:
'undefined' === typeof wasmExports ? Module['asm'] : wasmExports,
memory: Module.wasmMemory,
},
globalThis.sqlite3ApiConfig || {},
);
globalThis.sqlite3ApiConfig = SABC;
どうやらこのexports:
の部分がwasm.exports
を決めているらしい。そしてこのwasmExports
がunderfinedの場合Module['asm']
の中身がwasm.exports
になり、それ以外の場合はwasmExports
がwasm.exports
になるみたい。
この肝心のwasmExports
は、公式サイトだとundefinedになるのに対し、ローカルでは中身がある。そして、この変数はこのスクリプトの中にこの部分しか出てこない。つまり、犯人はwasmExports
の中身の違いと判明。
ここまで来ると次はwasmExports
がどこから来るのかを検索すればOKか?
というわけで普段使いのVScodeで検索してみたところ……。
意外とヒット数が少ない!
犯人の特定
犯人はビルド後のJSファイルであるmhrise-charm-substitutes-search.js
ビルド前の文はあまり触っておらず、少なくとも新たなライブラリを導入する等はしていない。なのでビルドで追加されたコードであり、ビルド環境が原因と判明。
ビルドの一部は以下のdockerで行われる。
docker run --rm -v `pwd`:/src emscripten/emsdk\
em++ -I./lib -std=c++17 -O2 -fno-exceptions\
-s WASM=1 -s NO_EXIT_RUNTIME=1 --bind\
-s ALLOW_MEMORY_GROWTH=1\
-s ASSERTIONS=1 -s SAFE_HEAP=1\
-gsource-map --source-map-base 'http://localhost:5000/cpp-source/'\
src/main.cpp\
-o build/mhrise-charm-substitutes-search.html\
--shell-file template.html
少し調べると、ローカルでemsdkの環境を作るのは少し面倒らしく、docker imageを使用するほうがはるかに簡単になりそうな感じがする。しかし、問題はemscripten/emsdk
のタグが指定されておらず、常にlatestのイメージがpullされること。
なので冒頭の通り、(モンハンriseの流行りを考えて)一年ほど前の3.1.40を指定したところ、wasmExports
が消失、無事に動くようになった。
原因について
前述の通り、sqliteはwasmExports
がない場合はModule['asm']
を使う。
emscriptenの変更履歴を見ているととあるバージョンにたどり着いた。
3.1.44 - 07/25/23g
musl libc updated from v1.2.3 to v1.2.4. (#19812)
The EM_LOG_FUNC_PARAMS flag to emscripten_log/emscripten_get_callstack has been deprecated and no longer has any effect. It was based on a long-deprecated JS API. (#19820)
The internal read_ and readAsync functions no longer handle data URIs. (Higher-level functions are expected to handle that themselves, before calling.) This only effects builds that use -sSINGLE_FILE or --memory-init-file. (#19792)
The asm property of the Module object (which held the raw exports of the wasm module) has been removed. Internally, this is now accessed via the wasmExports global. If necessary, it is possible to export wasmExports on the Module object using -sEXPORTED_RUNTIME_METHODS=wasmExports. (#19816)
Embind now supports generating TypeScript definition files using the --embind-emit-tsd <filename> option.
これがすべての元凶だ……!なので3.1.40で動作するのは正しい動作となる。
ところでasmってなんだ?となったが、どうやらasm.jsというものがあり、その互換として残してあるような気がする。
sqlite側はなぜこのような実装になってるのか調べると、こんなページが
2023-07-27
01:38
Accommodate a breaking change in emcc 3.1.44. (check-in: 2c5dd341 user: stephan tags: trunk)
01:35
Accommodate a breaking change in emcc 3.1.44. (check-in: 4ce38603 user: stephan tags: jspi)
Object.create(null), {
exports: ('undefined'===typeof wasmExports)
? Module['asm']/* emscripten <=3.1.43 */
: wasmExports /* emscripten >=3.1.44 */,
memory: Module.wasmMemory /* gets set if built with -sIMPORTED_MEMORY */
},
globalThis.sqlite3ApiConfig || {}
);
どうやらわざわざ変えたようなのに、emscripten >=3.1.44
の環境でエラーが出るのはなぜ??とりあえずこのコードをこの変更前の以下に書き換えて動かしてみる。
Object.create(null), {
exports: Module['asm'],
memory: Module.wasmMemory /* gets set if built with -sIMPORTED_MEMORY */
},
正しく動いている
書き換えると正しく動くが、他の副作用がありそうで怖い。
もしくはnpmの@sqlite.org/sqlite-wasmを3.41.2くらいまで下げると動く気がするが、これも他の依存関係が怖い。
なので結論の対処が今のところベストな気がする。
どうしてこのような状況なのかはよくわからないのが謎である。
Discussion