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.
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
}
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.
Ein Suspender
befindet sich in einem der folgenden Zustände:
caller
] - Steuerelement befindet sich innerhalb von Suspender
, wobei caller
die Funktion ist, die in Suspender
aufgerufen hat und ein externref
erwartet. 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:
suspender
nicht inaktiv istsuspender
in Aktiv [ caller
] (wobei caller
der aktuelle Anrufer ist)result
das Ergebnis des Aufrufs von func(args)
(oder einer beliebigen Trap oder ausgelösten Ausnahme) sein.suspender
Aktiv [ caller'
] für einige caller'
(sollte garantiert sein, obwohl sich der Anrufer möglicherweise geändert hat)suspender
in Inaktivresult
zu caller'
Die Methode suspender.suspendOnReturnedPromise(func)
func
ein WebAssembly.Function
, dann behauptet, dass sein Funktionstyp die Form [t*] -> [externref]
und gibt ein WebAssembly.Function
mit dem Funktionstyp [t*] -> [externref]
;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:
result
das Ergebnis des Aufrufs von func(args)
(oder einer beliebigen Trap oder ausgelösten Ausnahme) sein.result
kein zurückgegebenes Versprechen ist, dann gibt (oder wiederholt) result
suspender
nicht aktiv ist [ caller
] für einige caller
frames
die Stack-Frames seit caller
frames
Frames mit nicht anhängigen Funktionen gibtsuspender
in Gesperrtresult.then(onFulfilled, onRejected)
mit den Funktionen onFulfilled
und onRejected
, die Folgendes tun:suspender
‚s Zustand ist vorübergehend gesperrt (garantiert werden soll)suspender
in Aktiv [ caller'
], wobei caller'
der Anrufer von onFulfilled
/ onRejected
onFulfilled
den angegebenen Wert in externref
und gibt ihn in frames
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
suspendOnReturnedPromise
,returnPromiseOnSuspend
,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.
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
.
caller
auf den (gesperrten) Stack des Aufrufers, und das Feld suspended
ist nullsuspended
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
suspender.caller
und suspended.suspended
null sind (andernfalls einfangen)stack
ein neu zugewiesener WebAssembly-Stack sein, der mit suspender
verknüpft iststack
und Speichern des ehemaligen Stack in suspender.caller
result
das Ergebnis von func(args)
(oder einer beliebigen Falle oder ausgelösten Ausnahme) sein.suspender.caller
wechseln und auf null setzenstack
result
suspender.suspendOnReturnedPromise(func)(args)
wird implementiert von
func(args)
, Abfangen einer Falle oder ausgelösten Ausnahmeresult
kein zurückgegebenes Versprechen ist, wird result
returning zurückgegeben (oder erneut geworfen).suspender.caller
nicht null ist (andernfalls einfangen)stack
der aktuelle Stackstack
kein WebAssembly-Stack ist, der mit suspender
verknüpft ist:stack
ein WebAssembly-Stack ist (andernfalls einfangen)stack
auf stack.suspender.caller
suspender.caller
wechseln, auf null setzen und den früheren Stack in suspender.suspended
storing speichernresult.then(onFulfilled, onRejected)
mit den Funktionen onFulfilled
und onRejected
, die implementiert werden durchsuspender.suspended
wechseln, auf null setzen und den früheren Stack in suspender.caller
storing speichernonFulfilled
den angegebenen Wert in externref
umwandeln und zurückgebenonRejected
wird der angegebene Wert erneut geworfenDie Implementierung der Funktion, die durch das Erstellen einer Host-Funktion für eine suspendierbare Funktion generiert wird
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).