Asynchrone Web-APIs von WebAssembly verwenden

Die E/A-APIs im Web sind asynchron, in den meisten Systemsprachen jedoch synchron. Wenn Sie Code in WebAssembly kompilieren, müssen Sie eine Brücke zwischen einer API und einer anderen API schlagen. Diese Brücke ist Asyncify. In diesem Beitrag erfahren Sie, wann und wie Sie Asyncify verwenden und wie es im Hintergrund funktioniert.

E/A in Systemsprachen

Ich beginne mit einem einfachen Beispiel in C. Angenommen, Sie möchten den Namen des Nutzers aus einer Datei lesen und ihn mit der Nachricht „Hallo (Nutzername)!“ begrüßen:

#include <stdio.h>

int main() {
    FILE *stream = fopen("name.txt", "r");
    char name[20+1];
    size_t len = fread(&name, 1, 20, stream);
    name[len] = '\0';
    fclose(stream);
    printf("Hello, %s!\n", name);
    return 0;
}

Das Beispiel ist zwar nicht sehr leistungsfähig, zeigt aber bereits etwas, das Sie in einer Anwendung jeder Größe finden: Es liest einige Eingaben von außen, verarbeitet sie intern und schreibt die Ausgabe zurück an die Außenwelt. Alle diese Interaktionen mit der Außenwelt erfolgen über einige Funktionen, die allgemein als Eingabe-/Ausgabefunktionen bezeichnet werden, auch abgekürzt als E/A.

Um den Namen aus C zu lesen, sind mindestens zwei wichtige E/A-Aufrufe erforderlich: fopen, um die Datei zu öffnen, und fread, um Daten daraus zu lesen. Nachdem Sie die Daten abgerufen haben, können Sie mit einer anderen E/A-Funktion printf das Ergebnis in der Konsole ausgeben.

Diese Funktionen sehen auf den ersten Blick recht einfach aus und Sie müssen nicht zweimal über die Abläufe zum Lesen oder Schreiben von Daten nachdenken. Je nach Umgebung kann es jedoch ziemlich viel los sein:

  • Wenn sich die Eingabedatei auf einem lokalen Laufwerk befindet, muss die Anwendung eine Reihe von Speicher- und Laufwerkzugriffen ausführen, um die Datei zu finden, die Berechtigungen zu prüfen, sie zum Lesen zu öffnen und dann Block für Block zu lesen, bis die angeforderte Anzahl von Byte abgerufen wurde. Dies kann je nach Geschwindigkeit des Laufwerks und der angeforderten Größe ziemlich langsam sein.
  • Möglicherweise befindet sich die Eingabedatei auch an einem bereitgestellten Netzwerkspeicherort. In diesem Fall ist jetzt auch der Netzwerkstack beteiligt, was die Komplexität, die Latenz und die Anzahl der möglichen Wiederholungen für jeden Vorgang erhöht.
  • Schließlich gibt auch printf keine Garantie dafür, dass etwas an die Konsole ausgegeben wird. Möglicherweise wird es an eine Datei oder einen Netzwerkspeicherort weitergeleitet. In diesem Fall müssen die oben genannten Schritte ausgeführt werden.

Kurz gesagt: I/O-Vorgänge können langsam sein und Sie können nicht anhand eines kurzen Blicks auf den Code vorhersagen, wie lange ein bestimmter Aufruf dauern wird. Während dieser Vorgang ausgeführt wird, erscheint die gesamte Anwendung eingefroren und reagiert nicht auf Nutzereingaben.

Das ist auch nicht auf C oder C++ beschränkt. Die meisten Systemsprachen stellen alle E/A-Vorgänge in Form von synchronen APIs dar. Wenn Sie das Beispiel beispielsweise in Rust übersetzen, sieht die API möglicherweise einfacher aus, aber es gelten dieselben Prinzipien. Sie führen einfach einen Aufruf aus und warten synchron auf das Ergebnis, während alle ressourcenintensiven Vorgänge ausgeführt und das Ergebnis schließlich in einem einzigen Aufruf zurückgegeben wird:

fn main() {
    let s = std::fs::read_to_string("name.txt");
    println!("Hello, {}!", s);
}

Aber was passiert, wenn Sie versuchen, eines dieser Beispiele in WebAssembly zu kompilieren und in das Web zu übersetzen? Oder, um ein konkretes Beispiel zu nennen: Was könnte der Vorgang „Datei lesen“ bedeuten? Es müssten Daten aus einem Speicher gelesen werden.

