Это предложение было разработано в сотрудничестве с @fmccabe , @thibaudmichaud , @lukewagner и @kripken , вместе с отзывами от Подгруппы стеков (сегодня неофициальное голосование одобрило его
Цель этого предложения - обеспечить относительно эффективное и относительно эргономичное взаимодействие между обещаниями JavaScript и WebAssembly, но работать с ограничением, заключающимся в том, что изменения касаются только JS API, а не ядра wasm.
Ожидается, что предложение по переключению стека в конечном итоге расширит ядро WebAssembly с помощью функций для реализации операций, которые мы предоставляем в этом предложении, непосредственно в WebAssembly, наряду со многими другими ценными операциями переключения стека, но этот конкретный вариант использования переключения стека имел достаточная срочность, чтобы заслужить более быстрый путь через только JS API.
Для получения дополнительной информации, пожалуйста, обратитесь к примечаниям и слайдам к собранию подгруппы стека 28 июня 2021 г. , где подробно описаны сценарии использования и факторы, которые мы приняли во внимание, а также кратко изложены причины того, как мы пришли к следующему дизайну.
ОБНОВЛЕНИЕ: после обратной связи, полученной подгруппой стеков от TC39, это предложение разрешает только приостановку стеков WebAssembly - оно не вносит изменений в язык JavaScript и, в частности, не включает косвенно поддержку для отсоединенных asycn
/ await
в JavaScript.
Это зависит (в некоторой степени) от предложения js-типов , которое вводит WebAssembly.Function
в качестве подкласса Function
.
Предложение состоит в том, чтобы добавить следующий интерфейс, конструктор и методы в JS API, с более подробной информацией об их семантике ниже.
interface Suspender {
constructor();
Function suspendOnReturnedPromise(Function func); // import wrapper
// overloaded: WebAssembly.Function suspendOnReturnedPromise(WebAssembly.Function func);
WebAssembly.Function returnPromiseOnSuspend(WebAssembly.Function func); // export wrapper
}
Ниже приводится пример того, как мы ожидаем использовать этот API.
В наших сценариях использования мы сочли полезным рассмотреть модули WebAssembly, которые потенциально имеют «синхронный» и «асинхронный» импорт и экспорт.
Текущий JS API поддерживает только «синхронный» импорт и экспорт.
Методы интерфейса Suspender используются для обертывания соответствующих операций импорта и экспорта, чтобы сделать их «асинхронными», при этом сам объект Suspender явно связывает эти операции импорта и экспорта вместе, чтобы облегчить как реализацию, так и возможность компоновки.
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)
)
)
Текст ( 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);
...
});
В этом примере у нас есть модуль WebAssembly, который представляет собой очень упрощенный конечный автомат - каждый раз, когда вы обновляете состояние, он просто вызывает импорт для вычисления дельты для добавления к состоянию.
Однако со стороны JavaScript функция, которую мы хотим использовать для вычисления дельты, должна выполняться асинхронно; то есть он возвращает обещание числа, а не само число.
Мы можем преодолеть этот разрыв в синхронизации, используя новый JS API.
В этом примере импорт модуля WebAssembly заключен в оболочку с использованием suspender.suspendOnReturnedPromise
, а экспорт заключен в оболочку с использованием suspender.returnPromiseOnSuspend
, оба с использованием одного и того же suspender
.
Этот suspender
соединяется с двумя вместе.
Это делает так, что, если когда-либо (развернутый) импорт возвращает обещание, экспорт (завернутый) возвращает обещание, причем все вычисления между ними «приостанавливаются» до тех пор, пока не разрешится обещание импорта.
Обертывание экспорта, по сути, добавляет маркер async
, а обертка импорта, по сути, добавляет маркер await
, но, в отличие от JavaScript, нам не нужно явно указывать поток async
/ await
через все промежуточные функции WebAssembly!
Между тем, вызов init_state
сделанный во время инициализации, обязательно возвращается без приостановки, а вызовы экспорта get_state
также всегда возвращаются без приостановки, поэтому предложение по-прежнему поддерживает существующий «синхронный» импорт и экспорт. экосистема WebAssembly использует сегодня.
Конечно, упускается из виду множество деталей, таких как тот факт, что если синхронный экспорт вызывает асинхронный импорт, то программа перехватит, если импорт попытается приостановить.
Ниже приводится более подробная спецификация, а также некоторые стратегии реализации.
Suspender
находится в одном из следующих состояний:
caller
] - элемент управления находится внутри Suspender
, при этом caller
- это функция, которая вызывается в Suspender
и ожидает externref
к возвратуМетод suspender.returnPromiseOnSuspend(func)
утверждает, что func
является WebAssembly.Function
с типом функции в форме [ti*] -> [to]
а затем возвращает WebAssembly.Function
с типом функции [ti*] -> [externref]
, который при вызове с аргументами args
выполняет следующие действия:
suspender
не является Неактивнымsuspender
на Активное [ caller
] (где caller
- текущий вызывающий)result
будет результатом вызова func(args)
(или любой ловушки или брошенного исключения)suspender
Активно [ caller'
] для некоторых caller'
(должно быть гарантировано, хотя вызывающий мог измениться)suspender
" Неактивно"result
на caller'
Метод suspender.suspendOnReturnedPromise(func)
func
- это WebAssembly.Function
, то утверждает, что его тип функции имеет форму [t*] -> [externref]
и возвращает WebAssembly.Function
с типом функции [t*] -> [externref]
;func
является Function
и возвращает Function
.В любом случае функция, возвращаемая suspender.suspendOnReturnedPromise(func)
, при вызове с аргументами args
делает следующее:
result
быть результатом вызова func(args)
(или любой ловушки или выброшенного исключения)result
не является возвращенным обещанием, то возвращает (или повторно выбрасывает) result
suspender
неактивно [ caller
] для некоторого caller
frames
будет кадрами стека, поскольку caller
frames
есть фреймы не приостанавливаемых функцийsuspender
на Приостановленоresult.then(onFulfilled, onRejected)
с функциями onFulfilled
и onRejected
которые делают следующее:suspender
приостановлено (должно быть гарантировано)suspender
на Активное [ caller'
], где caller'
- это вызывающий onFulfilled
/ onRejected
onFulfilled
преобразует заданное значение в externref
и возвращает его в frames
onRejected
выбрасывает заданное значение до frames
в качестве исключения в соответствии с JS API предложения по обработке исключений.Функция приостанавливается, если она была
suspendOnReturnedPromise
,returnPromiseOnSuspend
,Важно отметить, что функции, написанные на JavaScript, не подлежат приостановке в соответствии с отзывами членов TC39 , а функции хоста (за исключением немногих, перечисленных выше) не подлежат приостановке в соответствии с отзывами специалистов по обслуживанию движка.
Ниже приводится стратегия реализации этого предложения.
Предполагается, что движок поддерживает коммутацию стека, что, конечно же, является основной проблемой реализации.
Есть два вида стеков: стек хоста (и JavaScript) и стек WebAssembly. В каждом стеке WebAssembly есть поле подтяжки с именем suspender
. У каждого потока есть стек хоста.
Каждый Suspender
имеет два поля ссылки на стек: одно называется caller
а второе - suspended
.
caller
ссылается на (приостановленный) стек вызывающего объекта, а поле suspended
имеет значение NULL.suspended
ссылается на (приостановленный) стек WebAssembly, связанный в данный момент с подвеской, а поле caller
имеет значение NULL.suspender.returnPromiseOnSuspend(func)(args)
реализуется
suspender.caller
и suspended.suspended
равны нулю (в противном случае выполняется захват)stack
будет новым выделенным стеком WebAssembly, связанным с suspender
stack
и сохранение прежнего стека в suspender.caller
result
будет результатом func(args)
(или любой ловушки или выброшенного исключения)suspender.caller
и установка значения nullstack
result
suspender.suspendOnReturnedPromise(func)(args)
реализуется
func(args)
, перехват любой ловушки или сгенерированного исключенияresult
не является возвращенным обещанием, возврат (или повторный выброс) result
suspender.caller
не равно нулю (в противном случае захват)stack
будет текущим стекомstack
не является стеком WebAssembly, связанным с suspender
:stack
является стеком WebAssembly (в противном случае - захват)stack
до stack.suspender.caller
suspender.caller
, установка значения null и сохранение прежнего стека в suspender.suspended
result.then(onFulfilled, onRejected)
с функциями onFulfilled
и onRejected
, которые реализованыsuspender.suspended
, установка значения null и сохранение прежнего стека в suspender.caller
onFulfilled
, преобразование заданного значения в externref
и его возврат.onRejected
, изменение заданного значенияРеализация функции, сгенерированной путем создания функции хоста для приостанавливаемой функции, изменяется так, чтобы сначала переключиться на стек хоста текущего потока (если он еще не включен) и, наконец, вернуться к прежнему стеку.
Можно ли открыть API, который получает асинхронную функцию / генератор (синхронизацию или асинхронность), а затем превращает его в приостанавливаемую функцию?
Не могли бы вы прояснить, может быть, с помощью какого-нибудь псевдокода или варианта использования, что вы имеете в виду? Я хочу быть уверен, что дам вам точный ответ.
Предполагается, что Suspender
будет частью JS или отдельным API? Это исключительно для wasm ( WebAssembly.Suspender
)? Мне кажется, что это предложение следует обсудить в TC39.
Это специально НЕ предназначено для воздействия на программы JS. Точнее, попытка приостановить JS-функцию приведет к ловушке. Мы приложили некоторые усилия, чтобы обеспечить это.
Однако я могу обсудить это с Шу-ю, чтобы узнать его мнение.
Извините, @chicoxyzzy , я вижу, что забыл включить некоторый контекст / обновления из подгруппы стеков. Старшее стека переключения предложения были написаны в надежде , что вы должны быть в состоянии захватить JavaScript / хост - кадры в подвесных стеков. Однако мы получили отзывы от людей в TC39 о том, что есть опасения, что это слишком сильно повлияет на экосистему JS, и мы получили отзывы от разработчиков хоста о том, что не все фреймы хоста смогут выдержать приостановку. Таким образом, подгруппа Stacks с тех пор гарантирует, что проекты захватывают только WebAssembly (связанные) фреймы в приостановленных стеках, и это предложение удовлетворяет этому свойству. Я обновил OP, чтобы включить это важное примечание.
Приятно видеть здесь прогресс. Есть ли примеры того, как это будет использоваться в интеграции ESM для Wasm?
Плохая новость заключается в том, что, поскольку все это находится в JS API, вы не можете просто импортировать модуль wasm ESM и получить поддержку переключения стека для обещаний. Хорошей новостью является то, что вы все еще можете использовать модули ESM с этим API, только с некоторыми модулями JS ESM в качестве клея.
В частности, вы настроили три модуля ESM: foo-exports.js
, foo-wasm.wasm
и foo-imports.js
. Модуль foo-imports.js
создает подтяжку, использует ее для обертывания всего «асинхронного» импорта, производящего обещания, необходимого для foo-wasm.wasm
, и экспортирует подтяжку и этот импорт. foo-wasm.wasm
затем импортирует весь «асинхронный» импорт из foo-imports.js
и весь «синхронный» импорт непосредственно из соответствующих модулей (или, конечно, вы также можете проксировать их через foo-imports.js
, который мог экспортировать их без упаковки). Наконец, foo-exports.js
импортирует подтяжку из foo-imports.js
, импортирует экспорт foo-wasm.wasm
, обертывает «асинхронный» экспорт с помощью подтяжки, а затем экспортирует (развернутый) «синхронный» экспорт и завернутый "асинхронный" экспорт. Затем клиенты импортируют из foo-exports.js
и никогда не касаются напрямую (или не нуждаются в знаниях) foo-wasm.wasm
или foo-imports.js
.
Это досадное препятствие, но это лучшее, что мы могли достичь, учитывая ограничение - не изменять основной wasm. Тем не менее, мы стремимся обеспечить совместимость этой конструкции с предложением о расширении ядра wasm таким образом, чтобы при отправке этого предложения вы могли заменить эти три модуля на один модуль расширенного wasm, и никто не мог семантически скажите разницу (переименование файла по модулю).
Было ли это понятно, и как вы думаете, удовлетворит ли это ваши потребности (хотя и неудобно)?
Я понимаю необходимость обертывания, по крайней мере, в то время как импорт Wasm типа WebAssembly.Module еще невозможен (и, надеюсь, он будет со временем).
В частности, мне было интересно, можно ли вообще украсить эти узоры в интеграции ESM, чтобы можно было лучше управлять обеими сторонами клея для подтяжек. Например, если были какие-то метаданные, которые связывали экспортируемые и импортированные функции в двоичном формате, интеграция ESM могла бы запросить это и сопоставить функции приостановки двойного импорта / экспорта внутри как часть уровня интеграции на основе определенных предсказуемых правил.
Ах. В настоящее время такого плана нет. По отзывам, которые я получил, было желание не менять интеграцию ESM. Короче говоря, есть надежда, что в конечном итоге все это станет возможным в ядре wasm, и поэтому мы хотим, чтобы это предложение оставило как можно меньше места.
По отзывам, которые я получил, было желание не менять интеграцию ESM.
Не могли бы вы подробно рассказать, откуда исходит эта обратная связь? Существует много возможностей для расширения интеграции ESM с помощью семантики интеграции более высокого уровня, и я не чувствую, что эта область полностью исследована, поэтому я поднимаю ее. Раньше я не слышал о сопротивлении улучшению этой области. Рассмотрение этого как области для шугаринга может быть преимуществом для разработчиков JS, разрешив прямой импорт / экспорт Promise.
Стоит отметить, что это предложение действительно препятствует возможности для одного модуля JS в цикле быть как импортером, так и импортируемым модулем Wasm, который все еще может работать в данный момент для импорта функций благодаря подъему функций цикла JS в интеграции ESM. , но не поддерживает этот цикл подъема с помощью оболочки выражения Suspender вокруг импортированной функции.
Такое впечатление произвел на меня @lukewagner. Я согласен, что есть возможности для расширения интеграции ESM, но я понимаю, что это требует изменений / расширений в файле wasm - чего мы пытались избежать (как часть цели по уменьшению занимаемой площади) - поэтому мы не хотели таких изменений / расширения, чтобы быть частью этого предложения. Конечно, если бы такие изменения / расширения были добавлены в предложение ESM, они идеально дополняли бы это предложение, так что не требовались бы модули оболочки JS для получения функциональности, предлагаемой этим предложением.
Я неправильно прочитал комментарий @ Jack-Works, скорректировал свой комментарий выше.
Спасибо @RossTate за разъяснения, да, я предлагаю изучить возможность сопоставления этих контекстов приостановки импорта и экспорта через метаданные в самом двоичном файле, чтобы информировать об интеграции хоста, но не ожидая этого в MVP каким-либо образом. Я также пользуюсь возможностью, чтобы указать, что интеграция ESM - это область, в которой сахар может выиграть в более общем плане, отдельно от базового JS API.
Чтобы быть ясным, проблема, которую я указал, заключалась в том, что любые параметры, которые мы добавили в WebAssembly.instantiate()
(или новые версии WebAssembly.instantiate()
с новыми параметрами), также должны будут каким-то образом отображаться при загрузке wasm через ESM. -интеграция, а не то, что ESM-интеграция была неизменной.
Ах, здорово, так что у нас больше гибкости в отношении ESM, чем я предполагал, если в этом возникнет необходимость. Спасибо, что исправили мое недоразумение.
Похоже, мы говорим о каком-то настраиваемом разделе, чтобы указать, как определенные экспортированные функции Wasm должны отображаться в JS как API на основе Promise, и, возможно, наоборот, как импорт из Wasm может быть преобразован из API на основе JS в какой-то коммутации стека. Я правильно понимаю?
Мне нравится эта идея. Я подозреваю, что нам понадобится аналогичный настраиваемый раздел для интеграции Wasm GC / JS-ESM (или его часть). Я не уверен, в какой степени этот настраиваемый раздел может быть кросс-языковым, но в обоих случаях он, вероятно, немного менее универсален, чем типы интерфейсов, и также имеет тенденцию использоваться внутри компонента, а не только между ними.
Кто-нибудь хочет написать какую-то суть или README, описывающую базовый дизайн для этого настраиваемого раздела?
Похоже, это возможный вариант. Как вы упомянули, аналогичные варианты обсуждались в предложении GC, например, в WebAssembly / gc # 203. Предварительно планируется обсудить JS-интеграцию в подгруппе GC завтра, поэтому было бы неплохо иметь в виду возможную связь с этим предложением во время обсуждения (или это может оказаться несвязанным, в зависимости от того, как пойдет обсуждение).