🙆‍♀️

emscriptenビルドを行った独自モジュールと併用するとsqlite3_mallocがないエラーの対処

2024/04/28に公開

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行付近の以下。

sqlite3-bundler-friendly.mjs
for (const key of [keyAlloc, keyDealloc, keyRealloc]) {
            const f = wasm.exports[key];
            if (!(f instanceof Function))
              toss3('Missing required exports[', key, '] function.');
          }

この部分のブレークポイントでwasm.exportsを確認すると……
ローカルではこう。
wasm.exportsのローカルでの結果画像

一方公式サイトでは……。
wasm.exportsの公式サイトの結果画像
~~画像の一部省略~~

比較すると明らかにsqlite3の関数がない!
しかし肝心なこのwasm.exportsの中身がどこで定義されているのかがよくわからない。

というかスクリプトは一字一句同じなため、もはや違いはなく、外部の要因しか考えられない。
が、どこからその要因が来るのか全くわからないため、大本の呼び出しコードであるconst sqlite3 = await sqlite3InitModule()からトレースすることに。

何度も見てみるうちに6055行付近で使用されているconfigの中身が気になり、wasmを定義している少し手前の5700行付近の以下の部分を発見。

sqlite3-bundler-friendly.mjs
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を見てみると……
ローカルではこう、
apiConfigのローカルでの結果画像
一方公式サイトでの結果を抜粋すると、
apiConfigの公式サイトの結果画像

こうなっており、明らかに上のwasm.exportsと様子が一致する。

ここで5700行付近で用いられているsqlite3ApiConfigの他の呼び出し位置を見ると気になる部分を発見。

sqlite3-bundler-friendly.mjs
('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になり、それ以外の場合はwasmExportswasm.exportsになるみたい。

この肝心のwasmExportsは、公式サイトだとundefinedになるのに対し、ローカルでは中身がある。そして、この変数はこのスクリプトの中にこの部分しか出てこない。つまり、犯人はwasmExportsの中身の違いと判明。

ここまで来ると次はwasmExportsがどこから来るのかを検索すればOKか?
というわけで普段使いのVScodeで検索してみたところ……。

wasmExportsをリポジトリのファイル内で横断検索した結果

意外とヒット数が少ない!

犯人の特定

犯人はビルド後のJSファイルであるmhrise-charm-substitutes-search.js

ビルド前の文はあまり触っておらず、少なくとも新たなライブラリを導入する等はしていない。なのでビルドで追加されたコードであり、ビルド環境が原因と判明。

ビルドの一部は以下のdockerで行われる。

Makefile
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)

sqlite3-api-cleanup.js
    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