Перейти к основному содержимому

В API WebAssembly JSPI появились изменения

· 6 мин. чтения
Фрэнсис МакКейб, Тибо Мишо, Илья Резвов, Брэндан Даль

В API интеграции JavaScript Promise (JSPI) WebAssembly появился новый API, доступный в Chrome версии M126. Мы расскажем, что изменилось, как использовать это с Emscripten, и как выглядит дорожная карта JSPI.

JSPI — это API, который позволяет приложениям WebAssembly, использующим последовательные API, обращаться к веб-API, которые являются асинхронными. Многие веб-API работают с объектами JavaScript Promise: вместо непосредственного выполнения запрашиваемой операции они возвращают Promise для выполнения. С другой стороны, многие приложения, скомпилированные в WebAssembly, приходят из мира C/C++, где доминируют API, блокирующие вызвавшего до завершения операции.

JSPI интегрируется в веб-архитектуру, чтобы позволить приложениям WebAssembly приостанавливать выполнение при возврате Promise и возобновлять выполнение, когда Promise будет выполнен.

Вы можете узнать больше о JSPI и его использовании в этом блоге и в спецификации.

Что нового?

Конец объектов Suspender

В январе 2024 года подгруппа Stacks группы Wasm CG проголосовала за внесение изменений в API JSPI. В частности, вместо явного объекта Suspender будет использоваться граница JavaScript/WebAssembly для определения расчетов, которые приостанавливаются.

Разница довольно мала, но может быть значительной: приостановка вычисления осуществляется в наиболее недавнем вызове экспортированной функции WebAssembly, который определяет 'точку отсечения' для приостановки.

Это означает, что разработчик, использующий JSPI, имеет чуть меньше контроля над этой точкой отсечения. С другой стороны, отказ от явного управления объектами Suspender делает API значительно проще в использовании.

Больше нет WebAssembly.Function

Еще одно изменение касается стиля API. Вместо описания оболочек JSPI в терминах конструктора WebAssembly.Function, мы предоставляем конкретные функции и конструкторы.

Это дает ряд преимуществ:

  • Устранение зависимости от предложения Type Reflection.
  • Упрощение инструментов для JSPI: новые функции API больше не нужно явно ссылаться на типы функций WebAssembly.

Это изменение стало возможным благодаря решению об отказе от явно ссылочных объектов Suspender.

Возврат без приостановки

Третье изменение касается поведения приостановки вызовов. Вместо того чтобы всегда приостанавливать вызов JavaScript-функции из импортированной функции с приостановкой, мы теперь будем приостанавливать выполнение только в случае, если JavaScript-функция фактически возвращает Promise.

Это изменение, несмотря на то, что оно кажется противоречащим рекомендациям W3C TAG, представляет собой безопасную оптимизацию для пользователей JSPI. Оно безопасно, поскольку JSPI фактически берет на себя роль вызвавшего функцию, возвращающую Promise.

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

Новый API

API прост: предоставляется функция, которая принимает функцию, экспортированную из модуля WebAssembly, и преобразует ее в функцию, возвращающую Promise:

Function Webassembly.promising(Function wsFun)

Обратите внимание, что даже если аргумент типизирован как JavaScript Function, он фактически ограничен функциями WebAssembly.

На стороне приостановки есть новый класс WebAssembly.Suspending вместе с конструктором, принимающим JavaScript-функцию в качестве аргумента. В WebIDL это выглядит следующим образом:

interface Suspending{
constructor (Function fun);
}

Обратите внимание, что этот API имеет асимметричный характер: есть функция, которая принимает функцию WebAssembly и возвращает новую, обнадеживающую (sic) функцию; тогда как для пометки функции с приостановкой ее нужно обернуть в объект Suspending. Это отражает более глубокую реальность происходящего за кулисами.

Поведение приостановки импорта является неотъемлемой частью вызова к импорту: т.е. какая-то функция внутри инстанцированного модуля вызывает импорт и приостанавливается вследствие этого.

С другой стороны, функция promising принимает регулярную функцию WebAssembly и возвращает новую, которая может реагировать на приостановку и возвращать Promise.

Использование нового API

Если вы пользователь Emscripten, то использование нового API, как правило, не потребует изменений в вашем коде. Вам нужно использовать версию Emscripten не ниже 3.1.61, а также версию Chrome не ниже 126.0.6478.17 (Chrome M126).

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

Старый API

Старый API будет продолжать работать как минимум до 29 октября 2024 года (Chrome M128). После этого мы планируем удалить старый API.

Обратите внимание, что Emscripten сам по себе не будет поддерживать старый API начиная с версии 3.1.61.

Определение, какой API используется в вашем браузере

Смена API никогда не должна восприниматься легкомысленно. Мы можем это сделать в данном случае, потому что сам JSPI все еще является предварительным. Есть простой способ проверить, какой API включен в вашем браузере:

function oldAPI(){
return WebAssembly.Suspender!=undefined
}

function newAPI(){
return WebAssembly.Suspending!=undefined
}

Функция oldAPI возвращает true, если старый JSPI API включен в вашем браузере, а функция newAPI возвращает true, если включен новый JSPI API.

Что происходит с JSPI?

Аспекты реализации

Самое большое изменение в JSPI, над которым мы работаем, на самом деле невидимо для большинства программистов: так называемые растущие стеки.

Текущая реализация JSPI основана на выделении стеков фиксированного размера. Фактически выделенные стеки довольно большие. Это связано с тем, что мы должны быть в состоянии обрабатывать произвольные вычисления WebAssembly, которые могут потребовать глубоких стеков для правильной обработки рекурсии.

Однако эта стратегия не является устойчивой: мы хотим поддерживать приложения с миллионами приостановленных сопрограмм; это невозможно, если каждый стек имеет размер 1 МБ.

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

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

Обратите внимание, что, хотя мы можем решать проблему переполнения стека для сопрограмм, мы не планируем делать основной или центральный стек растущим. Таким образом, если вашему приложению не хватает пространства в стеке, растущие стеки не решат вашу проблему, если вы не используете JSPI.

Процесс стандартизации

На момент публикации существует активное первоначальное испытание JSPI. Новый API будет доступен на протяжении оставшейся части первоначального испытания — начиная с Chrome M126.

Предыдущий API также будет доступен во время первоначального испытания; однако его планируется прекратить вскоре после Chrome M128.

После этого основное направление для JSPI будет сосредоточено вокруг процесса стандартизации. JSPI в настоящее время (на момент публикации) находится на этапе 3 процесса W3C Wasm CG. Следующий шаг, а именно переход на этап 4, знаменует собой ключевое принятие JSPI в качестве стандартного API для экосистем JavaScript и WebAssembly.

Нам интересно узнать ваше мнение об этих изменениях в JSPI! Присоединяйтесь к обсуждению в репозитории сообщества W3C WebAssembly.