Обнаружение функций WebAssembly

Узнайте, как использовать новейшие функции WebAssembly, поддерживая пользователей во всех браузерах.

WebAssembly 1.0 был выпущен четыре года назад, но разработка на этом не остановилась. Новые функции добавляются в процессе стандартизации предложений . Как это обычно бывает с новыми функциями в Интернете, порядок и сроки их реализации могут существенно различаться в разных движках. Если вы хотите использовать эти новые функции, вам необходимо убедиться, что ни один из ваших пользователей не остался в стороне. В этой статье вы узнаете, как этого добиться.

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

Вы можете найти полный список предложений и их соответствующие этапы в официальном репозитории или отслеживать статус их реализации в движках на официальной странице дорожной карты функций .

Чтобы гарантировать, что пользователи всех браузеров смогут использовать ваше приложение, вам необходимо выяснить, какие функции вы хотите использовать. Затем разделите их на группы в зависимости от поддержки браузера. Затем скомпилируйте свою кодовую базу отдельно для каждой из этих групп. Наконец, на стороне браузера вам необходимо обнаружить поддерживаемые функции и загрузить соответствующий пакет JavaScript и Wasm.

Выбор и группировка функций

Давайте пройдемся по этим шагам, выбрав в качестве примера произвольный набор функций. Допустим, я определил, что хочу использовать SIMD, потоки и обработку исключений в своей библиотеке из соображений размера и производительности. Их поддержка браузера следующая:

Таблица, показывающая поддержку выбранных функций браузером.
Просмотрите эту таблицу функций на веб-сайте webassembly.org/roadmap .

Вы можете разделить браузеры на следующие группы, чтобы обеспечить максимально оптимизированный интерфейс для каждого пользователя:

  • Браузеры на базе Chrome: поддерживаются потоки, SIMD и обработка исключений.
  • Firefox: поддерживаются потоки и SIMD, обработка исключений — нет.
  • Safari: потоки поддерживаются, SIMD и обработка исключений — нет.
  • Другие браузеры: предполагается только базовая поддержка WebAssembly.

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

Компиляция для разных наборов функций

В WebAssembly нет встроенного способа обнаружения поддерживаемых функций во время выполнения, поэтому все инструкции в модуле должны поддерживаться на целевом объекте. По этой причине вам необходимо скомпилировать исходный код в Wasm отдельно для каждого из этих различных наборов функций.

Каждая цепочка инструментов и система сборки различны, и вам нужно будет обратиться к документации вашего собственного компилятора, чтобы узнать, как настроить эти функции. Для простоты в следующем примере я буду использовать однофайловую библиотеку C++ и покажу, как ее скомпилировать с помощью Emscripten.

Я буду использовать SIMD через эмуляцию SSE2 , потоки через поддержку библиотеки Pthreads и выбирать между обработкой исключений Wasm и резервной реализацией JavaScript :

# First bundle: threads + SIMD + Wasm exceptions
$ emcc main.cpp -o main.threads-simd-exceptions.mjs -pthread -msimd128 -msse2 -fwasm-exceptions
# Second bundle: threads + SIMD + JS exceptions fallback
$ emcc main.cpp -o main.threads-simd.mjs -pthread -msimd128 -msse2 -fexceptions
# Third bundle: threads + JS exception fallback
$ emcc main.cpp -o main.threads.mjs -pthread -fexceptions
# Fourth bundle: basic Wasm with JS exceptions fallback
$ emcc main.cpp -o main.basic.mjs -fexceptions

Сам код C++ может использовать #ifdef __EMSCRIPTEN_PTHREADS__ и #ifdef __SSE2__ для условного выбора между параллельными (потоки и SIMD) реализациями одних и тех же функций и последовательными реализациями во время компиляции. Это будет выглядеть так:

void process_data(std::vector<int>& some_input) {
#ifdef __EMSCRIPTEN_PTHREADS__
#ifdef __SSE2__
  // …implementation using threads and SIMD for max speed
#else
  // …implementation using threads but not SIMD
#endif
#else
  // …fallback implementation for browsers without those features
#endif
}

Для обработки исключений не нужны директивы #ifdef , поскольку их можно использовать в C++ таким же образом, независимо от базовой реализации, выбранной с помощью флагов компиляции.

Загрузка правильного пакета

После того как вы создали пакеты для всех групп функций, вам необходимо загрузить правильный пакет из основного приложения JavaScript. Для этого сначала определите, какие функции поддерживаются в текущем браузере. Вы можете сделать это с помощью библиотеки wasm-feature-detect . Объединив его с динамическим импортом , вы сможете загрузить наиболее оптимизированный пакет в любом браузере:

import { simd, threads, exceptions } from 'https://unpkg.com/wasm-feature-detect?module';

let initModule;
if (await threads()) {
  if (await simd()) {
    if (await exceptions()) {
      initModule = import('./main.threads-simd-exceptions.mjs');
    } else {
      initModule = import('./main.threads-simd.mjs');
    }
  } else {
    initModule = import('./main.threads.mjs');
  }
} else {
  initModule = import('./main.basic.mjs');
}

const Module = await initModule();
// now you can use `Module` Emscripten object like you normally would

Заключительные слова

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

По мере роста количества функций количество когорт функций может стать неприемлемым. Чтобы облегчить эту проблему, вы можете выбрать когорты функций на основе ваших реальных пользовательских данных, пропустить менее популярные браузеры и позволить им вернуться к чуть менее оптимальным когортам. Пока ваше приложение по-прежнему работает для всех пользователей, этот подход может обеспечить разумный баланс между прогрессивным улучшением и производительностью во время выполнения.

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