Asynchrone Web-APIs von WebAssembly verwenden

Die E/A APIs im Web sind asynchron, aber in den meisten Systemsprachen synchron. Wenn Sie Code in WebAssembly kompilieren, müssen Sie eine Art von APIs mit einer anderen verbinden – diese Brücke ist „asyncify“. In diesem Beitrag erfährst du, wann und wie du Asyncify verwendest 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 auslesen und mit der Nachricht „Hello, (username)!“ begrüßt werden:

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

Obwohl das Beispiel nicht viel bietet, zeigt es bereits etwas, das Sie in einer Anwendung jeder Größe finden: Es liest einige Eingaben aus der externen Welt, verarbeitet sie intern und schreibt die Ausgaben zurück in die externe Welt. Solche Interaktionen mit der Außenwelt erfolgen über einige Funktionen, die im Allgemeinen als Eingabe-/Ausgabefunktionen bezeichnet und auch als E/A verkürzt werden.

Zum Lesen des Namens aus C benötigen Sie mindestens zwei wichtige E/A-Aufrufe: fopen zum Öffnen der Datei und fread zum Lesen von Daten aus der Datei. Nachdem Sie die Daten abgerufen haben, können Sie eine andere E/A-Funktion printf verwenden, um das Ergebnis in der Konsole auszugeben.

Diese Funktionen sehen auf den ersten Blick recht einfach aus und Sie müssen sich keine Gedanken über die Maschinen machen, die zum Lesen oder Schreiben von Daten erforderlich sind. Je nach Umgebung kann im Inneren jedoch viel passieren:

  • Wenn sich die Eingabedatei auf einem lokalen Laufwerk befindet, muss die Anwendung eine Reihe von Arbeitsspeicher- und Laufwerkzugriffen ausführen, um die Datei zu finden, Berechtigungen zu prüfen, sie zum Lesen zu öffnen und dann blockweise zu lesen, bis die angeforderte Anzahl von Byte abgerufen wurde. Je nach Geschwindigkeit des Laufwerks und angeforderter Größe kann dies ziemlich langsam sein.
  • Oder die Eingabedatei kann sich an einem bereitgestellten Netzwerkspeicherort befinden. In diesem Fall ist jetzt auch der Netzwerkstack beteiligt, wodurch die Komplexität, Latenz und Anzahl der potenziellen Wiederholungsversuche für jeden Vorgang erhöht werden.
  • Und schließlich ist auch printf nicht sicher, dass es Daten in der Konsole ausgibt, und möglicherweise an eine Datei oder einen Netzwerkspeicherort weitergeleitet wird. In diesem Fall muss es die oben genannten Schritte ausführen.

Kurz gesagt: E/A kann langsam sein und Sie können nicht mit einem kurzen Blick auf den Code vorhersagen, wie lange ein bestimmter Aufruf dauern wird. Während dieses Vorgangs wird die gesamte Anwendung eingefroren und reagiert auf Nutzer nicht mehr.

Dies ist auch nicht auf C oder C++ beschränkt. Die meisten Systemsprachen stellen alle E/A in Form von synchronen APIs dar. Wenn Sie das Beispiel beispielsweise in Rust übersetzen, sieht die API möglicherweise einfacher aus, es gelten jedoch dieselben Prinzipien. Sie führen einfach einen Aufruf aus und warten synchron, bis das Ergebnis zurückgegeben wird, während alle teuren Vorgänge ausgeführt und das Ergebnis schließlich mit 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 sie in das Web zu übersetzen? Oder was könnte die „Dateilesevorgang“ übersetzen, um ein konkretes Beispiel zu liefern? Es müsste Daten aus einem Teil des Speichers lesen.

Asynchrones Modell des Webs

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

Allerdings können nur zwei dieser APIs – der In-Memory-Speicher und der localStorage – synchron verwendet werden. Beide sind die Optionen, die wie lange gespeichert werden können, am stärksten. Alle anderen Optionen stellen nur asynchrone APIs zur Verfügung.

