Design: Предложение: Async / Await JS API

Созданный на 26 июл. 2021  ·  16Комментарии  ·  Источник: WebAssembly/design

Это предложение было разработано в сотрудничестве с @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 выполняет следующие действия:

  1. Ловушки, если состояние suspender не является Неактивным
  2. Изменяет состояние suspender на Активное [ caller ] (где caller - текущий вызывающий)
  3. Пусть result будет результатом вызова func(args) (или любой ловушки или брошенного исключения)
  4. Утверждает, что состояние suspender Активно [ caller' ] для некоторых caller' (должно быть гарантировано, хотя вызывающий мог измениться)
  5. Изменяет состояние suspender " Неактивно"
  6. Возвращает (или повторно выбрасывает) result на caller'

Метод suspender.suspendOnReturnedPromise(func)

  • если func - это WebAssembly.Function , то утверждает, что его тип функции имеет форму [t*] -> [externref] и возвращает WebAssembly.Function с типом функции [t*] -> [externref] ;
  • в противном случае утверждает, что func является Function и возвращает Function .

В любом случае функция, возвращаемая suspender.suspendOnReturnedPromise(func) , при вызове с аргументами args делает следующее:

  1. Позволяет result быть результатом вызова func(args) (или любой ловушки или выброшенного исключения)
  2. Если result не является возвращенным обещанием, то возвращает (или повторно выбрасывает) result
  3. Ловушки, если состояние suspender неактивно [ caller ] для некоторого caller
  4. Пусть frames будет кадрами стека, поскольку caller
  5. Ловушки, если в frames есть фреймы не приостанавливаемых функций
  6. Изменяет состояние suspender на Приостановлено
  7. Возвращает результат result.then(onFulfilled, onRejected) с функциями onFulfilled и onRejected которые делают следующее:

    1. Утверждает, что состояние suspender приостановлено (должно быть гарантировано)

    2. Изменяет состояние suspender на Активное [ caller' ], где caller' - это вызывающий onFulfilled / onRejected



      • В случае onFulfilled преобразует заданное значение в externref и возвращает его в frames


      • В случае onRejected выбрасывает заданное значение до frames в качестве исключения в соответствии с JS API предложения по обработке исключений.



Функция приостанавливается, если она была

  • определяется модулем WebAssembly,
  • возвращается suspendOnReturnedPromise ,
  • возвращается returnPromiseOnSuspend ,
  • или генерируется путем создания хост-функции для приостанавливаемой функции

Важно отметить, что функции, написанные на JavaScript, не подлежат приостановке в соответствии с отзывами членов TC39 , а функции хоста (за исключением немногих, перечисленных выше) не подлежат приостановке в соответствии с отзывами специалистов по обслуживанию движка.

Реализация

Ниже приводится стратегия реализации этого предложения.
Предполагается, что движок поддерживает коммутацию стека, что, конечно же, является основной проблемой реализации.

Есть два вида стеков: стек хоста (и JavaScript) и стек WebAssembly. В каждом стеке WebAssembly есть поле подтяжки с именем suspender . У каждого потока есть стек хоста.

Каждый Suspender имеет два поля ссылки на стек: одно называется caller а второе - suspended .

  • В неактивном состоянии оба поля пустые.
  • В активном состоянии поле caller ссылается на (приостановленный) стек вызывающего объекта, а поле suspended имеет значение NULL.
  • В состоянии Suspended поле suspended ссылается на (приостановленный) стек WebAssembly, связанный в данный момент с подвеской, а поле caller имеет значение NULL.

suspender.returnPromiseOnSuspend(func)(args) реализуется

  1. Проверка того, что suspender.caller и suspended.suspended равны нулю (в противном случае выполняется захват)
  2. Допустим, что stack будет новым выделенным стеком WebAssembly, связанным с suspender
  3. Переключение на stack и сохранение прежнего стека в suspender.caller
  4. Допустим, что result будет результатом func(args) (или любой ловушки или выброшенного исключения)
  5. Переключение на suspender.caller и установка значения null
  6. Освобождение stack
  7. Возврат (или повторный выброс) result

suspender.suspendOnReturnedPromise(func)(args) реализуется

  1. Вызов func(args) , перехват любой ловушки или сгенерированного исключения
  2. Если result не является возвращенным обещанием, возврат (или повторный выброс) result
  3. Проверка того, что suspender.caller не равно нулю (в противном случае захват)
  4. Пусть stack будет текущим стеком
  5. Хотя stack не является стеком WebAssembly, связанным с suspender :

    • Проверка того, что stack является стеком WebAssembly (в противном случае - захват)

    • Обновление stack до stack.suspender.caller

  6. Переключение на suspender.caller , установка значения null и сохранение прежнего стека в suspender.suspended
  7. Возврат результата result.then(onFulfilled, onRejected) с функциями onFulfilled и onRejected , которые реализованы

    1. Переключение на suspender.suspended , установка значения null и сохранение прежнего стека в suspender.caller



      • В случае onFulfilled , преобразование заданного значения в externref и его возврат.


      • В случае onRejected , изменение заданного значения



Реализация функции, сгенерированной путем создания функции хоста для приостанавливаемой функции, изменяется так, чтобы сначала переключиться на стек хоста текущего потока (если он еще не включен) и, наконец, вернуться к прежнему стеку.

Все 16 Комментарий

Можно ли открыть 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 завтра, поэтому было бы неплохо иметь в виду возможную связь с этим предложением во время обсуждения (или это может оказаться несвязанным, в зависимости от того, как пойдет обсуждение).

Была ли эта страница полезной?
0 / 5 - 0 рейтинги

Смежные вопросы

KloudKoder picture KloudKoder  ·  55Комментарии

jfbastien picture jfbastien  ·  244Комментарии

autumnontape picture autumnontape  ·  38Комментарии

lukewagner picture lukewagner  ·  44Комментарии

s3ththompson picture s3ththompson  ·  62Комментарии