Asynchrones Modell des Webs

Im Web gibt es eine Vielzahl verschiedener Speicheroptionen, die Sie zuordnen können, z. B. In-Memory-Speicher (JS-Objekte), localStorage, IndexedDB, serverseitiger Speicher und die neue File System Access API.

Allerdings können nur zwei dieser APIs – der In-Memory-Speicher und die localStorage – synchron verwendet werden. Beide sind die Optionen mit den stärksten Einschränkungen in Bezug auf das, was und wie lange Sie speichern können. Bei allen anderen Optionen werden nur asynchrone APIs bereitgestellt.

Dies ist eine der Haupteigenschaften der Codeausführung im Web: Alle zeitaufwendigen Vorgänge, einschließlich E/A-Vorgängen, müssen asynchron sein.

Der Grund dafür ist, dass das Web traditionell ein einzelner Thread ist und jeder Nutzercode, der die Benutzeroberfläche betrifft, im selben Thread wie die Benutzeroberfläche ausgeführt werden muss. Sie muss mit anderen wichtigen Aufgaben wie Layout, Rendering und Ereignisbehandlung um die CPU-Zeit konkurrieren. Es ist nicht wünschenswert, dass ein JavaScript- oder WebAssembly-Code einen Dateilesevorgang starten und alles andere – den gesamten Tab oder in der Vergangenheit den gesamten Browser – für einen Zeitraum von Millisekunden bis zu einigen Sekunden blockieren kann, bis der Vorgang abgeschlossen ist.

Stattdessen darf Code einen E/A-Vorgang nur zusammen mit einem Callback planen, der nach Abschluss ausgeführt werden soll. Solche Rückrufe werden im Rahmen des Ereignis-Loops des Browsers ausgeführt. Ich werde hier nicht weiter auf Details eingehen. Wenn Sie jedoch wissen möchten, wie der Ereignis-Loop im Detail funktioniert, lesen Sie den Artikel Tasks, Microtasks, Queues und Scheduler. Dort wird dieses Thema ausführlich erklärt.

Kurz gesagt: Der Browser führt alle Codeteile in einer Art Endlosschleife aus, indem er sie nacheinander aus der Warteschlange nimmt. Wenn ein Ereignis ausgelöst wird, stellt der Browser den entsprechenden Handler in die Warteschlange. Bei der nächsten Iteration der Schleife wird er aus der Warteschlange genommen und ausgeführt. Mit diesem Mechanismus können Sie die Parallelität simulieren und viele parallele Vorgänge ausführen, während nur ein einzelner Thread verwendet wird.

Wichtig ist, dass der Ereignis-Loop blockiert ist, während Ihr benutzerdefinierter JavaScript- (oder WebAssembly-)Code ausgeführt wird. In diesem Fall kann nicht auf externe Handler, Ereignisse, E/A usw. reagiert werden. Die einzigen Möglichkeiten, die E/A-Ergebnisse zurückzugeben, sind die Registrierung eines Rückrufs, die Ausführung des Codes und die Rückgabe der Steuerung an den Browser, damit er alle ausstehenden Aufgaben weiter verarbeiten kann. Sobald die E/A abgeschlossen ist, wird Ihr Handler zu einer dieser Aufgaben und wird ausgeführt.

Wenn Sie beispielsweise die obigen Beispiele in modernem JavaScript neu schreiben und einen Namen aus einer Remote-URL lesen möchten, verwenden Sie die Fetch API und die Async-Await-Syntax:

async function main() {
  let response = await fetch("name.txt");
  let name = await response.text();
  console.log("Hello, %s!", name);
}

Auch wenn es synchron aussieht, ist jede await im Grunde eine Syntax-Vereinfachung für Callbacks:

function main() {
  return fetch("name.txt")
    .then(response => response.text())
    .then(name => console.log("Hello, %s!", name));
}

In diesem Beispiel ohne Verschleierung, das etwas verständlicher ist, wird eine Anfrage gestartet und Antworten werden mit dem ersten Rückruf abonniert. Sobald der Browser die erste Antwort – nur die HTTP-Header – empfängt, ruft er diesen Rückruf asynchron auf. Der Callback beginnt mit dem Lesen des Textkörpers mit response.text() und abonniert das Ergebnis mit einem anderen Callback. Sobald fetch alle Inhalte abgerufen hat, ruft es den letzten Rückruf auf, der „Hallo, (Nutzername)!“ in die Konsole druckt.

