函式指標問題

函式指標有兩個主要問題

  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)。這兩種格式彼此不相容,您可能會收到警告。

    解決方法是透過參考傳遞結構體,或者只是不要在該位置混合 C 和 C++(例如,將 **.c** 檔案重新命名為 **.cpp**)。

除錯函式指標問題

SAFE_HEAPASSERTION 選項可以在執行時期捕捉到某些這類錯誤並提供有用的資訊。您也可以看看 EMULATE_FUNCTION_POINTER_CASTS 是否能為您解決問題,但請參閱後面的關於額外負荷的說明。

解決函式指標問題

這個問題有三種解決方案(第二種是首選)

  • 在呼叫函式指標之前,將其轉換回正確的類型。這是個問題,因為它要求呼叫者知道原始類型。

  • 手動編寫一個不需要轉換的配接器函式,並呼叫原始函式。例如,它可能會忽略一個參數,並以這種方式在不同的函式指標類型之間架起橋樑。

  • 使用 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);
}

程式碼定義了三個具有不同簽名的函式:類型為 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 的特殊情況下看到這一點)。此外,當將函式新增至 functionList[1] 時,emcc 將會繼續抱怨在 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);
}