Design: Proposition : API JS asynchrone/en attente

Créé le 26 juil. 2021  ·  16Commentaires  ·  Source: WebAssembly/design

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 .

Interface

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
}

Exemple

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.

spécification

Un Suspender est dans l'un des états suivants :

  • Inactif - pas utilisé pour le moment
  • Actif [ caller ] - le contrôle est à l'intérieur du Suspender , avec caller étant la fonction qui a appelé le Suspender et attend un externref à retourner
  • Suspendu - en attente d'une promesse de résolution

La 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 :

  1. Piège si l'état de suspender n'est pas Inactif
  2. Change l'état de suspender en Actif [ caller ] (où caller est l'appelant actuel)
  3. Soit result le résultat de l'appel de func(args) (ou de tout piège ou exception levée)
  4. Affirme que l'état de suspender est Actif [ caller' ] pour certains caller' (devrait être garanti, même si l'appelant peut avoir changé)
  5. Change l'état de suspender en Inactif
  6. Retourne (ou relance) result à caller'

La méthode suspender.suspendOnReturnedPromise(func)

  • si 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] ;
  • sinon, affirme que 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 :

  1. Soit result le résultat de l'appel de func(args) (ou de tout piège ou exception levée)
  2. Si result n'est pas une Promesse retournée, alors retourne (ou relance) result
  3. Piège si l'état de suspender n'est pas Actif [ caller ] pour certains caller
  4. Soit frames les cadres de pile depuis caller
  5. Piège s'il y a des cadres de fonctions non suspendues dans frames
  6. Change l'état de suspender en Suspendu
  7. Renvoie le résultat de result.then(onFulfilled, onRejected) avec les fonctions onFulfilled et onRejected qui effectuent les opérations suivantes :

    1. Affirme que l'état de suspender est Suspendu (doit être garanti)

    2. Change l'état de suspender en Actif [ caller' ], où caller' est l'appelant de onFulfilled / onRejected



      • Dans le cas de onFulfilled , convertit la valeur donnée en externref et la renvoie à frames


      • Dans le cas de onRejected , lève la valeur donnée jusqu'à frames en tant qu'exception selon l'API JS de la proposition de gestion des exceptions



Une fonction est suspendue si elle était

  • défini par un module WebAssembly,
  • retourné par suspendOnReturnedPromise ,
  • renvoyé par returnPromiseOnSuspend ,
  • ou généré en créant une fonction hôte pour une fonction pouvant être suspendue

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

Mise en œuvre

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 .

  • Dans l'état Inactif , les deux champs sont nuls.
  • Dans l'état Actif , le champ caller référence à la pile (suspendue) de l'appelant, et le champ suspended est nul
  • Dans l'état Suspendu , le champ suspended 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

  1. Vérifier que suspender.caller et suspended.suspended sont nuls (piégeage sinon)
  2. Laisser stack être une pile WebAssembly nouvellement allouée associée à suspender
  3. Passer à stack et stocker l'ancienne pile dans suspender.caller
  4. Laisser result être le résultat de func(args) (ou de tout piège ou exception levée)
  5. Passer à suspender.caller et le définir sur null
  6. Libérer stack
  7. Retour (ou relance) result

suspender.suspendOnReturnedPromise(func)(args) est implémenté par

  1. Appeler func(args) , intercepter tout piège ou exception levée
  2. Si result n'est pas une Promesse retournée, retour (ou relance) result
  3. Vérifier que suspender.caller n'est pas nul (piégeage sinon)
  4. Soit stack la pile actuelle
  5. Alors que stack n'est pas une pile WebAssembly associée à suspender :

    • Vérifier que stack est une pile WebAssembly (piégeage sinon)

    • Mise stack jour de stack.suspender.caller

  6. Basculer vers suspender.caller , le définir sur null et stocker l'ancienne pile dans suspender.suspended
  7. Renvoyer le résultat de result.then(onFulfilled, onRejected) avec les fonctions onFulfilled et onRejected qui sont implémentées par

    1. Basculer vers suspender.suspended , le définir sur null et stocker l'ancienne pile dans suspender.caller



      • Dans le cas de onFulfilled , convertir la valeur donnée en externref et la renvoyer


      • Dans le cas de onRejected , relancer la valeur donnée



L'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.

Tous les 16 commentaires

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

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

bobOnGitHub picture bobOnGitHub  ·  6Commentaires

spidoche picture spidoche  ·  4Commentaires

void4 picture void4  ·  5Commentaires

JimmyVV picture JimmyVV  ·  4Commentaires

mfateev picture mfateev  ·  5Commentaires