Design: Vorschlag: Async/Await JS API

Erstellt am 26. Juli 2021  ·  16Kommentare  ·  Quelle: WebAssembly/design

Dieser Vorschlag wurde in Zusammenarbeit mit @fmccabe , @thibaudmichaud , @lukewagner und @kripken entwickelt , zusammen mit Feedback von der Stacks-Untergruppe (mit einer informellen Abstimmung, die das Vorrücken in Phase 0 heute genehmigt). Beachten Sie, dass aus Zeitgründen geplant ist, eine sehr schnelle Präsentation (dh 5 Minuten) abzuhalten und abzustimmen, um diese am 3. August in Phase 1 vorzurücken. Um dies zu erleichtern, ermutigen wir die Leute dringend, Bedenken hier im Voraus zu äußern, damit wir feststellen können, ob es größere Bedenken gibt, die es verdienen würden, die Präsentation und Abstimmung auf einen späteren Zeitpunkt mit mehr Zeit zu verschieben.

Der Zweck dieses Vorschlags besteht darin, eine relativ effiziente und relativ ergonomische Interop zwischen JavaScript-Versprechen und WebAssembly bereitzustellen, jedoch unter der Einschränkung zu arbeiten, dass die einzigen Änderungen an der JS-API und nicht am Core-Wasm vorgenommen werden.
Es ist zu erwarten, dass der Stack-Switching-Vorschlag schließlich den Kern von WebAssembly um die Funktionalität erweitert, um die in diesem Vorschlag bereitgestellten Operationen zusammen mit vielen anderen wertvollen Stack-Switching-Vorgängen direkt in WebAssembly zu implementieren, aber dieser spezielle Anwendungsfall für Stack-Switching hatte ausreichende Dringlichkeit, um einen schnelleren Weg nur über die JS-API zu verdienen.
Weitere Informationen finden Sie in den Notizen und Folien zum Stack-Untergruppentreffen am 28. Juni 2021 , in denen die von uns berücksichtigten Nutzungsszenarien und Faktoren detailliert beschrieben und die Gründe für unsere Herleitung des folgenden Designs zusammengefasst werden.

UPDATE: Nach dem Feedback, das die Stacks-Untergruppe von TC39 erhalten hatte, erlaubt dieser Vorschlag nur das Aussetzen von WebAssembly-Stacks – er nimmt keine Änderungen an der JavaScript-Sprache vor und ermöglicht insbesondere nicht indirekt die Unterstützung für getrennte asycn / await in JavaScript.

Dies hängt (lose) vom js-types- Vorschlag ab, der WebAssembly.Function als Unterklasse von Function einführt.

Schnittstelle

Der Vorschlag besteht darin, die folgende Schnittstelle, den Konstruktor und die folgenden Methoden zur JS-API hinzuzufügen, mit weiteren Details zu ihrer Semantik weiter unten.

interface Suspender {
   constructor();
   Function suspendOnReturnedPromise(Function func); // import wrapper
   // overloaded: WebAssembly.Function suspendOnReturnedPromise(WebAssembly.Function func);
   WebAssembly.Function returnPromiseOnSuspend(WebAssembly.Function func); // export wrapper
}

Beispiel

Das Folgende ist ein Beispiel dafür, wie wir erwarten, dass jemand diese API verwendet.
In unseren Nutzungsszenarien fanden wir es sinnvoll, WebAssembly-Module so zu betrachten, dass sie konzeptionell "synchrone" und "asynchrone" Importe und Exporte haben.
Die aktuelle JS-API unterstützt nur "synchrone" Importe und Exporte.
Die Methoden der Suspender-Schnittstelle werden verwendet, um relevante Importe und Exporte zu umschließen, um sie "asynchron" zu machen, wobei das Suspender-Objekt selbst diese Importe und Exporte explizit miteinander verbindet, um sowohl die Implementierung als auch die Zusammensetzbarkeit zu erleichtern.

WebAssembly ( demo.wasm ):