Dies ist eine der Kerneigenschaften der Codeausführung im Web: Jeder zeitaufwendige Vorgang, einschließlich E/A, muss asynchron sein.

Der Grund dafür ist, dass das Web bisher nur mit einem Thread verbunden ist und jeder Nutzercode, der die UI berührt, im selben Thread wie die UI ausgeführt werden muss. Er muss mit anderen wichtigen Aufgaben wie Layout, Rendering und Ereignisverarbeitung für die CPU-Zeit konkurrieren. Sie möchten nicht, dass ein JavaScript- oder WebAssembly-Element einen Lesevorgang für eine Datei starten und alles andere blockieren kann – den gesamten Tab oder in der Vergangenheit den gesamten Browser – für einen Zeitraum von Millisekunden bis zu einigen Sekunden, bis er vorbei ist.

Stattdessen darf Code nur einen E/A-Vorgang zusammen mit einem Callback planen, der nach Abschluss ausgeführt wird. Solche Callbacks werden als Teil der Ereignisschleife des Browsers ausgeführt. Ich werde hier nicht ins Detail gehen. Wenn Sie aber wissen möchten, wie die Ereignisschleife im Hintergrund funktioniert, finden Sie unter Aufgaben, Microtasks, Warteschlangen und Zeitpläne eine ausführliche Erläuterung dieses Themas.

Die kurze Version besagt, dass der Browser alle Teile des Codes in einer Art Endlosschleife ausführt, indem er sie nacheinander aus der Warteschlange entnimmt. Wenn ein Ereignis ausgelöst wird, stellt der Browser den entsprechenden Handler in die Warteschlange. Bei der nächsten Schleifeniteration wird er aus der Warteschlange genommen und ausgeführt. Dieser Mechanismus ermöglicht die Simulation der Nebenläufigkeit und die Ausführung vieler paralleler Vorgänge, während nur ein einziger Thread verwendet wird.

Sie sollten bei diesem Mechanismus unbedingt daran denken, dass während der Ausführung Ihres benutzerdefinierten JavaScript- oder WebAssembly-Codes die Ereignisschleife blockiert wird. Es gibt zwar keine Möglichkeit, auf externe Handler, Ereignisse, E/A usw. zu reagieren. Die einzige Möglichkeit, die E/A-Ergebnisse wiederherzustellen, besteht darin, einen Callback zu registrieren, die Ausführung des Codes abzuschließen und alle ausstehenden Steuerelemente an den Browser zurückzusenden. Sobald die E/A abgeschlossen ist, wird Ihr Handler zu einer dieser Aufgaben und wird ausgeführt.

Wenn Sie die obigen Beispiele beispielsweise in modernem JavaScript umschreiben und einen Namen aus einer Remote-URL lesen möchten, verwenden Sie die Fetch API und die Syntax "async-await":

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

Obwohl dies synchron aussieht, handelt es sich bei jedem await im Wesentlichen um Syntaxzucker für Callbacks:

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

In diesem Beispiel ohne Zucker, das etwas klarer ist, wird eine Anfrage gestartet und Antworten mit dem ersten Callback abonniert. Sobald der Browser die erste Antwort – nur die HTTP-Header – erhält, ruft er diesen Callback asynchron auf. Der Callback beginnt, den Textkörper mit response.text() als Text zu lesen, und abonniert das Ergebnis mit einem weiteren Callback. Sobald fetch alle Inhalte abgerufen hat, wird der letzte Callback aufgerufen. Daraufhin wird „Hello, (username)!“ an die Konsole ausgegeben.

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

Als letztes Beispiel sind selbst einfache APIs wie „sleep“, bei denen eine Anwendung eine bestimmte Anzahl von Sekunden wartet, ebenfalls eine Form eines E/A-Vorgangs:

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

Sie könnten ihn natürlich so übersetzen, dass der aktuelle Thread bis zum Ablauf der Zeit gesperrt würde:

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

