函数指针问题

函数指针有两个主要问题

  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);
}

代码定义了三个具有不同签名的函数:类型为 vi (void (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

以下代码片段显示了如何在调用函数指针之前将其强制转换回原始签名,以便在正确的表中找到它。这要求表接收器对列表中的内容有特殊了解(你可以在循环中的索引 1 的特殊情况下看到这一点)。此外,emcc 将继续抱怨在 main() 中将函数添加到 functionList[1] 时发生的原始强制转换。

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);
}