私たちは「table index is out of bounds」エラーに直面している。これはWASMの関数テーブルインデックスが範囲外であることを示す。典型的には、C関数のエクスポート名や関数ポインタの扱いに問題がある可能性がある。また、Emscriptenのビルド設定や、エクスポートされた関数のシグネチャが一致していない場合にも発生する。
まず、現在のコードの問題点を洗い出す。
-
calc_lab.c: 各関数は EMSCRIPTEN_KEEPALIVE でエクスポートされている。問題はなさそう。
-
build_lab.sh: EXPORTED_FUNCTIONS に '_calc_sin' などが指定されているが、EMSCRIPTEN_KEEPALIVEを使っているので、エクスポートは自動的になるはず。しかし、明示的にリストアップしている。また、EXPORTED_RUNTIME_METHODS には 'ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8' が含まれている。OK。
-
calc_lab_worker.js: importScripts で読み込み、factory() を呼び出してモジュールを初期化。cwrap で各関数をラップ。問題は、cwrap の戻り値の型や引数型が正しいか。
-
index03.html: Worker を起動してメッセージを送信。
エラー「table index is out of bounds」は、関数ポインタ(関数テーブル)の呼び出し時に無効なインデックスを参照した場合に発生する。可能性としては、C側の関数がエクスポートされていない、または cwrap で指定した関数名が間違っている、または引数/戻り値の型が一致していないために、Emscriptenが内部的に間違った関数を呼び出そうとしている。
特に、cwrap の第2引数(戻り値の型)が 'number' で、第3引数が ['string', 'number', 'number'] など。これは正しい。
しかし、calc_lab.c の各関数は char* を返す(文字列へのポインタ)。Emscriptenでは char* は数値(ポインタ)として返される。cwrap で 'number' と指定しているので、ポインタが数値として得られる。その後、calcModule.UTF8ToString(ptr) で文字列に変換している。これは正しい。
エラーが起こるのは、関数テーブルにアクセスするとき、つまり、getWasmTableEntry などで関数ポインタを取得する際に、インデックスが範囲外。原因として考えられるのは、EXPORTED_FUNCTIONS に含めた関数名が、Cの関数名と一致していない(アンダースコアの有無など)か、あるいはエクスポートされていない関数を呼ぼうとしているか。
build_lab.sh の EXPORTED_FUNCTIONS には '_calc_sin' などが指定されているが、C側では EMSCRIPTEN_KEEPALIVE を使っているので、デフォルトでは _ が付く(EmscriptenはC関数名にアンダースコアを付けてエクスポートする)。そのため、リストは正しい。
ただ、EMSCRIPTEN_KEEPALIVE を使う場合、EXPORTED_FUNCTIONS に明示的に追加する必要はない(自動で追加される)が、追加しても問題ないはず。
別の可能性として、calc_lab_worker.js で self.calc_sin = calcModule.cwrap('calc_sin', ...) としているが、calc_sin という関数名はCで定義されているので、cwrap の第一引数は 'calc_sin' で正しい(アンダースコアは不要)。Emscriptenの cwrap は内部的に _ を補完するので、'calc_sin' で正しく _calc_sin にマッピングされる。
エラーが「table index is out of bounds」ということは、おそらく関数テーブルにない関数を呼び出そうとしている。考えられるのは、cwrap で指定した関数が実際にはエクスポートされていない場合。しかし、EMSCRIPTEN_KEEPALIVEを使っているのでエクスポートされているはず。
ビルド時の -s EXPORTED_FUNCTIONS に含まれている関数はエクスポートされるが、EMSCRIPTEN_KEEPALIVE も同様にエクスポートする。両方指定しても問題ない。
しかし、もしかすると、-s EXPORTED_FUNCTIONS にリストアップした関数名に誤りがあるかもしれない(アンダースコアが二重になっているなど)。また、-s EXPORTED_RUNTIME_METHODS に指定した ccall や cwrap は正しくエクスポートされている。
それよりも、より本質的な問題は、calc_lab_worker.js で factory() を呼び出してモジュールを初期化した後、cwrap を呼び出しているが、その時点でモジュールの関数テーブルが正しく設定されていない可能性がある。特に、-s MODULARIZE=1 と -s EXPORT_NAME="createCalcLabModule" を使用しているが、モジュールの初期化が完了する前に cwrap を呼ぼうとしているかもしれない。しかし、await factory() で完了しているので大丈夫。
また、calc_lab_worker.js では、importScripts の後に async function init() を定義し、その中で factory() を呼び出している。しかし、importScripts は同期的にスクリプトを実行するので、calc_lab.js 内で Module が定義されるが、その Module はファクトリ関数(createCalcLabModule)である。init() 内で await factory() しているので、モジュールの初期化が完了してから cwrap を設定している。問題なさそう。
エラーが起きるのは、計算実行時(worker.postMessage を受けて、実際に self.calc_sin を呼び出すとき)かもしれない。そのとき、cwrap でラップされた関数が呼び出され、内部で関数テーブルを参照するが、そのインデックスが無効。
考えられる原因:cwrap の引数型や戻り値型が間違っていると、Emscriptenが正しい関数を取得できないことがある? しかし、cwrap は関数名を指定して、その関数ポインタを取得する。関数がエクスポートされていれば正しく取得できるはず。
もしかすると、calc_lab.c の関数が EMSCRIPTEN_KEEPALIVE でエクスポートされているが、EXPORTED_FUNCTIONS に明示的に同じ関数を指定しているために、二重にエクスポートされ、何か問題が起きている? 通常は問題ないが、試しに EXPORTED_FUNCTIONS を削除してみる。
また、build_lab.sh で -s EXPORTED_FUNCTIONS に '_free_result' も追加すべきかもしれないが、EMSCRIPTEN_KEEPALIVE でカバーされている。
他に考えられるのは、calc_lab.c の関数が format_mpfr などの内部関数を使っているが、それらは問題ない。
もう一つの可能性:mpfr_get_str などのライブラリ関数が関数テーブルを参照している? しかし、GMP/MPFRは静的リンクされており、関数テーブルはWASMモジュール内に閉じている。問題はないはず。
それよりも、エラーが「table index is out of bounds」と出るのは、WASMの関数テーブル(間接呼び出し用)にアクセスする際に、インデックスがテーブルのサイズを超えていることを示す。これは通常、関数ポインタを経由して呼び出す関数が正しく定義されていないときに起こる。例えば、mpfr_set_str などの関数が正しくリンクされていない場合、それらが関数テーブルに存在しないインデックスを参照するかもしれないが、それはリンクエラーになるはず。
しかし、ここではビルドは成功しているので、リンクは成功している。実行時にテーブルインデックスが範囲外になるのは、例えば、関数ポインタを経由して呼び出すときに、その関数ポインタがNULLだったり、無効な値を指している場合。もしかすると、calc_lab.c 内で何らかの関数ポインタを使用している?(例えば、mpfr_set_str などは関数ポインタではないが、内部的にライブラリが関数ポインタを使っている可能性はある)。
もっと単純に、cwrap で関数をラップする際に、関数名に対応する関数がエクスポートされていないと、cwrap はダミー関数を返すか、エラーになる? 実際には、存在しない関数名を指定すると、cwrap は undefined を返すか、エラーになる。しかし、ここでは self.calc_sin に cwrap の結果を代入しているので、もし関数が見つからなければ undefined になる。その後に self.calc_sin(...) を呼び出すと、undefined is not a function というエラーになるはずで、テーブルインデックスエラーではない。
従って、関数は正しくエクスポートされており、cwrap も正しく関数を取得しているが、その関数の内部で何かがテーブルインデックスを越えてアクセスしている。つまり、Cコード内の間接呼び出しで問題が起きている可能性。
特に、mpfr ライブラリは内部で関数ポインタを使うことがある(例えば、丸めモードに応じて異なる関数を呼ぶなど)。もし、mpfr がWASM向けに正しくビルドされていないと、関数テーブルが不完全になるかもしれない。しかし、emconfigure でビルドしているので、通常は大丈夫。
あるいは、calc_lab.c 内で mpfr_get_str の第2引数に &exp を渡しているが、これは正しい。format_mpfr の中で mpfr_free_str を使っている。
問題を切り分けるために、まずはシンプルな関数だけを試してみる。例えば、calc_pi だけを呼び出してみて、同じエラーが出るかどうか。もし出るなら、関数テーブル全体の問題。もし三角関数だけなら、三角関数の内部で何かがおかしい。
しかし、ユーザーの報告では「❌ エラー: table index is out of bounds」とだけあり、どの操作で発生したか不明。なので、全ての操作で発生する可能性がある。
改善策として、build_lab.sh の EXPORTED_FUNCTIONS を削除して、EMSCRIPTEN_KEEPALIVE だけに頼る。また、-s EXPORTED_RUNTIME_METHODS に 'ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8' を指定しているが、UTF8ToString などはデフォルトでエクスポートされるかもしれないが、明示指定は良い。
さらに、-s ALLOW_MEMORY_GROWTH=1 はメモリが拡張されても関数テーブルは変化しないので問題ない。
また、-s NO_EXIT_RUNTIME=1 と -s ENVIRONMENT=web,worker も問題ない。
考えられるもう一つの原因は、calc_lab_worker.js での calcModule.UTF8ToString(ptr) の呼び出しが、ptr が無効な場合に何か問題を起こす? しかし、それはテーブルインデックスエラーではない。
もう一つ、cwrap で指定する関数名が、EMSCRIPTEN_KEEPALIVE でエクスポートされた名前と一致しているか確認する。Cの関数名は calc_sin で、エクスポート名も _calc_sin になる。cwrap は 'calc_sin' で指定すると内部で _ を付けるので問題ない。
しかし、EMSCRIPTEN_KEEPALIVE を使うと、関数は _ 付きでエクスポートされるが、cwrap の第一引数には _ なしで指定するのが正しい。
では、なぜテーブルインデックスエラーが起こるのか? もしかすると、mpfr や gmp の関数が、WASMの関数テーブルを間接的に使用するときに、正しく初期化されていない可能性がある。これは、ライブラリのビルド時に --disable-shared で静的リンクしているが、emconfigure が正しく --host を設定しているかどうか。--host=none-unknown-linux は適切か? Emscriptenの推奨は --host=wasm32-unknown-emscripten などかもしれないが、emconfigure は適切に変換するはず。
また、MPFRのビルドで --with-gmp-build="${GMP_DIR}" を指定しているが、これはビルドディレクトリを指定している。正しい。
もしかすると、calc_lab.c のコンパイル時に、-I や -L の指定が足りない? しかし、それはリンクエラーになるはず。
根本的に、table index is out of bounds は、WASMモジュールの call_indirect 命令が、テーブル内の存在しないインデックスを参照した場合に発生する。これは、関数ポインタが不正な値(例えば、ゼロや範囲外)であることを示す。Cコード内で関数ポインタを使っている場所を探す。
calc_lab.c では関数ポインタを直接使っていない(配列などを使っていない)。しかし、mpfr ライブラリ内部で関数ポインタを使っている可能性はある。特に、mpfr_set_str や mpfr_get_str などは、内部的に printf 系の関数を使うかもしれないが、それらはWASMの関数テーブルとは関係ない。
もしくは、emcc のコンパイル時に、最適化 -O3 が原因で関数テーブルの参照が乱れることは考えにくい。
別のアプローチとして、ビルド時に -s ASSERTIONS=1 を追加して、より詳細なエラーメッセージを表示させる。また、-s SAFE_HEAP=1 なども試すことができるが、パフォーマンスに影響する。
また、calc_lab_worker.js で cwrap を呼ぶ前に、モジュールが完全に初期化されていることを確認するために、calcModule が ready 状態になっているか? factory() の戻り値は Promise なので、await で待っている。問題ない。
さらに、calc_lab_worker.js で self.calc_sin などに代入しているが、これらはグローバルに定義されているので、onmessage 内で参照できる。問題ない。
もしかすると、importScripts で読み込んだ calc_lab.js が、createCalcLabModule という名前でエクスポートされているが、self にその関数が存在しない場合、フォールバックで self.Module をチェックしている。もし self.Module が関数ではなく、何か他のものであれば、問題が起きるかもしれない。しかし、通常は createCalcLabModule が存在するはず。
ユーザーが手元でビルドした calc_lab.js が正しく生成されているか確認する。build_lab.sh を実行して生成された calc_lab.js の末尾に var createCalcLabModule=... とあるはず。今、与えられた calc_lab.js の内容を見ると、最初の行は var createCalcLabModule=(()=>{... となっており、正しくエクスポートされている。
では、問題は calc_lab_worker.js の factory() の呼び出しで、await factory() が失敗している可能性は? しかし、init() 内で catch して init_error を送信しているので、エラーがあればそれが表示されるはず。今のエラーは table index is out of bounds なので、初期化は成功している。
実際に計算を実行しようとしたときにエラーが発生するということは、計算関数(calc_sin など)が呼び出されると、その関数内部で関数テーブル参照が失敗している。
そこで、calc_lab.c の各関数が、内部で mpfr 関数を呼び出しているが、それらの関数が正しくリンクされているか? ビルド時に -lmpfr -lgmp を指定しているので、リンクは成功している。しかし、もしかすると、mpfr や gmp が --disable-shared でビルドされているが、emcc のリンク時に静的ライブラリが正しく取り込まれているか? 確認するには、生成された .wasm ファイルのサイズを見ると、数MB以上あるはず。もし小さければ、ライブラリがリンクされていない可能性がある。
しかし、build_lab.sh では、emcc コマンドで -L"${GMP_DIR}/.libs" -L"${MPFR_DIR}/src/.libs" -lmpfr -lgmp としている。これで正しくリンクされるはず。
calc_lab.c の関数で、mpfr_set_str が失敗した場合のエラーハンドリングもしている。
もう一つの可能性:format_mpfr 関数内で mpfr_get_str を呼び出し、その後に mpfr_free_str を呼んでいるが、mpfr_get_str が失敗した場合に NULL を返す可能性があり、その場合 raw が NULL で、mpfr_free_str に渡すと問題が起きるかもしれない。しかし、その前に if (!raw) return strdup("Error: conversion failed"); でチェックしているので、大丈夫。
もしかすると、mpfr_get_str の第1引数に NULL を渡すことで、内部でメモリを確保するが、その際に関数ポインタを使用する? それはない。
では、mpfr ライブラリが、ビルド時に --with-gmp-build で GMP のビルドディレクトリを指定しているが、これが正しく機能していない可能性は? しかし、emmake make は成功している。
一つの仮説として、mpfr や gmp が、関数テーブルを直接使用するようなコードを生成している場合、その関数テーブルのサイズが不足している可能性がある。Emscripten のデフォルトの関数テーブルサイズは十分大きく、特に明示的に制限していないので問題ないはず。
もう一つ考えられるのは、emcc のバージョンや、emsdk のバージョンが古い場合、バグがある可能性。しかし、ユーザーは最新のものを使用していると仮定する。
問題を解決するために、以下の対策を提案する:
-
build_lab.sh から EXPORTED_FUNCTIONS を削除し、EMSCRIPTEN_KEEPALIVE のみに依存する。
-
-s EXPORTED_RUNTIME_METHODS に 'ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8' を追加する(これは既にしている)。
-
デバッグのために -s ASSERTIONS=1 を追加して、より詳細なエラーメッセージを表示する。
-
-s WASM_ASYNC_COMPILATION=0 を追加して、同期的にコンパイルする(問題が非同期にある場合)。
-
calc_lab_worker.js で cwrap の代わりに ccall を使ってみる? しかし、cwrap は正しい。
また、calc_lab_worker.js 内で、self.calc_sin などを関数として呼び出す前に、それらが undefined でないことを確認する。
さらに、index03.html で worker.postMessage に送るパラメータが正しいか確認する。例えば、digits が undefined になっていないか。
しかし、エラーが「table index is out of bounds」であることから、WASM内部の関数テーブル参照の問題が疑われる。そこで、Cコード側で関数ポインタを使っている箇所を確認する。calc_lab.c では使っていないが、mpfr 内部で使っている可能性が高い。もしかすると、mpfr が longjmp や setjmp を使用しており、それらがWASMで正しく実装されていない場合、関数テーブルを壊す可能性がある? しかし、mpfr は setjmp を使わないはず。
別の可能性:mpfr のビルド時に --enable-thread-safe などのオプションが有効になっていて、スレッド関連の関数が呼ばれている? しかし、WASMはシングルスレッドなので、スレッド関連の関数はダミーになるが、それが関数テーブルを参照するかもしれない。
そこで、build_lab.sh で GMP と MPFR をビルドする際に、--disable-thread などのオプションを追加して、スレッド関連を無効にする。
さらに、emconfigure の代わりに emcmake を使う方法もあるが、emconfigure で十分。
また、calc_lab.c をビルドする際に、-s SIDE_MODULE=0(デフォルト)でメインモジュールとしてビルドしている。
もしかすると、EXPORTED_FUNCTIONS に _free_result を含めていないので、free_result がエクスポートされていない? しかし、EMSCRIPTEN_KEEPALIVE でエクスポートされている。しかし、EXPORTED_FUNCTIONS に含めることで明示的にエクスポートできる。ただし、EMSCRIPTEN_KEEPALIVE があれば自動でエクスポートされる。
さて、解決策として、以下の新しいファイルを提供する:
-
build_lab.sh: EXPORTED_FUNCTIONS を削除し、-s ASSERTIONS=1 を追加。
-
calc_lab.c: 変更なし(ただし、format_mpfr で mpfr_get_str の第2引数に NULL を渡していないことを確認。既に修正済み)。
-
calc_lab_worker.js: 微調整(デバッグ出力を追加)。
-
index03.html: 変更なし(ただし、エラーメッセージをより詳しく表示するために、worker.onmessage でエラー内容を表示する)。
また、calc_lab_worker.js で、calcModule が初期化された後に、calcModule.UTF8ToString が正しく使えるか確認する。
根本的な解決として、cwrap でラップする代わりに、ccall を使って直接呼び出す方法もあるが、cwrap の方が効率的。
もしかすると、cwrap の第3引数(引数型)の指定が間違っているかもしれない。例えば、calc_sin の引数は (char* x_str, int digits, int unit) だが、cwrap では ['string', 'number', 'number'] と指定している。これは正しい。string は char* に変換される。
同様に、calc_pi は int digits なので ['number']。calc_arithmetic は (char* op, char* a_str, char* b_str, int digits) なので ['string', 'string', 'string', 'number']。正しい。
戻り値はすべて char* なので 'number' で良い。
では、なぜテーブルインデックスエラーが起きるのか? もしかすると、mpfr の関数が stdin や stdout などのファイルポインタを使用しており、それらが関数テーブルを参照する? しかし、ファイルポインタはWASMでは特殊な扱い。
いずれにせよ、ビルドスクリプトに -s EXPORTED_FUNCTIONS を明示的に指定するのは、EMSCRIPTEN_KEEPALIVE と競合する可能性は低いが、念のため削除する。また、-s ASSERTIONS=1 を追加して、より詳細なエラーを表示する。
もう一つ、-s ERROR_ON_UNDEFINED_SYMBOLS=1 を追加して、未定義シンボルがあればビルドエラーにする(リンク時)。これで、もしライブラリの関数が不足していればビルドが失敗するので、問題を早期に発見できる。
また、GMPとMPFRのビルド時に、--disable-assembly を追加して、アセンブリコードを使わないようにする(WASMではアセンブリは使えないが、自動で検出されるはず)。
新しい build_lab.sh を提案する。
また、calc_lab_worker.js で、cwrap の結果が undefined でないかチェックし、エラーを投げるようにする。
そして、index03.html でエラーメッセージをより詳細に表示するために、onResult で data.error をそのまま表示する。
最終的に、問題が解決しない場合は、calc_lab.c をシンプルにして、mpfr を一切使わずに printf だけの関数をエクスポートして、テーブルインデックスエラーが再現するか確認する。しかし、それは最終手段。
今回は、ビルドスクリプトとワーカーを改善する方向で提案する。
具体的に修正するファイル:
-
build_lab.sh: EXPORTED_FUNCTIONS を削除し、-s ASSERTIONS=1 と -s ERROR_ON_UNDEFINED_SYMBOLS=1 を追加。また、GMPとMPFRのビルドオプションに --disable-assembly を追加。
-
calc_lab_worker.js: エラーハンドリングを強化し、cwrap で取得した関数が存在するか確認。
-
index03.html: エラーメッセージ表示を改善。
また、calc_lab.c に変更はないが、念のため format_mpfr での mpfr_get_str の呼び出しで &exp を渡していることを再確認。
これらの修正で、エラーが回避される可能性がある。
さらに、もしエラーが続く場合、-s WASM=1 を -s WASM=0 にしてasm.jsで試すこともできるが、それは避けたい。
それでは、新しいファイル内容を提供する。