Genau das tut Emscripten bei der Standardimplementierung von „sleep“. Dies ist jedoch sehr ineffizient. Es blockiert die gesamte UI und lässt keine anderen Ereignisse zu. Dies sollte im Allgemeinen nicht in Produktionscode gemacht werden.

Stattdessen würde eine idiomatischere Version von „sleep“ in JavaScript das Aufrufen von setTimeout() und das Abonnieren mit einem Handler beinhalten:

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

Was ist all 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 ein entsprechendes Beispiel für das Web stattdessen eine asynchrone API verwendet. Bei der Kompilierung im Web müssen Sie zwischen diesen beiden Ausführungsmodellen transformieren. WebAssembly hat dafür noch keine integrierte Fähigkeit.

Die Lücke mit Asyncify schließen

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

Eine Aufrufgrafik, die ein JavaScript -> WebAssembly -> Web API -> den Aufruf einer asynchronen Aufgabe beschreibt, wobei Asyncify das Ergebnis der asynchronen Aufgabe wieder mit WebAssembly verbindet.

Verwendung in C / C++ mit Emscripten

Wenn Sie mit Asyncify einen asynchronen Ruhemodus für das letzte Beispiel implementieren möchten, könnten Sie dies wie folgt 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 wie C-Funktionen definiert werden können. Verwenden Sie darin die Funktion Asyncify.handleSleep(), die Emscripten anweist, das Programm anzuhalten und einen wakeUp()-Handler bereitzustellen, der nach Abschluss des asynchronen Vorgangs aufgerufen werden sollte. Im Beispiel oben wird der Handler an setTimeout() übergeben, kann aber auch in jedem anderen Kontext verwendet werden, in dem Callbacks akzeptiert werden. Schließlich können Sie async_sleep() wie die normale sleep() oder eine beliebige andere synchrone API überall aufrufen.

Beim Kompilieren eines solchen Codes müssen Sie Emscripten anweisen, die Asyncify-Funktion zu aktivieren. Dazu übergeben Sie -s ASYNCIFY und -s ASYNCIFY_IMPORTS=[func1, func2] mit einer Array-ähnlichen Liste von Funktionen, die möglicherweise asynchron sind.

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

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

Wenn Sie diesen Code nun im Browser ausführen, sehen Sie wie erwartet ein nahtloses Ausgabeprotokoll, wobei B nach einer kurzen Verzögerung nach A kommt.

A
B

Sie können auch Werte von Asyncify-Funktionen zurückgeben. Dazu müssen Sie das Ergebnis von handleSleep() zurückgeben und an den wakeUp()-Callback übergeben. Wenn Sie beispielsweise statt aus einer Datei eine Nummer aus einer Remote-Ressource abrufen möchten, können Sie ein Snippet wie das folgende verwenden, um eine Anfrage zu senden, den C-Code anzuhalten und fortzufahren, sobald der Antworttext abgerufen wurde. Dies geschieht 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 JavaScript-Funktion „async-await“ kombinieren, anstatt die Callback-basierte API zu verwenden. Rufen Sie dazu statt Asyncify.handleSleep() Asyncify.handleAsync() auf. Anstatt einen wakeUp()-Callback zu planen, können Sie eine async-JavaScript-Funktion übergeben und darin await und return verwenden. So sieht der Code noch natürlicher und synchroner aus, ohne von den Vorteilen der asynchronen E/A-Vorgänge zu profitieren.

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

Warten auf komplexe Werte

In diesem Beispiel sind Sie jedoch immer noch auf Zahlen beschränkt. 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? Tja, das kannst du auch!

Emscripten bietet eine Funktion namens Embind, mit der sich Conversions zwischen JavaScript- und C++-Werten verarbeiten lassen. Es unterstützt auch Asyncify, sodass Sie await() für externe Promises aufrufen können und es funktioniert wie await im 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 Compile-Flag übergeben, da es bereits standardmäßig enthalten ist.

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