(module
    (import "js" "init_state" (func $init_state (result f64)))
    (import "js" "compute_delta" (func $compute_delta (result f64)))
    (global $state f64)
    (func $init (global.set $state (call $init_state)))
    (start $init)
    (func $get_state (export "get_state") (result f64) (global.get $state))
    (func $update_state (export "update_state") (result f64)
      (global.set (f64.add (global.get $state) (call $compute_delta)))
      (global.get $state)
    )
)

Text ( data.txt ):

19827.987

JavaScript:

var suspender = new Suspender();
var init_state = () => 2.71;
var compute_delta = () => fetch('data.txt').then(res => res.text()).then(txt => parseFloat(txt));
var importObj = {js: {
    init_state: init_state,
    compute_delta: suspender.suspendOnReturnedPromise(compute_delta)
}};

fetch('demo.wasm').then(response =>
    response.arrayBuffer()
).then(buffer =>
    WebAssembly.instantiate(buffer, importObj)
).then(({module, instance}) => {
    var get_state = instance.exports.get_state;
    var update_state = suspender.returnPromiseOnSuspend(instance.exports.update_state);
    ...
});

In diesem Beispiel haben wir ein WebAssembly-Modul, das eine sehr vereinfachte Zustandsmaschine ist – jedes Mal, wenn Sie den Zustand aktualisieren, ruft es einfach einen Import auf, um ein Delta zu berechnen, das dem Zustand hinzugefügt wird.
Auf JavaScript-Seite stellt sich jedoch heraus, dass die Funktion, die wir zum Berechnen des Deltas verwenden möchten, asynchron ausgeführt werden muss; das heißt, es gibt eher ein Versprechen einer Zahl als eine Zahl selbst zurück.

Wir können diese Synchronisationslücke schließen, indem wir die neue JS-API verwenden.
Im Beispiel wird ein Import des WebAssembly-Moduls mit suspender.suspendOnReturnedPromise und ein Export mit suspender.returnPromiseOnSuspend , beide verwenden dasselbe suspender .
Das suspender verbindet die beiden miteinander.
Es macht es so, dass, wenn jemals der (unverpackte) Import ein Versprechen zurückgibt, der (verpackte) Export ein Versprechen zurückgibt, wobei die gesamte Berechnung dazwischen "ausgesetzt" wird, bis das Versprechen des Imports aufgelöst wird.
Das Umschließen des Exports fügt im Wesentlichen einen async Marker hinzu, und das Umschließen des Imports fügt im Wesentlichen einen await Marker hinzu, aber im Gegensatz zu JavaScript müssen wir async nicht explizit einfädeln. await durch alle zwischengeschalteten WebAssembly-Funktionen!

In der Zwischenzeit kehrt der Aufruf von init_state während der Initialisierung notwendigerweise ohne Unterbrechung zurück, und Aufrufe von Export get_state kehren ebenfalls immer ohne Unterbrechung zurück, sodass der Vorschlag weiterhin die bestehenden "synchronen" Importe und Exporte unterstützt das WebAssembly-Ökosystem heute verwendet.
Natürlich werden viele Details überflogen, wie zum Beispiel die Tatsache, dass, wenn ein synchroner Export einen asynchronen Import aufruft, das Programm abfängt, wenn der Import versucht, anzuhalten.
Im Folgenden finden Sie eine detailliertere Spezifikation sowie einige Implementierungsstrategien.

Spezifikation

Ein Suspender befindet sich in einem der folgenden Zustände:

  • Inaktiv - wird derzeit nicht verwendet
  • Aktiv [ caller ] - Steuerelement befindet sich innerhalb von Suspender , wobei caller die Funktion ist, die in Suspender aufgerufen hat und ein externref erwartet.
  • Gesperrt - warte derzeit auf ein Versprechen zur Lösung

