Esta propuesta se desarrolló en colaboración con @fmccabe , @thibaudmichaud , @lukewagner y @kripken , junto con los comentarios del Subgrupo Stacks (con una votación informal aprobando el avance a la Fase 0 hoy). Tenga en cuenta que, debido a limitaciones de tiempo, el plan es tener una presentación muy rápida (es decir, 5 minutos) y votar para avanzar a la Fase 1 el 3 de agosto. Para facilitar eso, recomendamos encarecidamente a las personas que planteen sus inquietudes aquí con anticipación para que podamos determinar si hay alguna inquietud importante que ameritaría retrasar la presentación + voto para una fecha posterior con más tiempo.
El propósito de esta propuesta es proporcionar una interoperabilidad relativamente eficiente y relativamente ergonómica entre las promesas de JavaScript y WebAssembly, pero trabajando bajo la restricción de que los únicos cambios son en la API de JS y no en el núcleo de wasm.
La expectativa es que la propuesta de Stack-Switching eventualmente extenderá WebAssembly central con la funcionalidad para implementar las operaciones que proporcionamos en esta propuesta directamente dentro de WebAssembly, junto con muchas otras valiosas operaciones de stack-switching, pero que este caso de uso particular para el cambio de pila tenía suficiente urgencia para merecer una ruta más rápida a través de solo la API de JS.
Para obtener más información, consulte las notas y diapositivas de la reunión del subgrupo de pila del 28 de junio de 2021 , que detalla los escenarios de uso y los factores que tomamos en consideración y resume la justificación de cómo llegamos al siguiente diseño.
ACTUALIZACIÓN: Después de los comentarios que el Subgrupo de Pilas había recibido de TC39, esta propuesta solo permite suspender las pilas de WebAssembly; no realiza cambios en el lenguaje JavaScript y, en particular, no habilita indirectamente la compatibilidad con asycn
/ await
en JavaScript.
Esto depende (vagamente) de la propuesta js-types , que introduce WebAssembly.Function
como una subclase de Function
.
La propuesta es agregar la siguiente interfaz, constructor y métodos a la API de JS, con más detalles sobre su semántica a continuación.
interface Suspender {
constructor();
Function suspendOnReturnedPromise(Function func); // import wrapper
// overloaded: WebAssembly.Function suspendOnReturnedPromise(WebAssembly.Function func);
WebAssembly.Function returnPromiseOnSuspend(WebAssembly.Function func); // export wrapper
}
El siguiente es un ejemplo de cómo esperamos que uno use esta API.
En nuestros escenarios de uso, nos pareció útil considerar que los módulos de WebAssembly tienen conceptualmente importaciones y exportaciones "sincrónicas" y "asincrónicas".
La API JS actual solo admite importaciones y exportaciones "síncronas".
Los métodos de la interfaz Suspender se utilizan para envolver las importaciones y exportaciones relevantes con el fin de hacer "asincrónicas", con el objeto Suspender en sí mismo conectando explícitamente estas importaciones y exportaciones para facilitar tanto la implementación como la componibilidad.
Montaje web ( 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)
)
)
Texto ( 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);
...
});
En este ejemplo, tenemos un módulo WebAssembly que es una máquina de estado muy simplista: cada vez que actualiza el estado, simplemente llama a una importación para calcular un delta y agregarlo al estado.
Sin embargo, en el lado de JavaScript, la función que queremos usar para calcular el delta resulta que debe ejecutarse de forma asíncrona; es decir, devuelve una Promesa de un Número en lugar de un Número en sí.
Podemos cerrar esta brecha de sincronía utilizando la nueva API de JS.
En el ejemplo, una importación del módulo WebAssembly se ajusta con suspender.suspendOnReturnedPromise
, y una exportación se ajusta con suspender.returnPromiseOnSuspend
, ambos con el mismo suspender
.
Ese suspender
conecta a los dos juntos.
Hace que, si alguna vez la importación (sin empaquetar) devuelve una Promesa, la exportación (empaquetada) devuelve una Promesa, con todos los cálculos intermedios "suspendidos" hasta que se resuelva la Promesa de la importación.
El empaquetado de la exportación consiste esencialmente en agregar un marcador async
, y el empaquetado de la importación esencialmente agrega un marcador await
, pero a diferencia de JavaScript, no tenemos que enhebrar explícitamente async
/ await
través de todas las funciones intermedias de WebAssembly!
Mientras tanto, la llamada realizada a init_state
durante la inicialización necesariamente regresa sin suspender, y las llamadas a la exportación get_state
también regresan siempre sin suspender, por lo que la propuesta aún admite las importaciones y exportaciones "síncronas" existentes. que utiliza el ecosistema WebAssembly en la actualidad.
Por supuesto, hay muchos detalles que se están repasando, como el hecho de que si una exportación síncrona llama a una importación asincrónica, el programa interceptará si la importación intenta suspender.
A continuación, se proporciona una especificación más detallada, así como una estrategia de implementación.
Un Suspender
encuentra en uno de los siguientes estados:
caller
] - el control está dentro de Suspender
, siendo caller
la función que llamó al Suspender
y está esperando un externref
para ser devueltoEl método suspender.returnPromiseOnSuspend(func)
afirma que func
es un WebAssembly.Function
con un tipo de función de la forma [ti*] -> [to]
y luego devuelve un WebAssembly.Function
con el tipo de función [ti*] -> [externref]
que hace lo siguiente cuando se llama con argumentos args
:
suspender
no está Inactivosuspender
a Activo [ caller
] (donde caller
es la persona que llama actualmente)result
sea el resultado de llamar a func(args)
(o cualquier trampa o excepción lanzada)suspender
es Activo [ caller'
] por unos caller'
(debería estar garantizado, aunque la persona que llama podría haber cambiado)suspender
a Inactivoresult
a caller'
El método suspender.suspendOnReturnedPromise(func)
func
es WebAssembly.Function
, entonces afirma que su tipo de función es de la forma [t*] -> [externref]
y devuelve WebAssembly.Function
con el tipo de función [t*] -> [externref]
;func
es un Function
y devuelve un Function
.En cualquier caso, la función devuelta por suspender.suspendOnReturnedPromise(func)
hace lo siguiente cuando se llama con argumentos args
:
result
sea el resultado de llamar a func(args)
(o cualquier trampa o excepción lanzada)result
no es una Promesa devuelta, entonces devuelve (o vuelve a lanzar) result
suspender
no está activo [ caller
] para algunos caller
frames
sean los marcos de pila desde caller
frames
suspender
a Suspendidoresult.then(onFulfilled, onRejected)
con las funciones onFulfilled
y onRejected
que hacen lo siguiente:suspender
está suspendido (debe estar garantizado)suspender
a Activo [ caller'
], donde caller'
es el llamador de onFulfilled
/ onRejected
onFulfilled
, convierte el valor dado en externref
y lo devuelve a frames
onRejected
, arroja el valor dado hasta frames
como una excepción de acuerdo con la API de JS de la propuesta de manejo de excepcionesUna función se puede suspender si se
suspendOnReturnedPromise
,returnPromiseOnSuspend
,Es importante destacar que las funciones escritas en JavaScript no se TC39 , y las funciones del host (excepto las pocas enumeradas anteriormente) no se
La siguiente es una estrategia de implementación para esta propuesta.
Asume el soporte del motor para el cambio de pila, que por supuesto es donde se encuentran los principales desafíos de implementación.
Hay dos tipos de pilas: una pila de host (y JavaScript) y una pila de WebAssembly. Cada pila de WebAssembly tiene un campo de suspensión llamado suspender
. Cada hilo tiene una pila de hosts.
Cada Suspender
tiene dos campos de referencia de pila: uno llamado caller
y otro llamado suspended
.
caller
referencia a la pila (suspendida) de la persona que llama, y el campo suspended
es nulosuspended
referencia a la pila de WebAssembly (suspendida) actualmente asociada con la suspensión, y el campo caller
es nulo.suspender.returnPromiseOnSuspend(func)(args)
es implementado por
suspender.caller
y suspended.suspended
son nulos (trapping de lo contrario)stack
sea una pila de ensamblaje web recién asignada asociada con suspender
stack
y almacenar la pila anterior en suspender.caller
result
sea el resultado de func(args)
(o cualquier trampa o excepción lanzada)suspender.caller
y establecerlo en nulostack
result
suspender.suspendOnReturnedPromise(func)(args)
es implementado por
func(args)
, atrapando cualquier trampa o excepción lanzadaresult
no es una Promesa devuelta, devolver (o volver a lanzar) result
suspender.caller
no sea nulo (trapping de lo contrario)stack
la pila actualstack
no es una pila de WebAssembly asociada con suspender
:stack
es una pila de WebAssembly (de lo contrario, se capturará)stack
para que sea stack.suspender.caller
suspender.caller
, establecerlo en nulo y almacenar la pila anterior en suspender.suspended
result.then(onFulfilled, onRejected)
con las funciones onFulfilled
y onRejected
implementadas porsuspender.suspended
, establecerlo en nulo y almacenar la pila anterior en suspender.caller
onFulfilled
, convertir el valor dado a externref
y devolverloonRejected
, volviendo a arrojar el valor dadoLa implementación de la función generada al crear una función de host para una función suspendible se cambia para cambiar primero a la pila de host del hilo actual (si no está ya en ella) y, por último, volver a la pila anterior.
¿Es posible exponer una API que recibe una función / generador asíncrono (sincronizado o asíncrono) y luego convertirlo en una función suspendible?
¿Puede aclarar, tal vez con algún pseudocódigo o un caso de uso, lo que quiere decir? Quiero asegurarme de darte una respuesta precisa.
¿La intención es que Suspender
sea parte de JS o sea una API separada? ¿Es exclusivamente para wasm ( WebAssembly.Suspender
)? Me parece que esta propuesta debería debatirse en el TC39.
Específicamente NO está destinado a afectar los programas JS. Más precisamente, intentar suspender una función JS resultará en una trampa. Nos hemos tomado la molestia de asegurar esto.
Sin embargo, puedo planteárselo a Shu-yu para obtener su opinión.
Lo siento, @chicoxyzzy , veo que olvidé incluir algunos contextos / actualizaciones del subgrupo de pilas. Las propuestas de conmutación de pila más antiguas se escribieron con la expectativa de que debería poder capturar marcos de JavaScript / host en pilas suspendidas. Sin embargo, recibimos comentarios de las personas en TC39 de que existía la preocupación de que esto afectara demasiado drásticamente el ecosistema JS, y recibimos comentarios de los implementadores de host de que existía la preocupación de que no todos los marcos de host pudieran tolerar la suspensión. Por lo tanto, desde entonces, el subgrupo de pilas se ha asegurado de que los diseños solo capturen marcos de WebAssembly (relacionados) en pilas suspendidas, y esta propuesta satisface esa propiedad. Actualicé el OP para incluir esta nota importante.
Es genial ver el progreso aquí. ¿Hay ejemplos de cómo se usaría esto en la integración de ESM para Wasm?
La mala noticia es que, debido a que todo esto está en la API de JS, no puede simplemente importar un módulo wasm de ESM y obtener este soporte de cambio de pila para promesas. La buena noticia es que aún puede usar módulos ESM con esta API, solo con algunos módulos JS ESM como pegamento.
En particular, configura tres módulos ESM: foo-exports.js
, foo-wasm.wasm
y foo-imports.js
. El módulo foo-imports.js
crea el suspensor, lo usa para envolver todas las importaciones "asincrónicas" que producen promesas necesarias para foo-wasm.wasm
, y exporta el suspensor y esas importaciones. foo-wasm.wasm
luego importa todas las importaciones "asincrónicas" de foo-imports.js
y todas las importaciones "síncronas" directamente desde sus respectivos módulos (o, por supuesto, también puede usarlas como proxy a través de foo-imports.js
, que podría exportarlos sin envolver). Por último, foo-exports.js
importa el suspensor de foo-imports.js
, importa las exportaciones de foo-wasm.wasm
, envuelve las exportaciones "asincrónicas" usando el suspensor y luego exporta el (desenvuelto) "síncrono" exportaciones y las exportaciones "asincrónicas" envueltas. Luego, los clientes importan desde foo-exports.js
y nunca tocan directamente (o necesitan conocer) foo-wasm.wasm
o foo-imports.js
.
Es un obstáculo desafortunado, pero fue lo mejor que pudimos lograr dada la restricción de no modificar el núcleo central. Sin embargo, nuestro objetivo es garantizar que este diseño sea compatible con la propuesta que extiende el núcleo wasm de tal manera que, cuando se envíe esa propuesta, pueda intercambiar estos tres módulos por un módulo extendido-wasm y nadie puede hacerlo semánticamente. decir la diferencia (cambio de nombre de archivo de módulo).
¿Fue comprensible y cree que podría satisfacer sus necesidades (aunque de forma incómoda)?
Entiendo la necesidad de envolver, al menos mientras las importaciones de Wasm de tipo WebAssembly.Module aún no son posibles (y espero que lo sean a su debido tiempo).
Sin embargo, más específicamente, me preguntaba si había margen para decorar estos patrones en la integración de ESM para que ambos lados del pegamento de suspensión pudieran manejarse mejor. Por ejemplo, si había algunos metadatos que vinculaban las funciones exportadas e importadas en formato binario, la integración de ESM podría interrogar eso y hacer coincidir las funciones de suspensión de envoltura de importación / exportación dual internamente como parte de la capa de integración en función de ciertas reglas predecibles.
¡Ah! En la actualidad, no existe tal plan. La retroalimentación que había recibido era que tampoco había el deseo de cambiar la integración de ESM. En resumen, la esperanza es que eventualmente todo esto sea posible en core wasm, por lo que queremos que esta propuesta deje una huella lo más pequeña posible.
La retroalimentación que había recibido era que tampoco había el deseo de cambiar la integración de ESM
¿Puede colaborar sobre de dónde provienen estos comentarios? Hay mucho margen para extender la integración de ESM con semánticas de integración de nivel superior, un espacio que no creo que se haya explorado por completo, por lo que lo menciono. No he oído hablar de resistencia a mejorar esta área en el pasado. Ver esto como un área para endulzar puede ser un beneficio para los desarrolladores de JS al permitir importaciones / exportaciones directas de Promise.
Vale la pena señalar que esta propuesta dificulta la capacidad de que un solo módulo JS en un ciclo sea tanto el importador como el importador a un módulo Wasm que aún puede funcionar en el momento para las importaciones de funciones gracias al levantamiento de funciones del ciclo JS en la integración ESM. , pero no admitiría este ciclo de elevación con un contenedor de expresión Suspender alrededor de la función importada.
Recibí esta impresión de @lukewagner. Estoy de acuerdo en que hay margen para ampliar la integración de ESM, pero tengo entendido que esto requiere cambios / extensiones en el archivo wasm, que estábamos tratando de evitar (como parte del objetivo de tamaño reducido), por lo que no queríamos tales cambios / extensiones para formar parte de esta propuesta. Por supuesto, si dichos cambios / extensiones se agregaran a la propuesta de ESM, lo ideal sería que complementaran esta propuesta de modo que uno no necesitaría los módulos de envoltura JS para obtener la funcionalidad que ofrece esta propuesta.
Leí mal el comentario de @ Jack-Works, he ajustado mi comentario anterior.
Gracias @RossTate por las aclaraciones, sí, sugiero explorar la posibilidad de hacer coincidir estos contextos de suspensión de importación y exportación a través de metadatos en el propio binario para informar las integraciones de host, pero sin esperar eso en el MVP de ninguna manera. También estoy aprovechando la oportunidad para señalar que la integración de ESM es un espacio que podría beneficiarse del azúcar de manera más general, por separado de la API JS base.
Para ser claros, el desafío que señalé fue que cualquier opción que agreguemos a WebAssembly.instantiate()
(o nuevas versiones de WebAssembly.instantiate()
con nuevos parámetros) también tendrían que aparecer de alguna manera cuando wasm se cargó a través de ESM. -integración, no es que la integración de ESM fuera inmutable.
Ah, genial, entonces tenemos más flexibilidad con respecto a ESM de lo que pensé, en caso de que surja la necesidad. Gracias por corregir mi malentendido.
Parece que estamos hablando de algún tipo de sección personalizada para especificar cómo ciertas funciones Wasm exportadas deberían aparecer en JS como API basadas en Promise, y tal vez a la inversa, cómo las importaciones de Wasm se pueden convertir de API basadas en JS Promise a algún tipo. de conmutación de pila. ¿Estoy entendiendo correctamente?
Me gusta esta idea. Sospecho que nos encontraremos queriendo una sección personalizada análoga para la integración de Wasm GC / JS-ESM (o parte de la misma). No estoy seguro de hasta qué punto esta sección personalizada podría ser de varios idiomas, pero en ambos casos, probablemente sea un poco menos universal que los tipos de interfaz, y también tiende a usarse dentro de un componente, no solo entre ellos.
¿Alguien quiere escribir algún tipo de esencia o README que describa un diseño básico para esta sección personalizada?
Parece que es una opción posible. Como mencionas, se han discutido opciones similares en la propuesta de GC, como en WebAssembly / gc # 203. La integración de JS está programada tentativamente para ser discutida en el subgrupo de GC mañana, por lo que sería bueno tener en cuenta la posible conexión con esta propuesta durante esa discusión (o podría resultar que no está relacionada, dependiendo de cómo se desarrolle la discusión).