Emscripten unterstützt zwei Wege (Asyncify und JSPI), die synchronen C- oder C++-Code die Interaktion mit asynchronem JavaScript ermöglichen. Dies erlaubt Dinge wie
Einen synchronen Aufruf in C, der an die Ereignisschleife abgibt, wodurch Browser-Ereignisse verarbeitet werden können.
Einen synchronen Aufruf in C, der auf den Abschluss einer asynchronen Operation in JS wartet.
Im Allgemeinen sind die beiden Optionen sehr ähnlich, basieren aber auf unterschiedlichen zugrunde liegenden Mechanismen.
Asyncify - Asyncify transformiert Ihren kompilierten Code automatisch in eine Form, die angehalten und fortgesetzt werden kann, und übernimmt das Anhalten und Fortsetzen für Sie, sodass er asynchron ist (daher der Name „Asyncify“), obwohl Sie ihn auf normale synchrone Weise geschrieben haben. Dies funktioniert in den meisten Umgebungen, kann aber dazu führen, dass die Wasm-Ausgabe viel größer wird.
JSPI (experimentell) - Verwendet die Unterstützung der VM für JavaScript Promise Integration (JSPI) zur Interaktion mit asynchronem JavaScript. Die Code-Größe bleibt gleich, aber die Unterstützung für diese Funktion ist noch experimentell.
Weitere Informationen zu Asyncify finden Sie im Einführungs-Blogbeitrag zu Asyncify für allgemeine Hintergrundinformationen und Details zur internen Funktionsweise (Sie können auch diesen Vortrag über Asyncify ansehen). Das Folgende erweitert die Emscripten-Beispiele aus diesem Beitrag.
Beginnen wir mit dem Beispiel aus diesem Blogbeitrag
// example.cpp
#include <emscripten.h>
#include <stdio.h>
// start_timer(): call JS to set an async timer for 500ms
EM_JS(void, start_timer, (), {
Module.timer = false;
setTimeout(function() {
Module.timer = true;
}, 500);
});
// check_timer(): check if that timer occurred
EM_JS(bool, check_timer, (), {
return Module.timer;
});
int main() {
start_timer();
// Continuously loop while synchronously polling for the timer.
while (1) {
if (check_timer()) {
printf("timer happened!\n");
return 0;
}
printf("sleeping...\n");
emscripten_sleep(100);
}
}
Sie können dies entweder mit -sASYNCIFY oder -sJSPI kompilieren
emcc -O3 example.cpp -s<ASYNCIFY or JSPI>
Hinweis
Es ist sehr wichtig, bei der Verwendung von Asyncify zu optimieren (hier -O3), da nicht optimierte Builds sehr groß sind.
Und Sie können es ausführen mit
nodejs a.out.js
Oder mit JSPI
nodejs --experimental-wasm-stack-switching a.out.js
Sie sollten dann so etwas sehen
sleeping...
sleeping...
sleeping...
sleeping...
sleeping...
timer happened!
Der Code ist mit einer einfachen Schleife geschrieben, die während ihrer Ausführung nicht beendet wird, was normalerweise keine Verarbeitung asynchroner Ereignisse durch den Browser ermöglichen würde. Mit Asyncify/JSPI geben diese Sleeps tatsächlich an die Haupt-Ereignisschleife des Browsers ab, und der Timer kann ausgelöst werden!
Abgesehen von emscripten_sleep und den anderen von Asyncify unterstützten Standard-Synchron-APIs können Sie auch Ihre eigenen Funktionen hinzufügen. Dazu müssen Sie eine JS-Funktion erstellen, die von Wasm aufgerufen wird (da Emscripten das Anhalten und Fortsetzen des Wasm von der JS-Laufzeit steuert).
Eine Möglichkeit dazu ist eine JS-Bibliotheksfunktion. Eine andere ist die Verwendung von EM_ASYNC_JS, die wir im nächsten Beispiel verwenden werden
// example.c
#include <emscripten.h>
#include <stdio.h>
EM_ASYNC_JS(int, do_fetch, (), {
out("waiting for a fetch");
const response = await fetch("a.html");
out("got the fetch response");
// (normally you would do something with the fetch here)
return 42;
});
int main() {
puts("before");
do_fetch();
puts("after");
}
In diesem Beispiel ist die asynchrone Operation ein fetch, was bedeutet, dass wir auf ein Promise warten müssen. Obwohl diese Operation asynchron ist, beachten Sie, wie der C-Code in main() vollständig synchron ist!
Um dieses Beispiel auszuführen, kompilieren Sie es zuerst mit
emcc example.c -O3 -o a.html -s<ASYNCIFY or JSPI>
Um dies auszuführen, müssen Sie einen lokalen Webserver starten und dann zu https://:8000/a.html navigieren. Sie werden so etwas sehen
before
waiting for a fetch
got the fetch response
after
Das zeigt, dass der C-Code erst nach Abschluss des asynchronen JS weiter ausgeführt wurde.
Wenn Ihre Ziel-JS-Engine die moderne async/await JS-Syntax nicht unterstützt, können Sie die obige Implementierung von do_fetch umschreiben, um Promises direkt mit EM_JS und Asyncify.handleAsync stattdessen zu verwenden
EM_JS(int, do_fetch, (), {
return Asyncify.handleAsync(function () {
out("waiting for a fetch");
return fetch("a.html").then(function (response) {
out("got the fetch response");
// (normally you would do something with the fetch here)
return 42;
});
});
});
Bei Verwendung dieser Form weiß der Compiler nicht mehr statisch, dass do_fetch asynchron ist. Stattdessen müssen Sie dem Compiler mitteilen, dass do_fetch() eine asynchrone Operation unter Verwendung von ASYNCIFY_IMPORTS durchführen kann, andernfalls wird der Code nicht instrumentiert, um das Anhalten und Fortsetzen zu ermöglichen (siehe weitere Details weiter unten)
emcc example.c -O3 -o a.html -sASYNCIFY -sASYNCIFY_IMPORTS=do_fetch
Schließlich, wenn Sie auch keine Promises verwenden können, können Sie das Beispiel umschreiben, um Asyncify.handleSleep zu verwenden, welches einen wakeUp-Callback an Ihre Funktionsimplementierung übergibt. Wenn dieser wakeUp-Callback aufgerufen wird, wird der C/C++-Code fortgesetzt
EM_JS(int, do_fetch, (), {
return Asyncify.handleSleep((wakeUp) => {
out("waiting for a fetch");
fetch("a.html").then(function (response) {
out("got the fetch response");
// (normally you would do something with the fetch here)
wakeUp(42);
});
});
});
Beachten Sie, dass Sie bei Verwendung dieser Form keinen Wert von der Funktion selbst zurückgeben können. Stattdessen müssen Sie ihn als Argument an den wakeUp-Callback übergeben und ihn durch Rückgabe des Ergebnisses von Asyncify.handleSleep in do_fetch selbst weiterleiten.
ASYNCIFY_IMPORTS¶Wie im obigen Beispiel können Sie JS-Funktionen hinzufügen, die eine asynchrone Operation ausführen, aber aus der Perspektive von C synchron erscheinen. Wenn Sie EM_ASYNC_JS nicht verwenden, ist es unerlässlich, solche Methoden zu ASYNCIFY_IMPORTS hinzuzufügen. Diese Liste der Importe ist die Liste der Importe in das Wasm-Modul, die der Asyncify-Instrumentierung bekannt sein müssen. Indem Sie diese Liste bereitstellen, teilen Sie mit, dass alle anderen JS-Aufrufe keine asynchrone Operation ausführen werden, wodurch unnötiger Overhead vermieden wird.
Hinweis
Wenn der Import nicht innerhalb von env liegt, muss der vollständige Pfad angegeben werden, zum Beispiel ASYNCIFY_IMPORTS=wasi_snapshot_preview1.fd_write
Wenn Sie Asyncify in dynamischen Bibliotheken verwenden möchten, sollten die Methoden, die von anderen verknüpften Modulen importiert werden (und die bei einer asynchronen Operation auf dem Stack liegen werden), in ASYNCIFY_IMPORTS aufgeführt werden.
// sleep.cpp
#include <emscripten.h>
extern "C" void sleep_for_seconds() {
emscripten_sleep(100);
}
Im Seitenmodul können Sie sleep.cpp auf die übliche Emscripten-Art der dynamischen Verknüpfung kompilieren
emcc sleep.cpp -O3 -o libsleep.wasm -sASYNCIFY -sSIDE_MODULE
// main.cpp
#include <emscripten.h>
extern "C" void sleep_for_seconds();
int main() {
sleep_for_seconds();
return 0;
}
Im Hauptmodul weiß der Compiler nicht statisch, dass sleep_for_seconds asynchron ist. Daher müssen Sie sleep_for_seconds zur Liste ASYNCIFY_IMPORTS hinzufügen.
emcc main.cpp libsleep.wasm -O3 -sASYNCIFY -sASYNCIFY_IMPORTS=sleep_for_seconds -sMAIN_MODULE
Wenn Sie Embind für die Interaktion mit JavaScript verwenden und ein dynamisch abgerufene Promise awaiten möchten, können Sie eine await()-Methode direkt auf der val-Instanz aufrufen
val my_object = /* ... */;
val result = my_object.call<val>("someAsyncMethod").await();
In diesem Fall müssen Sie sich keine Gedanken über ASYNCIFY_IMPORTS oder JSPI_IMPORTS machen, da es sich um ein internes Implementierungsdetail von val::await handelt und Emscripten sich automatisch darum kümmert.
Beachten Sie, dass bei Verwendung von Embind-Exporten Asyncify und JSPI sich unterschiedlich verhalten. Wenn Asyncify mit Embind verwendet wird und der Code von JavaScript aufgerufen wird, dann gibt die Funktion ein Promise zurück, wenn der Export suspendierende Funktionen aufruft, ansonsten wird das Ergebnis synchron zurückgegeben. Bei JSPI muss jedoch der Parameter emscripten::async() verwendet werden, um die Funktion als asynchron zu kennzeichnen, und der Export wird immer ein Promise zurückgeben, unabhängig davon, ob der Export suspendiert wurde.
#include <emscripten/bind.h>
#include <emscripten.h>
static int delayAndReturn(bool sleep) {
if (sleep) {
emscripten_sleep(0);
}
return 42;
}
EMSCRIPTEN_BINDINGS(example) {
// Asyncify
emscripten::function("delayAndReturn", &delayAndReturn);
// JSPI
emscripten::function("delayAndReturn", &delayAndReturn, emscripten::async());
}
Bau mit
emcc -O3 example.cpp -lembind -s<ASYNCIFY or JSPI>
Dann von JavaScript aus aufrufen (mit Asyncify)
let syncResult = Module.delayAndReturn(false);
console.log(syncResult); // 42
console.log(await syncResult); // also 42 because `await` is no-op
let asyncResult = Module.delayAndReturn(true);
console.log(asyncResult); // Promise { <pending> }
console.log(await asyncResult); // 42
Im Gegensatz zu JavaScript async Funktionen, die immer ein Promise zurückgeben, wird der Rückgabewert zur Laufzeit bestimmt, und ein Promise wird nur zurückgegeben, wenn Asyncify-Aufrufe (wie emscripten_sleep(), val::await(), etc.) auftreten.
Ist der Codepfad unbestimmt, kann der Aufrufer entweder prüfen, ob der zurückgegebene Wert ein instanceof Promise ist oder einfach auf den zurückgegebenen Wert awaiten.
Bei Verwendung von JSPI sind die Rückgabewerte immer ein Promise, wie unten gezeigt
let syncResult = Module.delayAndReturn(false);
console.log(syncResult); // Promise { <pending> }
console.log(await syncResult); // 42
let asyncResult = Module.delayAndReturn(true);
console.log(asyncResult); // Promise { <pending> }
console.log(await asyncResult); // 42
ccall¶Um einen Asyncify-nutzenden Wasm-Export von Javascript aus zu verwenden, können Sie die Funktion Module.ccall verwenden und async: true an das Call-Options-Objekt übergeben. ccall gibt dann ein Promise zurück, das mit dem Ergebnis der Funktion aufgelöst wird, sobald die Berechnung abgeschlossen ist.
In diesem Beispiel wird eine Funktion "func" aufgerufen, die eine Zahl zurückgibt.
Module.ccall("func", "number", [], [], {async: true}).then(result => {
console.log("js_func: " + result);
});
Neben der Verwendung unterschiedlicher zugrunde liegender Mechanismen behandeln Asyncify und JSPI auch asynchrone Importe und Exporte unterschiedlich. Asyncify bestimmt automatisch, welche Exporte asynchron werden, basierend darauf, was potenziell einen asynchronen Import (ASYNCIFY_IMPORTS) aufrufen könnte. Bei JSPI müssen die asynchronen Importe und Exporte jedoch explizit über die Einstellungen JSPI_IMPORTS und JSPI_EXPORTS festgelegt werden.
Hinweis
<JSPI/ASYNCIFY>_IMPORTS und JSPI_EXPORTS werden nicht benötigt, wenn verschiedene der oben genannten Helfer verwendet werden, wie z.B.: EM_ASYNC_JS, Embinds Async-Unterstützung, ccall, etc…
Hinweis
Dieser Abschnitt gilt nicht für JSPI.
Wie bereits erwähnt, können unoptimierte Builds mit Asyncify groß und langsam sein. Kompilieren Sie mit Optimierungen (z.B. -O3), um gute Ergebnisse zu erzielen.
Asyncify verursacht Overhead, sowohl bei der Code-Größe als auch bei der Geschwindigkeit, da es Code instrumentiert, um das Entwinden und Zurückspulen zu ermöglichen. Dieser Overhead ist normalerweise nicht extrem, etwa 50% oder so. Asyncify erreicht dies durch eine ganzheitliche Programmanalyse, um Funktionen zu finden, die instrumentiert werden müssen und welche nicht – im Grunde, welche etwas aufrufen können, das eine der ASYNCIFY_IMPORTS erreicht. Diese Analyse vermeidet viel unnötigen Overhead, ist jedoch durch indirekte Aufrufe begrenzt, da sie nicht wissen kann, wohin sie führen – es könnte alles in der Funktionstabelle sein (mit dem gleichen Typ).
Wenn Sie wissen, dass indirekte Aufrufe beim Entwinden niemals auf dem Stack sind, können Sie Asyncify anweisen, indirekte Aufrufe mit ASYNCIFY_IGNORE_INDIRECT zu ignorieren.
Wenn Sie wissen, dass einige indirekte Aufrufe wichtig sind und andere nicht, können Sie Asyncify eine manuelle Liste von Funktionen bereitstellen
ASYNCIFY_REMOVE ist eine Liste von Funktionen, die den Stack nicht entwinden. Wenn Asyncify den Aufrufbaum verarbeitet, werden Funktionen in dieser Liste entfernt, und weder sie noch ihre Aufrufer werden instrumentiert (es sei denn, ihre Aufrufer müssen aus anderen Gründen instrumentiert werden).
ASYNCIFY_ADD ist eine Liste von Funktionen, die den Stack entwinden und wie die Importe verarbeitet werden. Dies ist meistens nützlich, wenn Sie ASYNCIFY_IGNORE_INDIRECT verwenden, aber auch einige zusätzliche Funktionen markieren möchten, die entwinden müssen. Wenn die Einstellung ASYNCIFY_PROPAGATE_ADD jedoch deaktiviert ist, wird diese Liste erst nach der Ganzprogrammanalyse hinzugefügt. Wenn ASYNCIFY_PROPAGATE_ADD deaktiviert ist, müssen Sie auch deren Aufrufer, deren Aufrufer und so weiter hinzufügen.
ASYNCIFY_ONLY ist eine Liste der einzigen Funktionen, die den Stack entwinden können. Asyncify wird genau diese und keine anderen instrumentieren.
Sie können die Einstellung ASYNCIFY_ADVISE aktivieren, die dem Compiler mitteilt, welche Funktionen er gerade instrumentiert und warum. Sie können dann feststellen, ob Sie Funktionen zu ASYNCIFY_REMOVE hinzufügen sollten oder ob es sicher wäre, ASYNCIFY_IGNORE_INDIRECT zu aktivieren. Beachten Sie, dass diese Phase des Compilers nach vielen Optimierungsphasen stattfindet und mehrere Funktionen bereits inline sein können. Um sicherzugehen, führen Sie sie mit -O0 aus.
Weitere Details finden Sie in settings.js. Beachten Sie, dass die hier genannten manuellen Einstellungen fehleranfällig sind – wenn Sie die Dinge nicht genau richtig machen, kann Ihre Anwendung abstürzen. Wenn Sie nicht unbedingt maximale Leistung benötigen, ist es normalerweise in Ordnung, die Standardeinstellungen zu verwenden.
Wenn Sie eine Ausnahme sehen, die von einer asyncify_* API ausgelöst wird, kann es sich um einen Stack-Überlauf handeln. Sie können die Stack-Größe mit der Option ASYNCIFY_STACK_SIZE erhöhen.
Während des Wartens auf eine asynchrone Operation können Browser-Events auftreten. Das ist oft der Sinn der Verwendung von Asyncify, aber auch unerwartete Ereignisse können auftreten. Wenn Sie beispielsweise nur 100 ms pausieren möchten, können Sie emscripten_sleep(100) aufrufen, aber wenn Sie Event-Listener haben, z.B. für einen Tastendruck, dann wird der Handler ausgelöst, wenn eine Taste gedrückt wird. Wenn dieser Handler kompilierten Code aufruft, kann das verwirrend sein, da es wie Coroutinen oder Multithreading aussieht, mit mehreren verschachtelten Ausführungen.
Es ist nicht sicher, eine asynchrone Operation zu starten, während eine andere bereits läuft. Die erste muss abgeschlossen sein, bevor die zweite beginnt.
Solche Verschachtelungen können auch Annahmen in Ihrer Codebasis verletzen. Wenn beispielsweise eine Funktion eine globale Variable verwendet und davon ausgeht, dass nichts anderes sie ändern kann, bis sie zurückkehrt, aber wenn diese Funktion schläft und ein Ereignis dazu führt, dass anderer Code diese globale Variable ändert, können schlechte Dinge passieren.
Die obigen Beispiele zeigen, dass wakeUp() von JS (typischerweise nach einem Callback) aufgerufen wird, und ohne kompilierten Code auf dem Stack. Wenn kompilierten Code auf dem Stack wäre, könnte dies das korrekte Zurückspulen und Fortsetzen der Ausführung auf verwirrende Weise beeinträchtigen, und daher wird in einem Build mit ASSERTIONS eine Assertion ausgelöst.
(Insbesondere besteht das Problem darin, dass das Zurückspulen zwar ordnungsgemäß funktioniert, aber wenn Sie später erneut entwinden, wird dieses Entwinden auch durch den zusätzlichen kompilierten Code auf dem Stack erfolgen, was zu einem späteren schlechten Verhalten beim Zurückspulen führt.)
Eine einfache und nützliche Abhilfe ist ein setTimeout von 0, bei dem wakeUp() durch setTimeout(wakeUp, 0); ersetzt wird. Dadurch wird wakeUp in einem späteren Callback ausgeführt, wenn nichts anderes auf dem Stack ist.
Wenn Sie Code haben, der die alte Emterpreter-Async-API oder die alte Asyncify verwendet, sollte fast alles funktionieren, wenn Sie die Verwendung von -sEMTERPRETIFY durch -sASYNCIFY ersetzen. Insbesondere sollten alle Dinge wie emscripten_wget weiterhin wie zuvor funktionieren.
Einige geringfügige Unterschiede sind
Der Emterpreter hatte das Konzept des „Yielding“, das aber in Asyncify nicht benötigt wird. Sie können Aufrufe von
emscripten_sleep_with_yield()durchemscripten_sleep()ersetzen.Die interne JS-API ist anders. Siehe die obigen Hinweise zu
Asyncify.handleSleep()und weitere Beispiele insrc/library_async.js.