Nutzung in 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 den extern-Block definieren (oder die Syntax der von Ihnen gewählten Sprache für Fremdfunktionen).

extern {
    fn get_answer() -> i32;
}

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

Und kompilieren Sie Ihren Code in WebAssembly:

cargo build --target wasm32-unknown-unknown

Jetzt müssen Sie die WebAssembly-Datei mit Code zum Speichern/Wiederherstellen des Stacks instrumentieren. Für C/C++ würde Emscripten das übernehmen, aber hier wird es nicht verwendet, sodass der Vorgang etwas manueller ist.

Glücklicherweise ist die Asyncify-Transformation selbst vollständig von der Toolchain unabhängig. Sie kann beliebige WebAssembly-Dateien transformieren, unabhängig davon, von welchem Compiler sie erstellt wurde. 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 asynchroner Funktionen bereitzustellen, in denen der Programmstatus angehalten und später fortgesetzt werden soll.

Nun müssen Sie nur noch den Laufzeitcode bereitstellen, der dies unterstützt: WebAssembly-Code sperren und fortsetzen. Im C-/C++-Fall wäre dies wieder von Emscripten enthalten, aber jetzt benötigen Sie benutzerdefinierten JavaScript-Glue-Code, der beliebige WebAssembly-Dateien verarbeiten kann. Wir haben dafür eine Bibliothek erstellt.

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

Sie simuliert eine standardmäßige WebAssembly-Instanziierungs-API, jedoch unter einem eigenen Namespace. Der einzige Unterschied besteht darin, dass Sie unter einer regulären WebAssembly API nur synchrone Funktionen als Importe bereitstellen können, während Sie unter dem Asyncify-Wrapper auch asynchrone Importe bereitstellen 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 das zurückgegebene Promise, sperrt und speichert den Status der WebAssembly-Anwendung, abonniert die Promise-Vervollständigung. Sobald sie behoben ist, stellen Sie den Aufrufstack und den Status nahtlos wieder her und fahren mit der Ausführung fort, 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 zusammengefasst werden. Vielleicht ist Ihnen im obigen Beispiel aufgefallen, dass Sie das Ergebnis von instance.exports.main() mit await synchronisieren müssen, um zu wissen, wann die Ausführung wirklich abgeschlossen ist.

Wie funktioniert das alles im Hintergrund?

Wenn Asyncify einen Aufruf einer der ASYNCIFY_IMPORTS-Funktionen erkennt, wird ein asynchroner Vorgang gestartet und der gesamte Status der Anwendung, einschließlich des Aufrufstacks und aller temporären lokalen Instanzen, gespeichert. Wenn dieser Vorgang abgeschlossen ist, werden der gesamte Arbeitsspeicher- und Aufrufstack wiederhergestellt und an derselben Stelle und mit demselben Status fortgesetzt, als ob das Programm nie beendet worden wäre.

Diese Funktion ähnelt der Funktion „async-await“ in JavaScript, die ich zuvor gezeigt habe. Allerdings ist im Gegensatz zum JavaScript-Feature keine spezielle Syntax- oder Laufzeitunterstützung durch die Sprache erforderlich. Stattdessen werden einfache synchrone Funktionen zur Kompilierungszeit transformiert.

Beachten Sie beim Kompilieren des zuvor gezeigten Beispiels für den asynchronen Ruhemodus Folgendes:

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

Asyncify wandelt diesen Code in etwa in etwa so um (Pseudocode, echte Transformation ist komplizierter):

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

