Asynchrone Web-APIs von WebAssembly verwenden

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

I/O in Systemsprachen

Ich beginne mit einem einfachen Beispiel in C. Angenommen, Sie möchten den Namen des Nutzers aus einer Datei vorlesen und mit der Nachricht "Hello, (username)!" Nachricht:

#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 zeigt zwar nicht viel, zeigt aber bereits etwas, das Sie in einer Anwendung finden werden. beliebiger Größe: Sie liest einige Eingaben aus der Außenwelt, verarbeitet sie intern und schreibt an die Außenwelt zurückgegeben. Solche Interaktionen mit der Außenwelt erfolgen über ein Funktionen, die häufig als Eingabe-/Ausgabefunktionen bezeichnet werden, auch als E/A abgekürzt.

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

Diese Funktionen sehen auf den ersten Blick ziemlich einfach aus und Sie müssen sich nicht einmal Maschinen zum Lesen oder Schreiben von Daten. Abhängig von der Umgebung können jedoch Im Inneren ist viel los:

  • Befindet sich die Eingabedatei auf einem lokalen Laufwerk, muss die Anwendung die Speicher- und Laufwerkzugriffe, um die Datei zu finden, die Berechtigungen zu prüfen, sie zum Lesen zu öffnen Lesen Block für Block, bis die angeforderte Anzahl von Bytes abgerufen ist. Das kann ziemlich langsam sein, abhängig von der Geschwindigkeit des Laufwerks und der angeforderten Größe.
  • Die Eingabedatei kann sich auch an einem bereitgestellten Netzwerkspeicherort befinden. werden, was die Komplexität, Latenz und Anzahl der potenziellen Kunden für jeden Vorgang wiederholen.
  • Schließlich gibt auch printf keine Garantie dafür aus, dass er Informationen an die Konsole ausgibt, und wird möglicherweise weitergeleitet. an eine Datei oder einen Netzwerkspeicherort gesendet. In diesem Fall müssen dieselben Schritte wie oben beschrieben ausgeführt werden.

Kurz gesagt: Die I/O kann langsam sein und Sie können nicht vorhersagen, wie lange ein bestimmter Anruf einen Blick auf den Code werfen. Während dieser Vorgang ausgeführt wird, wird die gesamte Anwendung als eingefroren angezeigt. und reagieren nicht auf den Nutzer.

Dies ist auch nicht auf C oder C++ beschränkt. In den meisten Systemsprachen wird die gesamte E/A in Form von synchrone APIs verwendet werden. Wenn Sie das Beispiel in Rust übersetzen, sieht die API vielleicht einfacher aus, gelten dieselben Prinzipien. Sie rufen einfach auf und warten synchron, bis das Ergebnis zurückgegeben wird. während sie alle teuren Vorgänge ausführt und schließlich das Ergebnis in einer einzigen Aufruf:

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 Web? Um ein konkretes Beispiel zu nennen, was könnte „file read“ in welche Operation übersetzt wird? Das würde Daten aus einem Speicher lesen müssen.

Asynchrones Modell des Webs

Das Web bietet eine Vielzahl unterschiedlicher Speicheroptionen, die Sie zuordnen können, wie 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 verwendet werden: der In-Memory-Speicher und der localStorage. synchron sein. Beide Optionen sind die einschränkendsten Optionen in Bezug darauf, was Sie wie lange speichern können. Alle Die anderen Optionen bieten nur asynchrone APIs.

Dies ist eine der wichtigsten Eigenschaften beim Ausführen von Code im Web: jeder zeitaufwendige Vorgang, der enthält eine E/A, muss asynchron sein.

Der Grund dafür ist, dass das Web in der Vergangenheit ein Single-Threaded ist und jeder Nutzercode, der die Benutzeroberfläche beeinflusst, muss im selben Thread wie die UI ausgeführt werden. Sie muss mit anderen wichtigen Aufgaben wie Layout, Rendering und Ereignisbehandlung für die CPU-Zeit. Ein JavaScript-Code-Snippet oder WebAssembly zum Starten eines Dateilesevorgangs alles andere blockieren: den gesamten Tab, oder in der Vergangenheit den gesamten Browser.

Stattdessen darf Code nur einen E/A-Vorgang zusammen mit einem auszuführenden Callback planen. sobald er abgeschlossen ist. Solche Callbacks werden als Teil der Ereignisschleife des Browsers ausgeführt. Ich werde nicht wir gehen hier ins Detail, aber wenn Sie interessiert sind, wie die Ereignisschleife im Hintergrund funktioniert, auschecken Aufgaben, Mikroaufgaben, Warteschlangen und Zeitpläne wo dieses Thema ausführlich erläutert wird.