Da diese Schritte asynchron sind, kann die ursprüngliche Funktion die Steuerung an den Browser zurückgeben, sobald die E/A geplant wurde. Die gesamte UI ist dann responsiv und für andere Aufgaben wie Rendering, Scrollen usw. verfügbar, während die E/A im Hintergrund ausgeführt wird.

Als letztes Beispiel: Auch einfache APIs wie „sleep“, mit der eine Anwendung eine bestimmte Anzahl von Sekunden wartet, sind eine Form von I/O-Vorgang:

#include <stdio.h>
#include <unistd.h>
// ...
printf("A\n");
sleep(1);
printf("B\n");

Natürlich können Sie das ganz einfach so übersetzen, dass der aktuelle Thread bis zum Ablauf der Zeit blockiert wird:

console.log("A");
for (let start = Date.now(); Date.now() - start < 1000;);
console.log("B");

Genau das tut Emscripten in der Standardimplementierung von „sleep“. Das ist jedoch sehr ineffizient, wodurch die gesamte UI blockiert wird und in der Zwischenzeit keine anderen Ereignisse verarbeitet werden. Das ist im Produktionscode in der Regel nicht empfehlenswert.

Eine idiomatischere Version von „sleep“ in JavaScript würde stattdessen das Aufrufen von setTimeout() und das Abonnieren eines Handlers beinhalten:

console.log("A");
setTimeout(() => {
    console.log("B");
}, 1000);

Was haben alle diese Beispiele und APIs gemeinsam? In jedem Fall verwendet der idiomatische Code in der ursprünglichen Systemsprache eine blockierende API für die E/A, während in einem entsprechenden Beispiel für das Web stattdessen eine asynchrone API verwendet wird. Beim Kompilieren für das Web müssen Sie zwischen diesen beiden Ausführungsmodellen irgendwie transformieren. WebAssembly bietet derzeit keine integrierte Möglichkeit dazu.

Mit Asyncify die Lücke schließen

Hier kommt Asyncify ins Spiel. Asyncify ist ein von Emscripten unterstütztes Feature zur Kompilierungszeit, mit dem das gesamte Programm angehalten und später asynchron fortgesetzt werden kann.

Ein Aufrufgraph, der einen JavaScript- > WebAssembly- > Web-API- > asynchronen Aufgabenaufruf beschreibt, bei dem Asyncify das Ergebnis der asynchronen Aufgabe wieder mit WebAssembly verbindet

Verwendung in C/C++ mit Emscripten

Wenn Sie Asyncify verwenden möchten, um für das letzte Beispiel eine asynchrone Pause zu implementieren, können Sie Folgendes tun:

#include <stdio.h>
#include <emscripten.h>

EM_JS(void, async_sleep, (int seconds), {
    Asyncify.handleSleep(wakeUp => {
        setTimeout(wakeUp, seconds * 1000);
    });
});
…
puts("A");
async_sleep(1);
puts("B");

EM_JS ist ein Makro, mit dem JavaScript-Snippets so definiert werden können, als wären sie C-Funktionen. Verwenden Sie darin die Funktion Asyncify.handleSleep(), die Emscripten anweist, das Programm anzuhalten, und einen wakeUp()-Handler bereitstellen, der nach Abschluss des asynchronen Vorgangs aufgerufen werden sollte. Im obigen Beispiel wird der Handler an setTimeout() übergeben, er kann aber in jedem anderen Kontext verwendet werden, der Callbacks akzeptiert. Schließlich können Sie async_sleep() wie normale sleep() oder jede andere synchrone API an einer beliebigen Stelle aufrufen.

Beim Kompilieren solchen Codes müssen Sie Emscripten anweisen, die Asyncify-Funktion zu aktivieren. Dazu übergeben Sie -s ASYNCIFY und -s ASYNCIFY_IMPORTS=[func1, func2] eine arrayähnliche Liste von Funktionen, die asynchron sein können.

emcc -O2 \
    -s ASYNCIFY \
    -s ASYNCIFY_IMPORTS=[async_sleep] \
    ...

Dadurch weiß Emscripten, dass für Aufrufe dieser Funktionen möglicherweise der Zustand gespeichert und wiederhergestellt werden muss. Der Compiler fügt daher unterstützenden Code um solche Aufrufe ein.

Wenn Sie diesen Code jetzt im Browser ausführen, sehen Sie ein nahtloses Ausgabeprotokoll, wie Sie es erwarten würden, wobei B nach einer kurzen Verzögerung nach A kommt.

A
B

