Cette proposition a été développée en collaboration avec @fmccabe , @thibaudmichaud , @lukewagner et @kripken , avec les commentaires du sous-groupe Stacks (avec un vote informel approuvant son passage à la phase 0 aujourd'hui). Veuillez noter qu'en raison des contraintes de temps, le plan est d'avoir une présentation très rapide (c'est-à-dire 5 minutes) et un vote pour faire avancer cela à la phase 1 le 3 août. Pour faciliter cela, nous encourageons fortement les gens à faire part de leurs préoccupations ici à l'avance afin que nous puissions déterminer s'il existe des préoccupations majeures qui mériteraient de reporter la présentation + le vote à une date ultérieure avec plus de temps.
Le but de cette proposition est de fournir une interopérabilité relativement efficace et relativement ergonomique entre les promesses JavaScript et WebAssembly, mais en travaillant sous la contrainte que les seuls changements concernent l'API JS et non le core wasm.
On s'attend à ce que la proposition de commutation de pile étende à terme WebAssembly avec la fonctionnalité permettant d'implémenter les opérations que nous fournissons dans cette proposition directement dans WebAssembly, ainsi que de nombreuses autres opérations de commutation de pile précieuses, mais que ce cas d'utilisation particulier pour la commutation de pile avait une urgence suffisante pour mériter un chemin plus rapide via uniquement l'API JS.
Pour plus d'informations, veuillez vous référer aux notes et diapositives de la réunion du sous-groupe Stack du 28 juin 2021 , qui détaille les scénarios d'utilisation et les facteurs que nous avons pris en considération et résume la justification de la façon dont nous sommes arrivés à la conception suivante.
MISE À JOUR : Suite aux commentaires que le sous-groupe sur les piles a reçus du TC39, cette proposition n'autorise que la suspension des piles WebAssembly. Elle n'apporte aucune modification au langage JavaScript et, en particulier, n'active pas indirectement la prise en charge des asycn
détachés await
en JavaScript.
Cela dépend (largement) de la proposition js-types , qui introduit WebAssembly.Function
comme sous-classe de Function
.
La proposition consiste à ajouter l'interface, le constructeur et les méthodes suivants à l'API JS, avec plus de détails sur leur sémantique ci-dessous.
interface Suspender {
constructor();
Function suspendOnReturnedPromise(Function func); // import wrapper
// overloaded: WebAssembly.Function suspendOnReturnedPromise(WebAssembly.Function func);
WebAssembly.Function returnPromiseOnSuspend(WebAssembly.Function func); // export wrapper
}
Ce qui suit est un exemple de la façon dont nous nous attendons à ce que l'on utilise cette API.
Dans nos scénarios d'utilisation, nous avons trouvé utile de considérer les modules WebAssembly comme ayant des importations et des exportations « synchrones » et « asynchrones ».
L'API JS actuelle ne prend en charge que les importations et les exportations « synchrones ».
Les méthodes de l'interface Suspender sont utilisées pour envelopper les importations et les exportations pertinentes afin de rendre "asynchrone", l'objet Suspender lui-même connectant explicitement ces importations et exportations pour faciliter à la fois la mise en œuvre et la composabilité.
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)
)
)
Texte ( 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);
...
});
Dans cet exemple, nous avons un module WebAssembly qui est une machine d'état très simpliste : chaque fois que vous mettez à jour l'état, il appelle simplement une importation pour calculer un delta à ajouter à l'état.
Du côté de JavaScript, cependant, la fonction que nous voulons utiliser pour calculer le delta s'avère devoir être exécutée de manière asynchrone ; c'est-à-dire qu'il renvoie une promesse d'un nombre plutôt qu'un nombre lui-même.
Nous pouvons combler cet écart de synchronisation en utilisant la nouvelle API JS.
Dans l'exemple, une importation du module WebAssembly est encapsulée à l'aide de suspender.suspendOnReturnedPromise
, et une exportation est encapsulée à l'aide de suspender.returnPromiseOnSuspend
, les deux utilisant le même suspender
.
Ce suspender
connecte aux deux ensemble.
Cela fait en sorte que, si jamais l'importation (déballée) renvoie une promesse, l'exportation (emballée) renvoie une promesse, tous les calculs étant "suspendus" jusqu'à ce que la promesse de l'importation soit résolue.
L'emballage de l'exportation ajoute essentiellement un marqueur async
, et l'emballage de l'importation ajoute essentiellement un marqueur await
, mais contrairement à JavaScript, nous n'avons pas à enfiler explicitement async
/ await
tout au long de toutes les fonctions WebAssembly intermédiaires !
Pendant ce temps, l'appel effectué au init_state
lors de l'initialisation revient nécessairement sans suspension, et les appels à l'exportation get_state
reviennent toujours sans suspension, de sorte que la proposition prend toujours en charge les importations et exportations "synchrones" existantes l'écosystème WebAssembly utilise aujourd'hui.
Bien sûr, de nombreux détails sont survolés, tels que le fait que si une exportation synchrone appelle une importation asynchrone, le programme sera intercepté si l'importation tente de se suspendre.
Ce qui suit fournit une spécification plus détaillée ainsi qu'une stratégie de mise en œuvre.
Un Suspender
est dans l'un des états suivants :
caller
] - le contrôle est à l'intérieur du Suspender
, avec caller
étant la fonction qui a appelé le Suspender
et attend un externref
à retournerLa méthode suspender.returnPromiseOnSuspend(func)
affirme que func
est un WebAssembly.Function
avec un type de fonction de la forme [ti*] -> [to]
puis retourne un WebAssembly.Function
avec un type de fonction [ti*] -> [externref]
qui fait ce qui suit lorsqu'il est appelé avec des arguments args
:
suspender
n'est pas Inactifsuspender
en Actif [ caller
] (où caller
est l'appelant actuel)result
le résultat de l'appel de func(args)
(ou de tout piège ou exception levée)suspender
est Actif [ caller'
] pour certains caller'
(devrait être garanti, même si l'appelant peut avoir changé)suspender
en Inactifresult
à caller'
La méthode suspender.suspendOnReturnedPromise(func)
func
est un WebAssembly.Function
, alors affirme que son type de fonction est de la forme [t*] -> [externref]
et renvoie un WebAssembly.Function
avec le type de fonction [t*] -> [externref]
;func
est un Function
et renvoie un Function
.Dans les deux cas, la fonction renvoyée par suspender.suspendOnReturnedPromise(func)
fait ce qui suit lorsqu'elle est appelée avec les arguments args
:
result
le résultat de l'appel de func(args)
(ou de tout piège ou exception levée)result
n'est pas une Promesse retournée, alors retourne (ou relance) result
suspender
n'est pas Actif [ caller
] pour certains caller
frames
les cadres de pile depuis caller
frames
suspender
en Suspenduresult.then(onFulfilled, onRejected)
avec les fonctions onFulfilled
et onRejected
qui effectuent les opérations suivantes :suspender
est Suspendu (doit être garanti)suspender
en Actif [ caller'
], où caller'
est l'appelant de onFulfilled
/ onRejected
onFulfilled
, convertit la valeur donnée en externref
et la renvoie à frames
onRejected
, lève la valeur donnée jusqu'à frames
en tant qu'exception selon l'API JS de la proposition de gestion des exceptionsUne fonction est suspendue si elle était
suspendOnReturnedPromise
,returnPromiseOnSuspend
,Il est important de noter que les fonctions écrites en JavaScript ne sont TC39 , et les fonctions hôtes (à l'exception des quelques-unes énumérées ci-dessus) ne sont
Ce qui suit est une stratégie de mise en œuvre de cette proposition.
Il suppose la prise en charge du moteur pour la commutation de pile, ce qui bien sûr est l'endroit où se trouvent les principaux défis de mise en œuvre.
Il existe deux types de piles : une pile hôte (et JavaScript) et une pile WebAssembly. Chaque pile WebAssembly a un champ de suspension appelé suspender
. Chaque thread a une pile hôte.
Chaque Suspender
a deux champs de référence de pile : un appelé caller
et un autre appelé suspended
.
caller
référence à la pile (suspendue) de l'appelant, et le champ suspended
est nulsuspended
référence à la pile WebAssembly (suspendue) actuellement associée à la suspension, et le champ caller
est nul.suspender.returnPromiseOnSuspend(func)(args)
est implémenté par
suspender.caller
et suspended.suspended
sont nuls (piégeage sinon)stack
être une pile WebAssembly nouvellement allouée associée à suspender
stack
et stocker l'ancienne pile dans suspender.caller
result
être le résultat de func(args)
(ou de tout piège ou exception levée)suspender.caller
et le définir sur nullstack
result
suspender.suspendOnReturnedPromise(func)(args)
est implémenté par
func(args)
, intercepter tout piège ou exception levéeresult
n'est pas une Promesse retournée, retour (ou relance) result
suspender.caller
n'est pas nul (piégeage sinon)stack
la pile actuellestack
n'est pas une pile WebAssembly associée à suspender
:stack
est une pile WebAssembly (piégeage sinon)stack
jour de stack.suspender.caller
suspender.caller
, le définir sur null et stocker l'ancienne pile dans suspender.suspended
result.then(onFulfilled, onRejected)
avec les fonctions onFulfilled
et onRejected
qui sont implémentées parsuspender.suspended
, le définir sur null et stocker l'ancienne pile dans suspender.caller
onFulfilled
, convertir la valeur donnée en externref
et la renvoyeronRejected
, relancer la valeur donnéeL'implémentation de la fonction générée par la création d'une fonction hôte pour une fonction pouvant être suspendue est modifiée pour basculer d'abord vers la pile hôte du thread actuel (s'il n'y est pas déjà) et pour enfin revenir à l'ancienne pile.
Est-il possible d'exposer une API qui reçoit une fonction/générateur asynchrone (sync ou async) puis de la transformer en fonction suspendable ?
Pouvez-vous clarifier, peut-être avec un pseudo-code ou un cas d'utilisation, ce que vous voulez dire ? Je veux m'assurer de vous donner une réponse précise.
L'intention est-elle que Suspender
fasse partie de JS ou qu'il s'agisse d'une API distincte ? Est-ce exclusivement pour wasm ( WebAssembly.Suspender
) ? Il me semble que cette proposition devrait être discutée au TC39.
Il n'est spécifiquement PAS destiné à affecter les programmes JS. Plus précisément, essayer de suspendre une fonction JS entraînera un piège. Nous nous sommes donné beaucoup de mal pour nous en assurer.
Cependant, je peux le soulever avec Shu-yu pour avoir son avis.
Désolé, @chicoxyzzy , je vois que j'ai oublié d'inclure du contexte/des mises à jour du sous-groupe Stacks. Les anciennes propositions de commutation de pile ont été écrites dans l'espoir que vous devriez pouvoir capturer des cadres JavaScript/hôte dans des piles suspendues. Cependant, nous avons reçu des commentaires de personnes du TC39 selon lesquels cela pourrait affecter de manière trop drastique l'écosystème JS, et nous avons reçu des commentaires des implémenteurs d'hôtes selon lesquels tous les cadres hôtes ne seraient pas en mesure de tolérer une suspension. Ainsi, le sous-groupe Stacks s'est depuis assuré que les conceptions ne capturent que les cadres WebAssembly (liés) dans les piles suspendues, et cette proposition satisfait cette propriété. J'ai mis à jour le PO pour inclure cette note importante.
C'est super de voir des progrès ici. Existe-t-il des exemples de la façon dont cela serait utilisé dans l'intégration ESM pour Wasm ?
La mauvaise nouvelle est que, comme tout cela se trouve dans l'API JS, vous ne pouvez pas simplement importer un module wasm ESM et obtenir cette prise en charge de la commutation de pile pour les promesses. La bonne nouvelle est que vous pouvez toujours utiliser les modules ESM avec cette API, juste avec quelques modules JS ESM comme colle.
En particulier, vous configurez trois modules ESM : foo-exports.js
, foo-wasm.wasm
et foo-imports.js
. Le module foo-imports.js
crée la suspension, l'utilise pour envelopper toutes les importations "asynchrones" productrices de promesses nécessaires à foo-wasm.wasm
, et exporte la suspension et ces importations. foo-wasm.wasm
importe ensuite toutes les importations "asynchrones" de foo-imports.js
et toutes les importations "synchrones" directement à partir de leurs modules respectifs (ou, bien sûr, vous pouvez également les proxy via foo-imports.js
, qui pourrait les exporter sans wrapping). Enfin, foo-exports.js
importe la bretelle de foo-imports.js
, importe les exportations de foo-wasm.wasm
, encapsule les exportations "asynchrones" à l'aide de la bretelle, puis exporte le "synchrone" (déballé) exportations et les exportations "asynchrones" encapsulées. Les clients importent ensuite à partir de foo-exports.js
et ne touchent jamais directement (ni n'ont besoin de connaître) foo-wasm.wasm
ou foo-imports.js
.
C'est un obstacle malheureux, mais c'était le meilleur que nous puissions atteindre étant donné la contrainte de ne pas modifier le core wasm. Nous visons cependant à garantir que cette conception est compatible avec la proposition d'extension de core wasm de telle sorte que, lorsque cette proposition sera expédiée, vous pourrez échanger ces trois modules contre un module étendu de wasm et que personne ne puisse sémantiquement faire la différence (renommage de fichier modulo).
Était-ce compréhensible et pensez-vous que cela répondrait à vos besoins (bien que maladroitement) ?
Je comprends le besoin de wrapper, du moins alors que les importations de type WebAssembly.Module Wasm ne sont pas encore possibles (et j'espère qu'elles le seront en temps voulu).
Plus précisément, je me demandais s'il était possible de décorer ces motifs dans l'intégration ESM afin que les deux côtés de la colle de suspension soient mieux gérés. Par exemple, s'il y avait des métadonnées qui reliaient les fonctions exportées et importées au format binaire, l'intégration ESM pourrait interroger cela et faire correspondre les fonctions de suspension d'emballage double importation / exportation en interne dans le cadre de la couche d'intégration en fonction de certaines règles prévisibles.
Ah. À l'heure actuelle, aucun plan de ce type n'est en place. Les commentaires que j'avais reçus étaient qu'il y avait un désir de ne pas changer l'intégration ESM non plus. En bref, l'espoir est qu'à terme tout cela soit possible dans core wasm, et nous voulons donc que cette proposition laisse une empreinte aussi petite que possible.
Les commentaires que j'avais reçus étaient qu'il y avait un désir de ne pas changer l'intégration ESM non plus
Pouvez-vous expliquer d'où vient ce retour ? Il y a beaucoup de possibilités d'étendre l'intégration ESM avec une sémantique d'intégration de niveau supérieur, un espace que je ne pense pas avoir été entièrement exploré, c'est pourquoi je l'évoque. Je n'ai pas entendu parler de résistance à l'amélioration de ce domaine dans le passé. Considérer cela comme une zone de sucre peut être un avantage pour les développeurs de JS en autorisant les importations/exportations directes de Promesse.
Il convient de noter que cette proposition entrave la possibilité pour un seul module JS dans un cycle d'être à la fois l'importateur et l'importé d'un module Wasm qui peut encore fonctionner pour le moment pour les importations de fonctions grâce au levage de fonction de cycle JS dans l'intégration ESM , mais ne prendrait pas en charge ce cycle de levage avec un wrapper d'expression Suspender autour de la fonction importée.
J'ai eu cette impression de @lukewagner. Je suis d'accord qu'il est possible d'étendre l'intégration ESM, mais je crois comprendre que cela nécessite des modifications/extensions du fichier wasm - ce que nous essayions d'éviter (dans le cadre de l'objectif de faible encombrement) - nous ne voulions donc pas de tels changements/ extensions pour faire partie de cette proposition. Bien sûr, si de telles modifications/extensions étaient ajoutées à la proposition ESM, elles compléteraient idéalement cette proposition afin que l'on n'ait pas besoin des modules wrapper JS pour obtenir les fonctionnalités offertes par cette proposition.
J'ai mal lu le commentaire de @Jack-Works, j'ai ajusté mon commentaire ci-dessus.
Merci @RossTate pour les éclaircissements, oui, je suggère d'explorer la possibilité de faire correspondre ces contextes de suspension d'importation et d'exportation via des métadonnées dans le binaire lui-même pour informer les intégrations d'hôtes, mais je ne m'attends en aucun cas à cela dans le MVP. Je profite également de l'occasion pour souligner que l'intégration ESM est un espace qui pourrait bénéficier du sucre de manière plus générale, séparément de l'API JS de base.
Pour être clair, le défi que j'ai souligné était que toutes les options que nous avons ajoutées à WebAssembly.instantiate()
(ou les nouvelles versions de WebAssembly.instantiate()
avec de nouveaux paramètres) devraient également apparaître lorsque wasm a été chargé via ESM -intégration, pas que l'intégration ESM était immuable.
Ah, cool, nous avons donc plus de flexibilité concernant l'ESM que je ne le pensais, en cas de besoin. Merci d'avoir corrigé mon incompréhension.
Il semble que nous parlions d'une sorte de section personnalisée pour spécifier comment certaines fonctions Wasm exportées doivent apparaître à JS en tant qu'API basées sur Promise, et peut-être à l'inverse comment les importations de Wasm peuvent être converties à partir d'API basées sur JS Promise en une sorte de commutation de pile. Est-ce que je comprends bien ?
J'aime cette idée. Je soupçonne que nous nous retrouverons à vouloir une section personnalisée analogue pour l'intégration Wasm GC/JS-ESM (ou une partie de la même). Je ne sais pas dans quelle mesure cette section personnalisée peut être multilingue, mais dans les deux cas, elle est probablement un peu moins universelle que les types d'interface et a également tendance à être utilisée dans un composant, pas seulement entre eux.
Quelqu'un souhaite-t-il rédiger une sorte d'essentiel ou un fichier README décrivant une conception de base pour cette section personnalisée ?
Il semble que ce soit une option possible. Comme vous le mentionnez, des options similaires ont été discutées dans la proposition du GC, comme dans WebAssembly/gc#203. L'intégration JS est provisoirement prévue pour être discutée dans le sous-groupe du GC demain, il pourrait donc être bon de garder à l'esprit le lien possible avec cette proposition au cours de cette discussion (ou cela pourrait s'avérer sans rapport, selon la façon dont la discussion se déroule).