Es gibt zwei Hauptprobleme bei Funktionszeigern
Casts von Funktionszeigern können dazu führen, dass Aufrufe von Funktionszeigern fehlschlagen.
Funktionszeiger müssen mit dem korrekten Typ aufgerufen werden: Es ist in C und C++ undefiniertes Verhalten (UB), einen Funktionszeiger in einen anderen Typ zu casten und ihn auf diese Weise aufzurufen. Auf den meisten nativen Plattformen funktioniert dies zwar trotz UB, in Wasm kann es jedoch fehlschlagen. In diesem Fall sehen Sie möglicherweise ein abort(10) oder eine andere Nummer, und wenn Assertions aktiviert sind, sehen Sie eventuell eine Nachricht mit Details, die wie folgt beginnt
Invalid function pointer called
In seltenen Fällen sehen Sie möglicherweise eine Compiler-Warnung wie diese
warning: implicit declaration of function
Dies kann mit einem Problem beim Casten von Funktionszeigern zusammenhängen, da implizite Deklarationen einen anderen Typ haben können als die Art und Weise, wie Sie sie aufrufen. Im Allgemeinen kann der Compiler davor jedoch nicht warnen, und Sie werden ein Problem erst zur Laufzeit feststellen.
Ältere Versionen von clang können unterschiedlichen Code für C- und C++-Aufrufe generieren, wenn eine Struktur per Wert (by value) übergeben wird (der Vollständigkeit halber: eine Konvention ist struct byval und die andere ist field a, field b). Die beiden Formate sind inkompatibel zueinander, und Sie erhalten möglicherweise eine Warnung.
Die Abhilfe besteht darin, die Struktur per Referenz zu übergeben oder an dieser Stelle einfach C und C++ nicht zu mischen (benennen Sie zum Beispiel die .c-Datei in .cpp um).
Die Optionen SAFE_HEAP und ASSERTION können einige dieser Fehler zur Laufzeit abfangen und nützliche Informationen liefern. Sie können auch prüfen, ob EMULATE_FUNCTION_POINTER_CASTS die Probleme für Sie löst, beachten Sie jedoch weiter unten die Hinweise zum Overhead.
Es gibt drei Lösungen für dieses Problem (die zweite wird bevorzugt)
Casten Sie den Funktionszeiger zurück in den korrekten Typ, bevor er aufgerufen wird. Dies ist problematisch, da es voraussetzt, dass der Aufrufer den ursprünglichen Typ kennt.
Schreiben Sie manuell eine Adapterfunktion, die nicht gecastet werden muss und die ursprüngliche Funktion aufruft. Beispielsweise könnte sie einen Parameter ignorieren und auf diese Weise eine Brücke zwischen den verschiedenen Funktionszeigertypen schlagen.
Verwenden Sie
EMULATE_FUNCTION_POINTER_CASTS. Wenn Sie mit-sEMULATE_FUNCTION_POINTER_CASTSbauen, emittiert Emscripten Code, um Funktionszeiger-Casts zur Laufzeit zu emulieren, indem zusätzliche Argumente hinzugefügt, weggelassen, deren Typ geändert oder ein Rückgabetyp hinzugefügt/entfernt wird usw. Dies kann erheblichen Laufzeit-Overhead verursachen, wird daher nicht empfohlen, ist aber einen Versuch wert.
Betrachten Sie als praxisnahes Beispiel den unten stehenden Code
#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);
}
Der Code definiert drei Funktionen mit unterschiedlichen Signaturen: voidReturn vom Typ vi (void (int)), intReturn vom Typ ii und voidReturnNoParam vom Typ v. Diese Funktionszeiger werden in den Typ vi gecastet und einer Liste hinzugefügt. Die Funktionen werden dann über die Funktionszeiger in der Liste aufgerufen.
Der Code läuft (und funktioniert), wenn er als nativer Maschinencode kompiliert wird (auf allen gängigen Plattformen). Sie können es ausprobieren, indem Sie den Code als main.c speichern, cc main.c und dann ./a.out ausführen. Sie werden diese Ausgabe sehen
voidReturn: hello world
intReturn: hello world
voidReturnNoParam:
In Emscripten schlägt der Code jedoch mit einer Laufzeit-Exception fehl und zeigt die Konsolenausgabe an
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.
Hinweis
Sie können dies selbst ausprobieren. Speichern Sie den Code als main.c, kompilieren Sie ihn mit emcc -O0 main.c -o main.html und laden Sie dann main.html in einen Browser.
Das unten stehende Codefragment zeigt, wie wir den Funktionszeiger unmittelbar vor dem Aufruf zurück in seine ursprüngliche Signatur casten können, damit er in der richtigen Tabelle gefunden wird. Dies erfordert, dass der Empfänger der Tabelle spezielles Wissen darüber hat, was sich in der Liste befindet (dies sehen Sie im Spezialfall für Index 1 in der while-Schleife). Zusätzlich wird emcc sich weiterhin über den ursprünglichen Cast beschweren, der in main() beim Hinzufügen der Funktion zu functionList[1] stattfindet.
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++;
}
}
Das unten stehende Codefragment zeigt, wie man eine Adapterfunktion erstellt und verwendet, die die ursprüngliche Funktion aufruft. Der Adapter wird mit der gleichen Signatur definiert, die er beim Aufruf haben wird, und ist daher in der erwarteten Funktionszeiger-Tabelle verfügbar.
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);
}