Sie können auch Werte aus Asyncify-Funktionen zurückgeben. Sie müssen das Ergebnis von handleSleep() zurückgeben und das Ergebnis an den wakeUp()-Callback übergeben. Wenn Sie beispielsweise eine Zahl nicht aus einer Datei lesen, sondern aus einer Remote-Ressource abrufen möchten, können Sie mit einem Snippet wie dem unten stehenden eine Anfrage senden, den C-Code anhalten und fortsetzen, sobald der Antworttext abgerufen wurde. Das funktioniert nahtlos, als wäre der Aufruf synchron.

EM_JS(int, get_answer, (), {
     return Asyncify.handleSleep(wakeUp => {
        fetch("answer.txt")
            .then(response => response.text())
            .then(text => wakeUp(Number(text)));
    });
});
puts("Getting answer...");
int answer = get_answer();
printf("Answer is %d\n", answer);

Bei Promise-basierten APIs wie fetch() können Sie Asyncify sogar mit der async-await-Funktion von JavaScript kombinieren, anstatt die callback-basierte API zu verwenden. Rufen Sie dazu Asyncify.handleAsync() anstelle von Asyncify.handleSleep() auf. Anstatt einen wakeUp()-Callback planen zu müssen, können Sie dann eine async-JavaScript-Funktion übergeben und await und return darin verwenden. So wirkt der Code noch natürlicher und synchroner, ohne dass die Vorteile der asynchronen E/A verloren gehen.

EM_JS(int, get_answer, (), {
     return Asyncify.handleAsync(async () => {
        let response = await fetch("answer.txt");
        let text = await response.text();
        return Number(text);
    });
});

int answer = get_answer();

Es werden komplexe Werte erwartet

In diesem Beispiel sind aber immer noch nur Zahlen zulässig. Was ist, wenn Sie das ursprüngliche Beispiel implementieren möchten, in dem ich versucht habe, den Namen eines Nutzers aus einer Datei als String abzurufen? Das ist auch möglich.

Emscripten bietet die Funktion Embind, mit der Sie Konvertierungen zwischen JavaScript- und C++-Werten verarbeiten können. Außerdem wird Asyncify unterstützt. Du kannst await() also für externe Promises aufrufen. Es funktioniert dann genauso wie await in async-await-JavaScript-Code:

val fetch = val::global("fetch");
val response = fetch(std::string("answer.txt")).await();
val text = response.call<val>("text").await();
auto answer = text.as<std::string>();

Bei dieser Methode müssen Sie ASYNCIFY_IMPORTS nicht einmal als Kompilierungsflag übergeben, da es standardmäßig bereits enthalten ist.

Okay, das funktioniert in Emscripten gut. Was ist mit anderen Toolchains und Sprachen?

Verwendung aus anderen Sprachen

Angenommen, Sie haben irgendwo in Ihrem Rust-Code einen ähnlichen synchronen Aufruf, den Sie einer asynchronen API im Web zuordnen möchten. Wie Sie sehen, können Sie das auch!

Zuerst müssen Sie eine solche Funktion als regulären Import über einen extern-Block (oder die Syntax der von Ihnen ausgewählten Sprache für externe Funktionen) definieren.

extern {
    fn get_answer() -> i32;
}

println!("Getting answer...");
let answer = get_answer();
println!("Answer is {}", answer);

Und kompilieren Sie den Code in WebAssembly:

cargo build --target wasm32-unknown-unknown

Jetzt müssen Sie die WebAssembly-Datei mit Code zum Speichern und Wiederherstellen des Stacks instrumentieren. Bei C/C++ würde Emscripten dies für uns erledigen, wird hier aber nicht verwendet. Daher ist der Prozess etwas manueller.

Glücklicherweise ist die Asyncify-Transformation selbst völlig unabhängig von der Toolchain. Sie kann beliebige WebAssembly-Dateien umwandeln, unabhängig davon, von welchem Compiler sie erstellt wird. Die Transformation wird separat als Teil des wasm-opt-Optimierers aus der Binaryen-Toolchain bereitgestellt und kann so aufgerufen werden:

wasm-opt -O2 --asyncify \
      --pass-arg=asyncify-imports@env.get_answer \
      [...]

Übergeben Sie --asyncify, um die Transformation zu aktivieren, und verwenden Sie dann --pass-arg=…, um eine durch Kommas getrennte Liste von asynchronen Funktionen anzugeben, bei denen der Programmstatus angehalten und später fortgesetzt werden soll.