Die Methode suspender.returnPromiseOnSuspend(func) behauptet, dass func ein WebAssembly.Function mit einem Funktionstyp der Form [ti*] -> [to] und gibt dann ein WebAssembly.Function mit einem Funktionstyp zurück [ti*] -> [externref] das folgendes macht, wenn es mit den Argumenten args aufgerufen wird:

  1. Traps, wenn der Status von suspender nicht inaktiv ist
  2. Ändert den Status von suspender in Aktiv [ caller ] (wobei caller der aktuelle Anrufer ist)
  3. Lassen Sie result das Ergebnis des Aufrufs von func(args) (oder einer beliebigen Trap oder ausgelösten Ausnahme) sein.
  4. Behauptet, dass der Status von suspender Aktiv [ caller' ] für einige caller' (sollte garantiert sein, obwohl sich der Anrufer möglicherweise geändert hat)
  5. Ändert den Status von suspender in Inaktiv
  6. Gibt (oder wiederholt) result zu caller'

Die Methode suspender.suspendOnReturnedPromise(func)

  • wenn func ein WebAssembly.Function , dann behauptet, dass sein Funktionstyp die Form [t*] -> [externref] und gibt ein WebAssembly.Function mit dem Funktionstyp [t*] -> [externref] ;
  • behauptet andernfalls, dass func Function und gibt Function .

In beiden Fällen führt die von suspender.suspendOnReturnedPromise(func) Funktion Folgendes aus, wenn sie mit den Argumenten args aufgerufen wird:

  1. Lassen Sie result das Ergebnis des Aufrufs von func(args) (oder einer beliebigen Trap oder ausgelösten Ausnahme) sein.
  2. Wenn result kein zurückgegebenes Versprechen ist, dann gibt (oder wiederholt) result
  3. Traps, wenn der Status von suspender nicht aktiv ist [ caller ] für einige caller
  4. Lassen Sie frames die Stack-Frames seit caller
  5. Traps, wenn es in frames Frames mit nicht anhängigen Funktionen gibt
  6. Ändert den Status von suspender in Gesperrt
  7. Gibt das Ergebnis von result.then(onFulfilled, onRejected) mit den Funktionen onFulfilled und onRejected , die Folgendes tun:

    1. Behauptet , dass suspender ‚s Zustand ist vorübergehend gesperrt (garantiert werden soll)

    2. Ändert den Status von suspender in Aktiv [ caller' ], wobei caller' der Anrufer von onFulfilled / onRejected



      • Konvertiert im Fall von onFulfilled den angegebenen Wert in externref und gibt ihn in frames


      • Im Fall von onRejected wird der angegebene Wert bis zu frames als Ausnahme gemäß der JS-API des Exception Handling- Vorschlags



Eine Funktion ist suspendierbar, wenn sie es war

  • durch ein WebAssembly-Modul definiert,
  • zurückgegeben von suspendOnReturnedPromise ,
  • zurückgegeben von returnPromiseOnSuspend ,
  • oder generiert durch Erstellen einer Hostfunktion für eine aussetzbare Funktion

Wichtig ist, dass in JavaScript geschriebene Funktionen nicht aussetzbar sind, was dem Feedback von Mitgliedern von TC39 entspricht , und Host-Funktionen (mit Ausnahme der wenigen oben aufgeführten) sind nicht aussetzbar, entsprechend dem Feedback von Engine-Maintainern.

Implementierung

Das Folgende ist eine Umsetzungsstrategie für diesen Vorschlag.
Es übernimmt die Engine-Unterstützung für das Stack-Switching, wo natürlich die Hauptherausforderungen bei der Implementierung liegen.

Es gibt zwei Arten von Stacks: einen Host- (und JavaScript-)Stack und einen WebAssembly-Stack. Jeder WebAssembly-Stack hat ein Suspender-Feld namens suspender . Jeder Thread hat einen Host-Stack.

Jedes Suspender hat zwei Stack-Referenzfelder: eines namens caller und eines namens suspended .

  • Im Zustand Inaktiv sind beide Felder null.
  • Im Status Aktiv verweist das Feld caller auf den (gesperrten) Stack des Aufrufers, und das Feld suspended ist null
  • Im Suspended- Zustand verweist das suspended Feld auf den (suspendierten) WebAssembly-Stack, der derzeit dem Suspendierer zugeordnet ist, und das caller Feld ist null.

suspender.returnPromiseOnSuspend(func)(args) wird implementiert von

  1. Überprüfen, ob suspender.caller und suspended.suspended null sind (andernfalls einfangen)
  2. Lassen Sie stack ein neu zugewiesener WebAssembly-Stack sein, der mit suspender verknüpft ist
  3. Wechseln zu stack und Speichern des ehemaligen Stack in suspender.caller
  4. Lassen Sie result das Ergebnis von func(args) (oder einer beliebigen Falle oder ausgelösten Ausnahme) sein.
  5. Zu suspender.caller wechseln und auf null setzen
  6. Befreiung von stack
  7. Zurückgeben (oder erneut werfen) result

suspender.suspendOnReturnedPromise(func)(args) wird implementiert von

  1. Aufrufen von func(args) , Abfangen einer Falle oder ausgelösten Ausnahme
  2. Wenn result kein zurückgegebenes Versprechen ist, wird result returning zurückgegeben (oder erneut geworfen).
  3. Überprüfen, ob suspender.caller nicht null ist (andernfalls einfangen)
  4. Sei stack der aktuelle Stack
  5. Während stack kein WebAssembly-Stack ist, der mit suspender verknüpft ist:

    • Überprüfen, ob stack ein WebAssembly-Stack ist (andernfalls einfangen)

    • Aktualisieren von stack auf stack.suspender.caller

  6. Zu suspender.caller wechseln, auf null setzen und den früheren Stack in suspender.suspended storing speichern
  7. Rückgabe des Ergebnisses von result.then(onFulfilled, onRejected) mit den Funktionen onFulfilled und onRejected , die implementiert werden durch

    1. Zu suspender.suspended wechseln, auf null setzen und den früheren Stack in suspender.caller storing speichern



      • Im Fall von onFulfilled den angegebenen Wert in externref umwandeln und zurückgeben


      • Im Fall von onRejected wird der angegebene Wert erneut geworfen



Die Implementierung der Funktion, die durch das Erstellen einer Host-Funktion für eine suspendierbare Funktion generiert wird

Alle 16 Kommentare

Ist es möglich, eine API bereitzustellen, die eine asynchrone Funktion/einen asynchronen Generator (sync oder async) empfängt, und sie dann in eine aussetzbare Funktion umzuwandeln?

Können Sie, vielleicht mit einem Pseudocode oder einem Anwendungsfall, verdeutlichen, was Sie meinen? Ich möchte sicherstellen, dass ich Ihnen eine genaue Antwort gebe.

Ist Suspender beabsichtigt, ein Teil von JS zu sein oder ist es eine separate API? Ist es ausschließlich für wasm ( WebAssembly.Suspender )? Meiner Ansicht nach sollte dieser Vorschlag im TC39 erörtert werden.

Es ist ausdrücklich NICHT dafür gedacht, JS-Programme zu beeinflussen. Genauer gesagt führt der Versuch, eine JS-Funktion auszusetzen, zu einer Falle. Wir haben uns einige Mühe gegeben, dies sicherzustellen.
Ich kann es jedoch mit Shu-yu ansprechen, um seine Meinung zu erfahren.

Entschuldigung, @chicoxyzzy , ich sehe, dass ich vergessen habe, einige Kontexte/Updates aus der Stacks-Untergruppe in der Lage sein sollten, JavaScript-/Host-Frames in ausgesetzten Stacks zu erfassen. Wir haben jedoch Feedback von Leuten in TC39 erhalten, dass Bedenken bestanden, dass dies das JS-Ökosystem zu drastisch beeinträchtigen würde, und wir erhielten Feedback von Host-Implementierern, dass es Bedenken gab, dass nicht alle Host-Frames eine Aussetzung tolerieren könnten. Daher hat die Stack-Untergruppe seitdem sichergestellt, dass Designs nur WebAssembly(-bezogene) Frames in suspendierten Stacks erfassen, und dieser Vorschlag erfüllt diese Eigenschaft. Ich habe das OP aktualisiert, um diesen wichtigen Hinweis aufzunehmen.

Es ist toll, hier Fortschritte zu sehen. Gibt es Beispiele dafür, wie dies in der ESM-Integration für Wasm verwendet werden würde?

Die schlechte Nachricht ist, dass Sie nicht einfach ein ESM-Wasm-Modul importieren und diese Stack-Switching-Unterstützung für Versprechen erhalten können, da dies alles in der JS-API enthalten ist. Die gute Nachricht ist, dass Sie mit dieser API immer noch ESM-Module verwenden können, nur mit einigen JS-ESM-Modulen als Kleber.

Insbesondere richten Sie drei ESM-Module ein: foo-exports.js , foo-wasm.wasm und foo-imports.js . Das foo-imports.js Modul erstellt den Suspendierer, verwendet ihn, um alle "asynchronen" Promise-produzierenden Importe zu umschließen, die von foo-wasm.wasm , und exportiert den Suspendierer und diese Importe. foo-wasm.wasm importiert dann alle "asynchronen" Importe von foo-imports.js und alle "synchronen" Importe direkt aus ihren jeweiligen Modulen (oder Sie können sie natürlich auch über foo-imports.js , die sie ohne Umbruch exportieren könnte). Schließlich importiert foo-exports.js den Hosenträger aus foo-imports.js , importiert die Exporte von foo-wasm.wasm , verpackt die "asynchronen" Exporte mit dem Hosenträger und exportiert dann den (unverpackten) "synchronen" Exporte und die umschlossenen "asynchronen" Exporte. Clients importieren dann aus foo-exports.js und berühren niemals direkt (oder benötigen Kenntnisse über) foo-wasm.wasm oder foo-imports.js .

Es ist eine bedauerliche Hürde, aber das Beste, was wir erreichen konnten, da wir den Kern-Wasm nicht modifizieren mussten. Unser Ziel ist es jedoch sicherzustellen, dass dieses Design vorwärtskompatibel mit dem Vorschlag zur Erweiterung von Core-Wasm ist, sodass Sie diese drei Module bei Auslieferung des Vorschlags gegen das eine erweiterte Wasm-Modul austauschen können und niemand dies semantisch kann den Unterschied erkennen (Modulo-Datei-Umbenennung).

War das verständlich und denkst du, dass es deinen Bedürfnissen (wenn auch umständlich) entsprechen würde?

Ich verstehe die Notwendigkeit einer Umhüllung, zumindest während WebAssembly.Module-Typ Wasm-Importe noch nicht möglich sind (und hoffentlich zu gegebener Zeit).

Genauer gesagt habe ich mich gefragt, ob es in der ESM-Integration überhaupt Spielraum für die Dekoration dieser Muster gibt, damit beide Seiten des Hosenträgerklebers besser verwaltet werden können. Wenn es beispielsweise einige Metadaten gab, die die exportierten und importierten Funktionen im Binärformat verknüpften, könnte die ESM-Integration dies abfragen und die doppelten Import-/Export-Hüllenfunktionen intern als Teil der Integrationsschicht basierend auf bestimmten vorhersehbaren Regeln abgleichen.

Ah. Derzeit gibt es keinen solchen Plan. Das Feedback, das ich erhalten hatte, war, dass man auch die ESM-Integration nicht ändern wollte. Kurz gesagt, die Hoffnung ist, dass all dies irgendwann im Kernwasm möglich sein wird, und deshalb möchten wir, dass dieser Vorschlag einen möglichst kleinen Fußabdruck hinterlässt.

Das Feedback, das ich erhalten hatte, war, dass ich auch die ESM-Integration nicht ändern wollte

Können Sie erläutern, woher dieses Feedback kommt? Es gibt viel Spielraum, die ESM-Integration um eine Integrationssemantik auf höherer Ebene zu erweitern, ein Bereich, der meiner Meinung nach noch nicht vollständig erforscht wurde, weshalb ich ihn anspreche. Ich habe in der Vergangenheit noch nie von Widerständen gehört, diesen Bereich zu verbessern. Dies als einen Bereich für Sugaring zu sehen, kann für JS-Entwickler von Vorteil sein, da sie direkte Promise-Importe / -Exporte ermöglichen.

Es ist erwähnenswert, dass dieser Vorschlag es einem einzelnen JS-Modul in einem Zyklus verhindert, sowohl Importeur als auch Importeur eines Wasm-Moduls zu sein, das dank des JS-Zyklusfunktions-Hoisting in der ESM-Integration derzeit noch für Funktionsimporte funktionieren kann , aber würde dieses Hochziehen des Zyklus mit einem Suspender-Ausdruckswrapper um die importierte Funktion nicht unterstützen.

Diesen Eindruck habe ich von @lukewagner. Ich stimme zu, dass es Spielraum gibt, die ESM-Integration zu erweitern, aber mein Verständnis ist, dass dies Änderungen/Erweiterungen an der wasm-Datei erfordert – die wir (als Teil des Small-Footprint-Ziels) zu vermeiden versuchten –, also wollten wir solche Änderungen nicht/ Erweiterungen in diesen Vorschlag aufzunehmen. Wenn dem ESM-Vorschlag solche Änderungen/Erweiterungen hinzugefügt würden, würden diese diesen Vorschlag natürlich ideal ergänzen, sodass die JS-Wrapper-Module nicht benötigt werden, um die Funktionalität dieses Vorschlags zu erhalten.

Ich habe den Kommentar von @ Jack-Works falsch gelesen und meinen Kommentar oben angepasst.

Danke @RossTate für die Klarstellungen, ja, ich abzugleichen , um Host-Integrationen zu informieren, aber dies in keiner Weise im MVP zu erwarten. Ich nutze auch nur die Gelegenheit, um darauf hinzuweisen, dass die ESM-Integration ein Bereich ist, der von Zucker im Allgemeinen unabhängig von der Basis-JS-API profitieren könnte.

Um es klar zu sagen, die Herausforderung, auf die ich hingewiesen habe, war, dass alle Optionen, die wir zu WebAssembly.instantiate() (oder neuen Versionen von WebAssembly.instantiate() mit neuen Parametern) hinzugefügt haben, auch irgendwie angezeigt werden müssen, wenn wasm über ESM geladen wird -Integration, nicht dass die ESM-Integration unveränderlich war.

Ah, cool, wir haben also mehr Flexibilität in Bezug auf ESM, als ich dachte, falls es nötig sein sollte. Danke für die Korrektur meines Missverständnisses.

Es hört sich so an, als würden wir über eine Art benutzerdefinierten Abschnitt sprechen, um anzugeben, wie bestimmte exportierte Wasm-Funktionen in JS als Promise-basierte APIs angezeigt werden sollen, und vielleicht umgekehrt, wie Importe aus Wasm von JS-Promise-basierten APIs in eine Art konvertiert werden können des Stapelwechsels. Verstehe ich richtig?

Ich mag diese Idee. Ich vermute, dass wir einen analogen benutzerdefinierten Abschnitt für die Wasm GC/JS-ESM-Integration (oder einen Teil desselben) benötigen werden. Ich bin mir nicht sicher, inwieweit dieser benutzerdefinierte Abschnitt sprachübergreifend sein könnte, aber in beiden Fällen ist er wahrscheinlich etwas weniger universell als Schnittstellentypen und wird auch tendenziell innerhalb einer Komponente verwendet, nicht nur zwischen ihnen.

Möchte jemand eine Art Inhaltsverzeichnis oder README schreiben, das ein grundlegendes Design für diesen benutzerdefinierten Abschnitt beschreibt?

Das klingt nach einer möglichen Option. Wie Sie bereits erwähnt haben, wurden ähnliche Optionen im GC-Vorschlag diskutiert, beispielsweise in WebAssembly/gc#203. Die JS-Integration soll voraussichtlich morgen in der GC-Untergruppe diskutiert werden, daher könnte es gut sein, die mögliche Verbindung zu diesem Vorschlag während dieser Diskussion im Auge zu behalten (oder sie könnte sich je nach Verlauf der Diskussion als nicht zusammenhängend erweisen).

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

konsoletyper picture konsoletyper  ·  6Kommentare

spidoche picture spidoche  ·  4Kommentare

nikhedonia picture nikhedonia  ·  7Kommentare

void4 picture void4  ·  5Kommentare

beriberikix picture beriberikix  ·  7Kommentare