Die kurze Version ist, dass der Browser alle Code-Elemente in einer Art Endlosschleife ausführt: und sie nacheinander aus der Warteschlange nehmen. Wenn ein Ereignis ausgelöst wird, stellt der Browser den entsprechenden Handler. Bei der nächsten Schleifeniteration wird er aus der Warteschlange genommen und ausgeführt. Dieser Mechanismus ermöglicht die Simulation von Nebenläufigkeit und die Ausführung vieler paralleler Vorgänge, wobei nur in einem einzigen Thread.

Wichtig bei diesem Mechanismus ist, dass Sie, obwohl Ihr benutzerdefiniertes JavaScript (oder WebAssembly-Code ausgeführt wird, die Ereignisschleife blockiert wird und obwohl es keine Möglichkeit gibt, auf externe Handler, Ereignisse, E/A usw. enthalten. Die einzige Möglichkeit, E/A-Ergebnisse zurückzuerhalten, besteht darin, eine den Code zurückrufen, die Codeausführung abschließen und die Steuerung an den Browser zurückgeben, ausstehende Aufgaben werden verarbeitet. Sobald die E/A abgeschlossen ist, wird Ihr Handler zu einer dieser Aufgaben und ausgeführt werden.

Wenn Sie beispielsweise die obigen Beispiele in modernem JavaScript umschreiben und sich dazu entschlossen haben, aus einer Remote-URL verwenden, würden Sie die Fetch API und die Syntax "async-await" verwenden:

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 jedes await im Grunde Callbacks verwenden:

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 mit dem ersten Callback eine Anfrage gestartet und Antworten abonniert. Sobald der Browser die erste Antwort erhält – nur die HTTP- -Headers an, wird dieser Callback asynchron aufgerufen. Der Callback liest den Text als Text vor. response.text() und abonniert das Ergebnis mit einem anderen Callback. Wenn fetch schließlich den gesamten Inhalt abgerufen hat, ruft er den letzten Callback auf, der "Hello, (username)!" zu den .

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

Als letztes Beispiel können sogar einfache APIs wie „sleep“ dazu führen, dass eine Anwendung eine bestimmte Sekunden, stellen auch eine Form eines E/A-Vorgangs dar:

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

Sicher, man könnte sie so einfach übersetzen, dass der aktuelle Thread blockiert wird. bis zum Ablauf der Zeit:

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

Genau das macht Emscripten in der Standardimplementierung „Ruhemodus“, Das ist jedoch sehr ineffizient, wodurch die gesamte Benutzeroberfläche blockiert wird und keine anderen Ereignisse verarbeitet werden können. währenddessen. Im Produktionscode sollten Sie dies in der Regel nicht tun.

Stattdessen gibt es eine idiomatischere Version von „sleep“. in JavaScript würde setTimeout() aufgerufen werden. Abonnieren mit einem Handler:

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

Was ist in diesen Beispielen und APIs gleich? In jedem Fall wird der idiomatische Code im Original Systemsprache eine blockierende API für die E/A verwendet, während ein entsprechendes Beispiel für das Web ein asynchrone API verwenden. Beim Kompilieren im Web müssen Sie irgendwie zwischen diesen beiden wechseln. Ausführungsmodelle. WebAssembly hat dazu noch keine integrierte Fähigkeit.

Die Lücke mit Asyncify schließen

Hier kommt Asyncify ins Spiel. Asyncify ist ein von Emscripten unterstützte Kompilierungszeitfunktion, die das Pausieren des gesamten Programms und sie später asynchron fortsetzen.

Eine Aufrufgrafik
die ein JavaScript beschreiben -> WebAssembly -> Web-API -> asynchroner Aufgabenaufruf, bei dem Asyncify eine Verbindung
das Ergebnis der asynchronen Aufgabe zurück in WebAssembly

Verwendung in C / C++ mit Emscripten

Wenn Sie im letzten Beispiel mithilfe von Asyncify einen asynchronen Ruhemodus implementieren möchten, könnten Sie Folgendes tun: so:

#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 , mit dem JavaScript-Snippets so definiert werden können, als wären es C-Funktionen. Verwenden Sie darin eine Funktion, Asyncify.handleSleep() Dadurch wird Emscripten angewiesen, das Programm anzuhalten, und es wird ein wakeUp()-Handler bereitgestellt, der aufgerufen, sobald der asynchrone Vorgang abgeschlossen ist. Im obigen Beispiel wird der Handler an setTimeout(), kann aber in jedem anderen Kontext verwendet werden, in dem Callbacks akzeptiert werden. Schließlich können Sie wie bei der regulären sleep() oder jeder anderen synchronen API, um async_sleep() überall aufzurufen.

Beim Kompilieren eines solchen Codes müssen Sie Emscripten anweisen, die Asyncify-Funktion zu aktivieren. Tun Sie das, indem Sie Übergeben von -s ASYNCIFY sowie -s ASYNCIFY_IMPORTS=[func1, func2] mit einem 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 jeden Aufruf dieser Funktionen das Speichern und Wiederherstellen der , sodass der Compiler unterstützenden Code um solche Aufrufe einfügt.

Wenn Sie nun diesen Code 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 Werte aus Asyncify funktioniert ebenfalls. Was? müssen Sie das Ergebnis von handleSleep() zurückgeben und das Ergebnis an den wakeUp() übergeben. Callback des Nutzers an. Wenn Sie z. B. nicht aus einer Datei lesen möchten, sondern eine Nummer von einem Remote-Gerät abrufen möchten, Ressource ist, können Sie ein Snippet wie das folgende verwenden, um eine Anfrage zu senden, den C-Code zu sperren und wird fortgesetzt, sobald der Antworttext abgerufen wurde – alles nahtlos, als ob der Aufruf synchron wäre.

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

Für Promise-basierte APIs wie fetch() können Sie sogar Asyncify mit JavaScript-Funktionen async-await verwenden, anstatt die Callback-basierte API zu verwenden. Anstelle von Asyncify.handleSleep(), Asyncify.handleAsync() anrufen. Anstatt einen Termin für ein wakeUp()-Callback verwenden, kannst du eine async-JavaScript-Funktion übergeben und await und return verwenden. sodass der Code noch natürlicher und synchroner wirkt, ohne dass die Vorteile der die asynchrone E/A.

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

Aber auch in diesem Beispiel sind nur Zahlen verfügbar. Wie gehen Sie vor, wenn Sie die ursprüngliche Wo habe ich beispielsweise versucht, den Namen eines Nutzers als String aus einer Datei abzurufen? Sie können das auch!

Emscripten bietet die Funktion Embind Konvertierungen zwischen JavaScript- und C++-Werten. Es unterstützt auch Asyncify, Du kannst await() über externe Promises aufrufen. Es verhält sich dann 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 nicht einmal ASYNCIFY_IMPORTS als Kompilierungs-Flag übergeben, da dies sind bereits standardmäßig enthalten.

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

Verwendung in anderen Sprachen

Angenommen, Sie haben in Ihrem Rust-Code einen ähnlichen synchronen Aufruf, den Sie einem Asynchrone API im Web verfügbar. Wie Sie sehen, können Sie das auch!

Zuerst müssen Sie eine solche Funktion als regulären Import über den extern-Block (oder den von Ihnen ausgewählten Syntax einer Sprache für fremde Funktionen).

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 dies für uns erledigen, aber es wird hier 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 Transformationen WebAssembly-Dateien, unabhängig davon, von welchem Compiler sie erstellt wurden. Die Transformation wird separat bereitgestellt. im Rahmen des wasm-opt-Optimierers von Binaryen Toolchain 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 geben Sie dann mit --pass-arg=… ein durch Kommas getrenntes Feld an. Liste der asynchronen Funktionen, bei denen der Programmstatus ausgesetzt und später wieder aufgenommen werden soll.

Jetzt müssen Sie nur noch unterstützenden Laufzeitcode bereitstellen, der diesen Zweck erfüllt: Sperren und fortsetzen WebAssembly-Code. Im C / C++ Fall würde dies ebenfalls von Emscripten verwendet werden, aber jetzt müssen Sie benutzerdefinierten JavaScript-Glue-Code, der beliebige WebAssembly-Dateien verarbeiten kann. Wir haben eine Bibliothek erstellt nur dafür.

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

Es simuliert eine standardmäßige WebAssembly-Instanziierung. API, aber unter einem eigenen Namespace. Die einzige Der Unterschied besteht darin, dass Sie unter einem regulären WebAssembly-API nur synchrone Funktionen als Imports erstellen, 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();

Sobald Sie versuchen, eine solche asynchrone Funktion – wie get_answer() im obigen Beispiel – aus einer auf der WebAssembly-Seite erkennt die Bibliothek das zurückgegebene Promise, sperrt und speichert den Status die WebAssembly-Anwendung abschließen, das Promise-Abo abschließen und später, sobald es behoben ist, Aufrufstack und Status nahtlos wiederherzustellen und die Ausführung so fortzusetzen, als wäre nichts passiert.

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

Wie funktioniert das alles im Hintergrund?

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

Dies ist der Funktion „async-await“ in JavaScript, das ich zuvor gezeigt habe, sehr ähnlich, aber im Gegensatz zur Funktion „async-await“ JavaScript 1 erfordert in der Sprache keine spezielle Syntax- oder Laufzeitunterstützung. durch Umwandlung einfacher synchroner Funktionen zur Kompilierungszeit.

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 wie den folgenden um (Pseudocode, realer Code ist die Transformation komplizierter:

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. Entsprechend wird der transformierte Code zum ersten Mal ausgeführt wird, wird nur der Teil vor async_sleep() ausgewertet. Sobald das Ereignis geplant ist, spart Asyncify alle lokalen Einwohner und lockert den Stack durch von jeder Funktion bis zum Anfang zurück, sodass der Browser Ereignisschleife.

Sobald async_sleep() aufgelöst ist, ändert sich der Asyncify-Supportcode mode in REWINDING und rufen Sie die Funktion erneut auf. Diesmal erfolgt die normale Ausführung Zweig wird übersprungen, da dies bereits geschehen ist. den Auftrag beim letzten Mal eingefordert hat und ich möchte vermeiden, dass 2-mal angezeigt. Stattdessen gelangen Sie direkt zur "Zurückspulen" Branch. Sobald dieser erreicht ist, werden alle gespeicherten lokalen Kontakte wiederhergestellt. Der Modus wechselt wieder zu "normal" und setzt die Ausführung so fort, als wäre der Code nie gestoppt worden.

Transformationskosten

Leider ist die Asyncify-Transformation nicht völlig kostenlos, da sie eine ganze Menge zum Speichern und Wiederherstellen dieser lokalen Codes, Navigieren Sie verschiedene Modi usw. Es wird versucht, nur Funktionen zu ändern, die im Befehl als asynchron gekennzeichnet sind. sowie allen potenziellen Aufrufern angezeigt, aber der Codegrößen-Overhead kann vor der Komprimierung immer noch etwa 50% betragen.

Ein Diagramm, das Code zeigt
von fast 0% unter abgestimmten Bedingungen bis über 100% im schlimmsten
Fälle

Dies ist nicht ideal, aber in vielen Fällen akzeptabel, wenn die Alternative nicht die entsprechende Funktionalität oder den ursprünglichen Code umschreiben müssen.

Aktivieren Sie immer die Optimierungen für die endgültigen Builds, um zu verhindern, dass die Leistung weiter gesteigert wird. Sie können Aktivieren Sie auch die Option Asyncify-spezifische Optimierung Optionen zur Reduzierung des Aufwands um Transformationen auf bestimmte Funktionen und/oder nur direkte Funktionsaufrufe beschränken. Es gibt auch eine geringe Kosten für die Laufzeitleistung verursacht, aber beschränkt sich auf die asynchronen Aufrufe. Im Vergleich zu auf die Kosten der tatsächlichen Arbeit ist er in der Regel vernachlässigbar.

Demos aus der Praxis

Nachdem Sie sich nun die einfachen Beispiele angesehen haben, wenden wir uns den komplexeren Szenarien zu.

Wie bereits am Anfang des Artikels erwähnt, ist eine der Speicheroptionen im Web ein asynchrone File System Access API verwenden. Sie erhalten Zugriff auf eine echtes Host-Dateisystem ü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 Systemsprachen und ermöglicht die Darstellung aller Arten von Dateisystemen und anderen Vorgängen in einem herkömmlichen synchron.

Was wäre, wenn Sie einander zuordnen könnten? Dann können Sie jede Anwendung in jeder Ausgangssprache kompilieren. mit jeder Toolchain, die das WASI-Ziel unterstützt, und es in einer Sandbox im Web ausführen, während damit es mit echten Nutzerdateien verarbeitet werden kann. Mit Asyncify ist genau das möglich.

In dieser Demo habe ich Rust coreutils-Kisten mit einem Einige kleinere Patches für WASI werden über die Asyncify-Transformation übertragen und asynchron implementiert. bindings aus WASI auf die File System Access API auf der JavaScript-Seite. Nach der Kombination mit Xterm.js-Terminalkomponente ist eine realistisch wirkende Shell für die Ausführung im Browser-Tab und arbeiten mit echten Nutzerdateien – genau wie bei einem echten Terminal.

Unter https://wasi.rreverser.com/ kannst du sie dir live ansehen.

Anwendungsfälle der Asynchronisierung sind nicht nur auf Timer und Dateisysteme beschränkt. Sie können weiter gehen und Nischen-APIs im Web zu nutzen.

Mit Asyncify ist es beispielsweise möglich, libusb, die wahrscheinlich beliebteste native Bibliothek für die Arbeit mit USB-Geräten – an eine WebUSB API, die asynchronen Zugriff auf solche Geräte ermöglicht im Web. Nach der Zuordnung und Kompilierung erhielt ich Standard-libusb-Tests und -Beispiele, die ich direkt in der Sandbox einer Webseite.

Screenshot von libusb
Debug-Ausgabe auf einer Webseite mit Informationen zur angeschlossenen Canon-Kamera

Wahrscheinlich geht es aber um die Story eines anderen Blogposts.

Diese Beispiele zeigen, wie leistungsfähig Asyncify sein kann, um Lücken zu schließen und alle Anwendungen auf das Web zu bringen, was Ihnen plattformübergreifenden Zugriff, Sandbox-Technologie und ohne Funktionsverlust.