Es bleibt nur noch, den unterstützenden Runtime-Code bereitzustellen, der dies tatsächlich tut – WebAssembly-Code anhalten und fortsetzen. Im Fall von C/C++ würde dies wieder von Emscripten übernommen, aber jetzt benötigen Sie benutzerdefinierten JavaScript-Bindungscode, der beliebige WebAssembly-Dateien verarbeitet. Dafür haben wir eine Bibliothek erstellt.

Sie finden sie auf GitHub unter https://github.com/GoogleChromeLabs/asyncify oder npm unter dem Namen asyncify-wasm.

Sie simuliert eine standardmäßige WebAssembly-Instanzierungs-API, aber in einem eigenen Namespace. Der einzige Unterschied besteht darin, dass Sie unter einer regulären WebAssembly API nur synchrone Funktionen als Importe angeben können, während Sie unter dem Asyncify-Wrapper auch asynchrone Importe angeben können:

const { instance } = await Asyncify.instantiateStreaming(fetch('app.wasm'), {
    env: {
        async get_answer() {
            let response = await fetch("answer.txt");
            let text = await response.text();
            return Number(text);
        }
    }
});
…
await instance.exports.main();

Wenn Sie versuchen, eine solche asynchrone Funktion wie get_answer() im Beispiel oben von der WebAssembly-Seite aus aufzurufen, erkennt die Bibliothek den zurückgegebenen Promise, hält die Ausführung an und speichert den Status der WebAssembly-Anwendung. Außerdem wird die Fertigstellung des Promises abonniert. Sobald das Problem behoben ist, werden der Aufrufstapel und der Status nahtlos wiederhergestellt und die Ausführung wird fortgesetzt, als wäre nichts passiert.

Da jede Funktion im Modul einen asynchronen Aufruf ausführen kann, werden auch alle Exporte potenziell asynchron, sodass sie ebenfalls umschlossen werden. Im obigen Beispiel haben Sie vielleicht bemerkt, dass Sie das Ergebnis von instance.exports.main() await müssen, um zu wissen, wann die Ausführung wirklich abgeschlossen ist.

Wie funktioniert das alles?

Wenn Asyncify einen Aufruf einer der ASYNCIFY_IMPORTS-Funktionen erkennt, startet es einen asynchronen Vorgang, speichert den gesamten Zustand der Anwendung, einschließlich des Aufrufstacks und aller temporären lokalen Variablen, und stellt später, wenn dieser Vorgang abgeschlossen ist, den gesamten Arbeitsspeicher und den Aufrufstack wieder her und fährt an derselben Stelle und mit demselben Status fort, als wäre das Programm nie angehalten worden.

Das ähnelt der async-await-Funktion in JavaScript, die ich bereits gezeigt habe. Im Gegensatz zu dieser erfordert es jedoch keine spezielle Syntax oder Laufzeitunterstützung der Sprache, sondern funktioniert durch die Umwandlung einfacher synchroner Funktionen zur Laufzeit.

Wenn Sie das zuvor gezeigte Beispiel für den asynchronen Ruhemodus kompilieren:

puts("A");
async_sleep(1);
puts("B");

Asyncify wandelt diesen Code in etwa in den folgenden Code um (Pseudocode, die tatsächliche Transformation ist komplexer):

if (mode == NORMAL_EXECUTION) {
    puts("A");
    async_sleep(1);
    saveLocals();
    mode = UNWINDING;
    return;
}
if (mode == REWINDING) {
    restoreLocals();
    mode = NORMAL_EXECUTION;
}
puts("B");

Anfangs ist mode auf NORMAL_EXECUTION gesetzt. Wenn dieser transformierte Code zum ersten Mal ausgeführt wird, wird nur der Teil bis zu async_sleep() ausgewertet. Sobald der asynchrone Vorgang geplant ist, speichert Asyncify alle lokalen Daten und löst den Stack aus, indem sie von jeder Funktion ganz nach oben zurückkehrt. Auf diese Weise erhält die Browser-Ereignisschleife die Kontrolle zurück.

Sobald async_sleep() aufgelöst wurde, ändert der Asyncify-Supportcode mode in REWINDING und ruft die Funktion noch einmal auf. Dieses Mal wird der Zweig „normale Ausführung“ übersprungen, da die Aufgabe bereits beim letzten Mal ausgeführt wurde und ich vermeiden möchte, „A“ zweimal zu drucken. Stattdessen wird direkt der Zweig „Zurückspulen“ aufgerufen. Sobald dieser erreicht ist, werden alle gespeicherten lokalen Kontakte wiederhergestellt, der Modus wieder auf „Normal“ gesetzt und die Ausführung so fortgesetzt, als ob der Code nie gestoppt worden wäre.