Standardmäßig ist mode auf NORMAL_EXECUTION festgelegt. Entsprechend wird bei der ersten Ausführung eines solchen transformierten Codes nur der Teil vor async_sleep() ausgewertet. Sobald der asynchrone Vorgang geplant ist, speichert Asyncify alle lokalen Quellen und entspannt den Stack, indem er von jeder Funktion ganz nach oben zurückkehrt. Auf diese Weise erhält die Browserereignisschleife wieder die Kontrolle.

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 er den Job bereits beim letzten Mal ausgeführt hat und ich vermeiden möchte, dass "A" zweimal ausgegeben wird. Stattdessen gelangt er direkt zum Zweig "Zurückspulen". Sobald dieser erreicht ist, werden alle gespeicherten lokalen Instanzen wiederhergestellt, der Modus wieder in den normalen Modus versetzt und die Ausführung fortgesetzt, als ob der Code nie angehalten worden wäre.

Transformationskosten

Leider ist die Asyncify-Transformation nicht völlig kostenlos, da sie eine Menge unterstützenden Code zum Speichern und Wiederherstellen all dieser lokalen Elemente, zum Navigieren im Aufrufstack in verschiedenen Modi usw. einschleust. Dabei wird versucht, nur Funktionen, die in der Befehlszeile als asynchron gekennzeichnet sind, und die potenziellen Aufrufer zu ändern. Der Aufwand für die Codegröße kann jedoch vor der Komprimierung noch bei etwa 50% betragen.

Ein Diagramm, das den Aufwand für die Codegröße für verschiedene Benchmarks zeigt, von nahezu 0% unter fein abgestimmten Bedingungen bis über 100% in schlimmsten Fällen

Dies ist nicht ideal, jedoch in vielen Fällen akzeptabel, wenn die Alternative nicht vollständig die Funktionalität umfasst oder erhebliche Überschreibungen im ursprünglichen Code erforderlich sind.

Achten Sie darauf, immer Optimierungen für die endgültigen Builds zu aktivieren, um einen noch höheren Anstieg zu vermeiden. Sie können auch Asyncify-spezifische Optimierungsoptionen aktivieren, um den Aufwand zu reduzieren, indem Sie Transformationen auf bestimmte Funktionen und/oder nur direkte Funktionsaufrufe beschränken. Es fallen auch geringe Kosten für die Laufzeitleistung an, sie sind jedoch auf die asynchronen Aufrufe selbst beschränkt. Im Vergleich zu den Kosten der Arbeit sind sie in der Regel jedoch vernachlässigbar.

Reale Demos

Nachdem Sie sich nun die einfachen Beispiele angesehen haben, kommen wir zu komplexeren Szenarien.

Wie am Anfang des Artikels erwähnt, ist eine der Speicheroptionen im Web die asynchrone File System Access API. Sie ermöglicht den 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 einer traditionellen synchronen Form bereit.

Was wäre, wenn Sie einen Eintrag einem anderen zuordnen könnten? Anschließend können Sie jede Anwendung in einer beliebigen Ausgangssprache mit einer beliebigen Toolchain kompilieren, die das WASI-Ziel unterstützt, und sie in einer Sandbox im Web ausführen, wobei sie weiterhin mit echten Nutzerdateien arbeiten kann. Mit Asyncify ist genau das möglich.

In dieser Demo habe ich die Rust-coreutils-Krate mit einigen kleineren Patches an WASI kompiliert, die über die Asyncify-Transformation übergeben und asynchrone Bindungen von WASI zur File System Access API auf der JavaScript-Seite implementiert haben. In Kombination mit der Xterm.js-Terminalkomponente bietet dies eine realistische Shell, die auf dem Browsertab ausgeführt wird und mit echten Nutzerdateien funktioniert – genau wie ein echtes Terminal.

Sie können ihn unter https://wasi.rreverser.com/ live ansehen.

Asyncify-Anwendungsfälle sind auch nicht nur auf Timer und Dateisysteme beschränkt. Sie können noch weiter gehen und Nischen-APIs im Web verwenden.

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

Screenshot der Ausgabe von libusb zur Fehlerbehebung auf einer Webseite mit Informationen zur verbundenen Canon-Kamera

Wahrscheinlich ist es aber auch eine Geschichte für einen anderen Blogbeitrag.

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