Function Pointer Issues

There are two main issues with function pointers:

  1. Function pointer casts can cause function pointer calls to fail.

    Function pointers must be called with the correct type: it is undefined behavior in C and C++ to cast a function pointer to another type and call it that way. This does work in most native platforms, however, despite it being UB, but in Wasm it can fail. In that case, you may see an abort(10) or some other number, and if assertions are on you may see a message with details that start with

    Invalid function pointer called
    

    Rarely, you may see a compiler warning like this:

    warning: implicit declaration of function
    

    This may be related to a function pointer cast problem as implicit declarations may have a different type than how you call them. However, in general the compiler cannot warn about this, and you will only see a problem at runtime.

  2. Older versions of clang can generate different code for C and C++ calls when a structure is passed by value (for completeness, one convention is struct byval and the other is field a, field b). The two formats are incompatible with each other, and you may get a warning.

    The workaround is to pass the structure by reference, or simply not mix C and C++ in that location (for example, rename the .c file to .cpp).

Debugging function pointer issues

The SAFE_HEAP and ASSERTION options can catch some of these errors at runtime and provide useful information. You can also see if EMULATE_FUNCTION_POINTER_CASTS fixes things for you, but see later down about the overhead.

Working around function pointer issues

There are three solutions to this problem (the second is preferred):

  • Cast the function pointer back to the correct type before it is called. This is problematic because it requires that the caller knows the original type.

  • Manually write an adapter function that does not need to be cast, and calls the original function. For example, it might ignore a parameter, and in that way bridge between the different function pointer types.

  • Use EMULATE_FUNCTION_POINTER_CASTS. When you build with -sEMULATE_FUNCTION_POINTER_CASTS, Emscripten emits code to emulate function pointer casts at runtime, adding extra arguments/dropping them/changing their type/adding or dropping a return type/etc. This can add significant runtime overhead, so it is not recommended, but is be worth trying.

For a real-world example, consider the code below:

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

The code defines three functions with different signatures: voidReturn of type vi (void (int)), intReturn of type ii, and voidReturnNoParam of type v. These function pointers are cast to type vi and added to a list. The functions are then called using the function pointers in the list.

The code runs (and works) when compiled to native machine code (on all major platforms). You can try it by saving the code as main.c and executing cc main.c and then ./a.out. You’ll see this output:

voidReturn: hello world
intReturn: hello world
voidReturnNoParam:

However, the code fails with a runtime exception in Emscripten, and displays the console output:

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.

Note

You can try this yourself. Save the code as main.c, compile using emcc -O0 main.c -o main.html, and then load main.html into a browser.

The code fragment below shows how we can cast the function pointer back to its original signature just before calling it, so that it is found in the correct table. This requires the receiver of the table to have special knowledge about what is in the list (you can see this in the special case for index 1 in the while loop). Additionally, emcc will continue to complain about the original cast taking place in main() when adding the function to 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++;
  }
}

The code fragment below shows how to make and use an adapter function that calls the original function. The adapter is defined with the same signature as it will have when called, and is hence available in the expected function-pointer table.

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