Transformationskosten

Leider ist die Asyncify-Transformation nicht vollständig kostenlos, da sie einiges an unterstützenden Code zum Speichern und Wiederherstellen der lokalen Elemente einfügen muss, zum Navigieren im Aufrufstack unter verschiedenen Modi und so weiter. Es werden nur Funktionen geändert, die an der Befehlszeile als asynchron gekennzeichnet sind, sowie alle potenziellen Aufrufer. Der Overhead der Codegröße kann vor der Komprimierung jedoch immer noch etwa 50 % betragen.

Ein Diagramm mit dem Overhead der Codegröße für verschiedene Benchmarks, von nahezu 0 % bei optimierten Bedingungen bis über 100 % im Worst-Case

Das ist zwar nicht ideal, aber in vielen Fällen akzeptabel, wenn die Alternative darin besteht, die Funktion nicht zu haben oder den ursprünglichen Code erheblich umschreiben zu müssen.

Aktivieren Sie immer Optimierungen für die finalen Builds, damit der Wert nicht noch höher wird. Sie können auch die Asyncify-spezifischen Optimierungsoptionen aktivieren, um den Overhead zu reduzieren, indem Sie Transformationen auf bestimmte Funktionen und/oder direkte Funktionsaufrufe beschränken. Außerdem ist die Laufzeitleistung etwas geringer, was sich aber nur auf die asynchronen Aufrufe selbst auswirkt. Im Vergleich zu den Kosten für die eigentliche Arbeit sind sie jedoch in der Regel vernachlässigbar.

Praxisnahe Demos

Nachdem Sie sich die einfachen Beispiele angesehen haben, möchte ich nun zu komplexeren Szenarien übergehen.

Wie zu Beginn des Artikels erwähnt, ist eine der Speicheroptionen im Web eine asynchrone File System Access API. Es bietet Zugriff auf ein echtes Hostdateisystem über eine Webanwendung.

Andererseits gibt es einen De-facto-Standard namens WASI für WebAssembly-E/A in der Konsole und auf der Serverseite. Es wurde als Kompilierungsziel für Systemsprachen entwickelt und stellt alle Arten von Dateisystem- und anderen Vorgängen in traditioneller synchroner Form bereit.

Was wäre, wenn Sie sie aufeinander abbilden könnten? Dann können Sie jede Anwendung in jeder Quellsprache mit jeder Toolchain kompilieren, die das WASI-Ziel unterstützt, und sie in einer Sandbox im Web ausführen, während sie weiterhin auf echten Nutzerdateien ausgeführt werden kann. Mit Asyncify ist genau das möglich.

In dieser Demo habe ich den Rust-Chrom coreutils mit einigen kleineren Patches für WASI kompiliert, über die Asyncify-Transformation übergeben und asynchrone Bindungen von WASI zur File System Access API auf der JavaScript-Seite implementiert. In Kombination mit der Terminalkomponente Xterm.js wird eine realistische Shell bereitgestellt, die im Browsertab ausgeführt wird und mit echten Nutzerdateien arbeitet – genau wie ein echtes Terminal.

Sie können sich das Live unter https://wasi.rreverser.com/ ansehen.

Anwendungsfälle der Asynchronität sind nicht nur auf Timer und Dateisysteme beschränkt. Sie können auch speziellere APIs im Web verwenden.

Mit Asyncify ist es beispielsweise möglich, libusb – die wahrscheinlich beliebteste native Bibliothek für die Arbeit mit USB-Geräten – einer WebUSB API zuzuordnen, die asynchronen Zugriff auf solche Geräte im Web ermöglicht. Nach dem Zuordnen und Kompilieren konnte ich Standard-Libusb-Tests und ‑Beispiele direkt in der Sandbox einer Webseite auf ausgewählten Geräten ausführen.

Screenshot der libusb-Debugausgabe auf einer Webseite mit Informationen zur verbundenen Canon-Kamera

Wahrscheinlich geht es aber um die Story eines anderen Blogposts.

Diese Beispiele zeigen, wie leistungsstark Asyncify sein kann, um die Lücke zu schließen und alle Arten von Anwendungen ins Web zu portieren. So erhalten Sie plattformübergreifenden Zugriff, Sandboxing und eine bessere Sicherheit, ohne dass Funktionen verloren gehen.