Die Emscripten-Laufzeitumgebung unterscheidet sich von derjenigen, die die meisten C/C++-Anwendungen erwarten. Emscripten arbeitet hart daran, diese Unterschiede zu abstrahieren und abzumildern, sodass Code im Allgemeinen mit wenig oder gar keinen Änderungen kompiliert werden kann.
Dieser Artikel geht näher auf einige der Unterschiede und die daraus resultierenden API-Einschränkungen ein und skizziert die wenigen Änderungen, die Sie möglicherweise an Ihrem C/C++-Code vornehmen müssen.
Emscripten implementiert die Simple DirectMedia Layer API (SDL) für die Browserumgebung, die einen hardwarenahen Zugriff auf Audio, Tastatur, Maus, Joystick und Grafikhardware bietet. Anwendungen, die SDL verwenden, benötigen in der Regel keine Änderungen an der Eingabe/Ausgabe, um im Browser zu laufen.
Darüber hinaus haben wir eine begrenztere Unterstützung für glut, glfw, glew und xlib.
Anwendungen, die SDL oder die anderen APIs nicht nutzen, können die Emscripten-spezifischen APIs für die Eingabe und Ausgabe verwenden
html5.h, welche die Emscripten-Low-Level-Glue-Bindings definiert, um mit HTML5-Events aus nativem Code zu interagieren, einschließlich Zugriff auf Tasten, Maus, Mausrad, Geräteausrichtung, Batteriestand, Vibration usw.
Multimedia- und Grafik-APIs, einschließlich OpenGL und EGL.
Viel C/C++-Code verwendet die synchronen Dateisystem-APIs in libc und libcxx, um auf Code im lokalen Dateisystem zuzugreifen. Dies ist problematisch, da der Browser verhindert, dass Code direkt auf Dateien auf dem Host-System zugreift, und weil JavaScript außerhalb von Web-Workern nur asynchronen Dateizugriff unterstützt.
Emscripten bietet eine Implementierung von libc und libcxx sowie ein virtuelles Dateisystem, sodass normaler C/C++-Code ohne Änderungen kompiliert und ausgeführt werden kann. Die meisten Entwickler müssen lediglich die Dateien angeben, die zur Laufzeit für das Vorladen in das virtuelle Dateisystem gepackt werden sollen.
Hinweis
Die Verwendung eines virtuellen Dateisystems umgeht die oben genannten Einschränkungen. Die Dateidaten werden zum Zeitpunkt der Kompilierung gepackt und über asynchrone JavaScript-APIs in das Dateisystem heruntergeladen, bevor der kompilierte Code ausgeführt werden darf. Der kompilierte Code führt dann "Datei"-Aufrufe aus, die in Wirklichkeit nur Aufrufe in den Programmspeicher sind.
Das Standard-Dateisystem (MEMFS) speichert Dateien im Arbeitsspeicher, sodass alle Änderungen verloren gehen, wenn die Seite neu geladen wird. Wenn Dateiänderungen dauerhafter gespeichert werden müssen, können Entwickler das IDBFS-Dateisystem einbinden, das eine Speicherung der Daten im Browser ermöglicht. Wenn Code in node.js ausgeführt wird, können Entwickler NODEFS einbinden, um dem Code direkten Zugriff auf das lokale Dateisystem zu geben.
Emscripten verfügt außerdem über eine API zur Unterstützung von asynchronem Dateizugriff.
Weitere Informationen und Beispiele finden Sie unter Dateien und Dateisysteme.
Das Browser-Ereignismodell verwendet kooperatives Multitasking — jedes Ereignis hat einen "Zug", um ausgeführt zu werden, und muss dann die Kontrolle an den Browser zurückgeben, damit andere Ereignisse verarbeitet werden können. Eine häufige Ursache für das Hängen von HTML-Seiten ist JavaScript, das nicht abgeschlossen wird und die Kontrolle nicht an den Browser zurückgibt.
Grafische C++-Apps laufen normalerweise in einer Endlosschleife. In jeder Iteration der Schleife führt die App Ereignisbehandlung, Verarbeitung und Rendering durch, gefolgt von einer Verzögerung ("Wait"), um die Bildrate konstant zu halten. Diese Endlosschleife ist in der Browserumgebung ein Problem, da die Kontrolle nicht an den Browser zurückgegeben werden kann, damit anderer Code ausgeführt werden kann. Nach einer gewissen Zeit benachrichtigt der Browser den Benutzer, dass die Seite hängt, und bietet an, sie anzuhalten oder zu schließen.
Ebenso können JavaScript-APIs wie WebGL nur ausgeführt werden, wenn der aktuelle "Zug" vorbei ist, und werden an diesem Punkt automatisch rendern und die Buffer tauschen. Dies steht im Gegensatz zu OpenGL-C++-Apps, bei denen Sie die Buffer manuell tauschen müssten.
Die Standardlösung für dieses Problem besteht darin, eine C-Funktion zu definieren, die eine Iteration Ihrer Hauptschleife ausführt (ohne die "Verzögerung"). Für einen nativen Build kann diese Funktion in einer Endlosschleife aufgerufen werden, wodurch das Verhalten effektiv unverändert bleibt.
Innerhalb von mit Emscripten kompiliertem Code verwenden wir emscripten_request_animation_frame_loop(), um die Umgebung zu veranlassen, dieselbe Funktion in der richtigen Frequenz für das Rendering eines Frames aufzurufen (d. h. wenn der Browser mit 60fps rendert, wird dies 60 Mal pro Sekunde aufgerufen). Die Iteration wird immer noch "unendlich" ausgeführt, aber jetzt kann anderer Code zwischen den Iterationen laufen und der Browser hängt nicht.
Normalerweise haben Sie einen kleinen Abschnitt mit #ifdef __EMSCRIPTEN__ für die zwei Fälle. Zum Beispiel
#include <emscripten.h>
#include <emscripten/html5.h>
#include <stdio.h>
// Our "main loop" function. This callback receives the current time as
// reported by the browser, and the user data we provide in the call to
// emscripten_request_animation_frame_loop().
bool one_iter(double time, void* userData) {
// Can render to the screen here, etc.
puts("one iteration");
// Return true to keep the loop running.
return true;
}
int main() {
#ifdef __EMSCRIPTEN__
// Receives a function to call and some user data to provide it.
emscripten_request_animation_frame_loop(one_iter, 0);
#else
while (1) {
one_iter();
// Delay to keep frame rate constant (using SDL).
SDL_Delay(time_to_next_frame());
}
#endif
}
Hinweis
Eine funktionsreichere API wird in emscripten_set_main_loop() bereitgestellt, mit der Sie die Frequenz, mit der die Funktion aufgerufen werden soll, und andere Dinge festlegen können.
Hinweis
rendern nur einen einzelnen Frame und halten dann an. Beachten Sie außerdem
Die aktuelle Emscripten-Implementierung von SDL_QUIT funktioniert, wenn Sie emscripten_set_main_loop() verwenden. Wenn die Seite geschlossen wird, wird ein finaler direkter Aufruf der Hauptschleife erzwungen, was ihr die Chance gibt, das SDL_QUIT-Ereignis zu bemerken. Wenn Sie keine Hauptschleife verwenden, schließt Ihre App, bevor Sie die Gelegenheit hatten, dieses Ereignis zu bemerken.
Es gibt Einschränkungen für das, was Sie tun können, während die Seite geschlossen wird (in onunload). Einige Aktionen, wie das Anzeigen von Warnmeldungen (Alerts), sind von Browsern zu diesem Zeitpunkt untersagt.
Eine weitere Option ist die Verwendung von Asyncify, welches das Programm so umschreibt, dass es zur Hauptereignisschleife des Browsers zurückkehren kann, indem es einfach emscripten_sleep() aufruft. Beachten Sie, dass dieses Umschreiben Overhead bei Größe und Geschwindigkeit verursacht, während emscripten_request_animation_frame_loop / emscripten_set_main_loop, wie zuvor beschrieben, dies nicht tun.
Wenn eine mit Emscripten kompilierte Anwendung geladen wird, beginnt sie mit der Vorbereitung der Daten in der Preloading-Phase. Dateien, die Sie für das Vorladen markiert haben (mit emcc --preload-file oder manuell aus JavaScript mit FS.createPreloadedFile()), werden in dieser Phase eingerichtet.
Sie können zusätzliche Operationen mit addRunDependency() hinzufügen, was ein Zähler für alle Abhängigkeiten ist, die ausgeführt werden müssen, bevor der kompilierte Code laufen kann. Sobald diese abgeschlossen sind, können Sie removeRunDependency() aufrufen, um die erledigten Abhängigkeiten zu entfernen.
Hinweis
Im Allgemeinen ist es nicht notwendig, zusätzliche Operationen hinzuzufügen — das Vorladen ist für fast alle Anwendungsfälle geeignet.
Wenn alle Abhängigkeiten erfüllt sind, ruft Emscripten run() auf, welches wiederum Ihre main()-Funktion aufruft. Die main()-Funktion sollte verwendet werden, um Initialisierungsaufgaben durchzuführen, und wird oft emscripten_set_main_loop() aufrufen (wie oben beschrieben). Die Hauptschleifen-Funktion wird dann mit der angeforderten Frequenz aufgerufen.
Sie können den Betrieb der Hauptschleife auf verschiedene Weise beeinflussen
emscripten_push_main_loop_blocker() fügt eine Funktion hinzu, die die Hauptschleife blockiert, bis der Blocker abgeschlossen ist.
Dies ist nützlich, um beispielsweise das Laden neuer Spiel-Level zu verwalten. Nachdem ein Level abgeschlossen ist, können Sie Blocker für jede beteiligte Aktion hinzufügen (Datei entpacken, Datenstrukturen generieren usw.). Wenn alle Blocker abgeschlossen sind, wird die Hauptschleife fortgesetzt und das Spiel sollte den neuen Level ausführen. Sie können diese Funktion auch in Verbindung mit emscripten_set_main_loop_expected_blockers() verwenden, um den Benutzer über den Fortschritt auf dem Laufenden zu halten.
emscripten_pause_main_loop() pausiert die Hauptschleife, und emscripten_resume_main_loop() setzt sie fort. Dies sind hardwarenahe (weniger empfohlene) Alternativen zu den Blocker-Funktionen.
emscripten_async_call() ermöglicht es Ihnen, eine Funktion nach einem bestimmten Intervall aufzurufen. Dies verwendet standardmäßig requestAnimationFrame oder setTimeout, wenn ein spezifisches Intervall angefordert wurde.
Die Referenz zur Browser-Ausführungsumgebung (emscripten.h) beschreibt eine Reihe weiterer Methoden zur Steuerung der Ausführung.
Sowohl in asm.js als auch in WebAssembly stellt Emscripten den Speicher in einer Weise dar, die nativen Architekturen ähnelt. Pointer stellen Offsets in den Speicher dar, Structs belegen denselben Adressraum wie gewöhnlich, und so weiter.
In WebAssembly erfolgt dies über ein WebAssembly.Memory-Objekt, das für diesen Zweck entwickelt wurde. In asm.js verwendet Emscripten ein einzelnes Typed Array, wobei verschiedene Views den Zugriff auf unterschiedliche Typen ermöglichen (HEAPU32 für vorzeichenlose 32-Bit-Integer usw.).
Emscripten experimentierte in der Vergangenheit mit anderen Speicherdarstellungen und landete schließlich beim "Typed Arrays Mode 2"-Ansatz für JS und dann asm.js, wie oben beschrieben, woraufhin WebAssembly etwas Ähnliches implementierte.