関数ポインタの問題

関数ポインタには主に2つの問題があります

  1. 関数ポインタのキャストにより、関数ポインタの呼び出しが失敗する可能性があります。

    関数ポインタは正しい型で呼び出す必要があります。関数ポインタを別の型にキャストしてそのように呼び出すことは、CおよびC++では未定義の動作です。これはほとんどのネイティブプラットフォームでは、UBであるにもかかわらず機能しますが、Wasmでは失敗する可能性があります。その場合、abort(10)またはその他の数値が表示される場合があり、アサーションがオンの場合は、次で始まる詳細を含むメッセージが表示される場合があります。

    Invalid function pointer called
    

    まれに、次のようなコンパイラの警告が表示される場合があります

    warning: implicit declaration of function
    

    これは、暗黙的な宣言が呼び出し方とは異なる型を持っている可能性があるため、関数ポインタのキャストの問題に関連している可能性があります。ただし、一般的にコンパイラはこれについて警告できず、実行時にのみ問題が発生します。

  2. 古いバージョンのclangでは、構造体が値渡しされる場合、CとC++の呼び出しに対して異なるコードを生成する可能性があります(完全を期すために、一方の規約はstruct byvalであり、もう一方はfield a, field bです)。2つの形式は互換性がなく、警告が表示される場合があります。

    回避策は、構造体を参照渡しするか、その場所でCとC++を混在させないことです(たとえば、.cファイルを.cppに名前を変更します)。

関数ポインタの問題のデバッグ

SAFE_HEAPおよびASSERTIONオプションを使用すると、これらのエラーの一部を実行時にキャッチし、役立つ情報を提供できます。EMULATE_FUNCTION_POINTER_CASTSで問題が解決するかどうかを確認することもできますが、オーバーヘッドについては後で説明します。

関数ポインタの問題の回避策

この問題には3つの解決策があります(2番目が推奨されます)

  • 関数ポインタを呼び出す前に、元の型にキャストし直します。これは、呼び出し側が元の型を知っている必要があるため、問題があります。

  • キャストする必要がなく、元の関数を呼び出すアダプター関数を手動で作成します。たとえば、パラメーターを無視して、異なる関数ポインタ型間のブリッジになる可能性があります。

  • EMULATE_FUNCTION_POINTER_CASTSを使用します。-sEMULATE_FUNCTION_POINTER_CASTSでビルドすると、Emscriptenは実行時に関数ポインタのキャストをエミュレートするコードを出力し、追加の引数を追加/削除したり、型を変更したり、戻り値の型を追加または削除したりします。これにより、実行時のオーバーヘッドが大幅に増加する可能性があるため、推奨されませんが、試してみる価値はあります。

実際の例として、以下のコードを考えてみましょう

#include <stdio.h>

typedef void(*voidReturnType)(const char *);

void voidReturn(const char *message) {
  printf( "voidReturn: %s\n", message );
}


int intReturn(const char *message) {
  printf( "intReturn: %s\n", message );
  return 1;
}

void voidReturnNoParam() {
  printf( "voidReturnNoParam:\n" );
}

void callFunctions(const voidReturnType * funcs, size_t size) {
  size_t current = 0;
  while (current < size) {
    funcs[current]("hello world");
    current++;
  }
}

int main() {
  voidReturnType functionList[3];

  functionList[0] = voidReturn;
  functionList[1] = (voidReturnType)intReturn;         // Breaks in Emscripten.
  functionList[2] = (voidReturnType)voidReturnNoParam; // Breaks in Emscripten.

  callFunctions(functionList, 3);
}

このコードは、異なるシグネチャを持つ3つの関数を定義しています。型vivoid (int))のvoidReturn、型iiintReturn、および型vvoidReturnNoParamです。これらの関数ポインタは型viにキャストされ、リストに追加されます。その後、リスト内の関数ポインタを使用して関数が呼び出されます。

このコードは、ネイティブマシンコードにコンパイルすると(すべての主要プラットフォームで)実行(および動作)します。コードをmain.cとして保存し、cc main.cを実行してから./a.outを実行することで試すことができます。次の出力が表示されます

voidReturn: hello world
intReturn: hello world
voidReturnNoParam:

ただし、このコードはEmscriptenで実行時例外で失敗し、次のコンソール出力を表示します

voidReturn: hello world
Invalid function pointer called with signature 'vi'. Perhaps this is an invalid value (e.g. caused by calling a virtual method on a NULL pointer)? Or calling a function with an incorrect type, which will fail? (it is worth building your source files with -Werror (warnings are errors), as warnings can indicate undefined behavior which can cause this)
Build with ASSERTIONS=2 for more info.

注意

これは自分で試すことができます。コードをmain.cとして保存し、emcc -O0 main.c -o main.htmlを使用してコンパイルし、main.htmlをブラウザにロードします。

以下のコードフラグメントは、関数ポインタを呼び出す直前に元のシグネチャにキャストし直すことで、正しいテーブルで見つかるようにする方法を示しています。これには、テーブルの受信側がリストの内容について特別な知識を持っている必要があります(whileループのインデックス1の特別なケースでこれを確認できます)。さらに、emccは、関数をfunctionList[1]に追加する際に、main()で発生する元のキャストについて引き続き文句を言います。

void callFunctions(const voidReturnType * funcs, size_t size) {
  size_t current = 0;
  while (current < size) {
    if ( current == 1 ) {
      ((intReturnType)funcs[current])("hello world"); // Special-case cast
    } else {
      funcs[current]("hello world");
    }
    current++;
  }
}

以下のコードフラグメントは、元の関数を呼び出すアダプター関数を作成して使用する方法を示しています。アダプターは、呼び出されるときと同じシグネチャで定義されており、したがって、期待される関数ポインタテーブルで使用できます。

void voidReturnNoParamAdapter(const char *message) {
  voidReturnNoParam();
}

int main() {
  voidReturnType functionList[3];

  functionList[0] = voidReturn;
  functionList[1] = (voidReturnType)intReturn; // Fixed in callFunctions
  functionList[2] = voidReturnNoParamAdapter; // Fixed by Adapter

  callFunctions(functionList, 3);
}