Введение в WebAssembly: как устроена технология и почему она важна

WebAssembly

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

WebAssembly или wasm — это низкоуровневый формат байт-кода для клиентских скриптов на стороне браузера. Если вы пишете компилятор для языка программирования, один из вариантов — выбрать готовую платформу, например JVM или .NET, и компилировать ваш язык в её байт-код. WebAssembly занимает ту же роль, поэтому при компиляции в WebAssembly вы делаете свою программу доступной для всех платформ, на которых поддерживается wasm, другими словами, для всех браузеров.

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

Разработка MVP была завершена в марте 2017, и сейчас есть готовые реализации для всех основных браузеров.

Почему это важно?

Во-первых, новый формат WebAssembly обещает значительное увеличение производительности парсинга. Как сказано в FAQ WebAssembly, тип бинарного формата, используемый в WebAssembly, может быть декодирован гораздо быстрее, чем JavaScript может быть пропарсен (эксперименты показывают более чем 20-кратную разницу). На мобильных устройствах большому скомпилированному коду может запросто потребоваться 20–40 секунд только на парсинг, поэтому встроенное декодирование (особенно в сочетании с такими технологиями, как улучшенная по сравнению с gzip потоковая подача для сжатия) имеет решающее значение для обеспечения хорошего пользовательского опыта.

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

Первыми пользователями wasm, вероятно, будут разработчики игровых движков, поскольку они всё время ищут, как бы улучшить производительность. До WebAssembly лучшим, на что они могли надеяться, был asm.js (упрощённый JavaScript, оптимизированный для скорости), который был хорошей технологией, но не очень полезной для многих игр.

В сущности, Autodesk планирует добавить поддержку WebAssembly для своего игрового движка Stingray, а Unity Technologies, создатели игрового движка Unity, начали экспериментировать с WebAssembly ещё в 2015 году. Разработчики Rust также работают над поддержкой WebAssembly для запуска кода Rust в вебе.

Прим.перев. На самом деле, Rust уже умеет компилировать в wasm напрямую, без Emscipten. Кроме того, с момента написания статьи произошли изменения: Autodesk сообщила о прекращении разработки и продажи Stingray.

Зачем это вам

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

Конечно, вы можете использовать лучшие версии JavaScript, такие как TypeScript или даже новые языки вроде Kotlin. Но, в конце концов, все они должны быть скомпилированы в JavaScript. В свою очередь это создало проблемы для разработчиков языка JavaScript, которые должны поддерживать практически все возможные сценарии и все стили программирования. WebAssembly изменит это и позволит всем сосредоточиться на том, что у них получается лучше всего.

Это ещё не всё: WebAssembly можно будет переносить на другие платформы. Это означает, что, если вы пишете программное обеспечение на языке, который компилируется в WebAssembly, вы сможете запустить его на .NET. Тот факт, что wasm позволяет повторно использовать уже существующую инфраструктуру JavaScript в вебе, означает, что вы уже можете использовать его для разработки.

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

Например, вы можете создать компилятор WebAssembly для DSL, который вы используете внутри вашей компании, и запустить его в сети на стороне клиента без использования настраиваемых плагинов, таких как Oracle Java Plug-in или Adobe Flash.

Как это работает

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

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

(func (param i32) (local f64)
  get_local 0
  get_local 1)

Однако инструменты, вероятно, покажут нечто более похожее на это (пример из документации):

C++BINARYTEXT
int factorial(int n) {
  if (n == 0)
    return 1;
  else
    return n * factorial(n-1);
}
20 00
42 00
51
04 7e
42 01
05
20 00
20 00
42 01
7d
10 00
7e
0b
get_local 0
i64.const 0
i64.eq
if i64
    i64.const 1
else
    get_local 0
    get_local 0
    i64.const 1
    i64.sub
    call 0
    i64.mul
end

Если вас интересует, почему в качестве примера используется C++, то это из-за того, что целью начального выпуска (MVP) WebAssembly была поддержка C/C++. Поддержка других языков будет позже; в данный момент они находятся в разработке. Этот выбор был сделан по нескольким техническим и практическим причинам:

  • MVP WebAssembly не поддерживает сборку мусора (в разработке);
  • Реализация компилятора C/C++ в WebAssembly может полагаться на проверенные временем инструменты вроде LLVM (один из наиболее используемых наборов инструментов для компилятора).

Разработчики WebAssembly используют LLVM для сокращения объёма работы, необходимой для получения рабочего продукта. Кроме того, это позволило им легко интегрироваться с другими инструментами, которые работают с LLVM, такими как Emscripten.

Наличие MVP даёт обычным разработчикам возможность тестировать и использовать WebAssebmly, что позволяет соответственно его улучшать.

Инструменты WebAssembly

На данный момент официальные инструменты WebAssembly могут компилировать только C/C++ в WebAssembly, хотя другие разработчики уже начали работу по добавлению поддержки других языков и платформ (ранее мы упоминали .NET и Rust). Однако даже с помощью официальных инструментов вы уже можете использовать его для веб-разработки двумя способами:

  • Записывать WebAssembly в текстовом формате и преобразовывать его в двоичный файл с использованием предоставленных инструментов;
  • Использовать сторонний инструмент, основанный на этих инструментах.

Первый вариант не очень практичен для общего использования, но он подойдёт, если вы хотите получить представление о формате или начать работу по его интеграции в свои собственные инструменты. В настоящее время есть два набора инструментов: WebAssembly Binary Toolkit и Binaryen.

WABT включает в себя инструменты для разработки и/или использования в инструментах, предназначенных для работы с WebAssembly:

  • Он отлично поддерживает спецификацию формата;
  • Он может конвертировать в текстовый формат и из него;
  • Он включает в себя интерпретатор.

Иными словами, он обеспечивает чистый и лёгкий доступ к данным в формате WebAssembly, чтобы вы могли с ними работать.

С другой стороны, Binaryen — это мощный набор инструментов, предназначенный для использования в инфраструктуре компилятора:

  • Он может работать с кодом WebAssembly или графом потока управления, предназначенным для компиляторов;
  • Он оптимизирует код многими способами, с использованием как стандартных техник оптимизации, так и специально рассчитанных на WebAssembly;
  • Он может компилировать из и в asm.js, Rust MIR (промежуточный язык для Rust) и LLVM.

Таким образом, этот набор инструментов готов к интеграции в ваш бэкенд.

По сути, оба позволяют создавать инструменты, которые управляют WebAssembly, но WABT предназначен для инструментов, которые используются во время разработки (например, статический анализ), в то время как Binaryen предназначен для поддержки WebAssembly в компиляторах.

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

Если вам нужно создать такие инструменты, есть ещё и второй вариант — написать всё с нуля. Этот вариант подойдёт, если вам нужен собственный компилятор или интерпретатор для вашего собственного языка, нужна лучшая производительность или лёгкий инструмент. В этом деле вам может помочь библиотека WasmCompilerKit. Это Kotlin-библиотека, которую вы можете использовать для загрузки WASM-файлов, их изменения и создания. Учитывая то, что она написана на Kotlin, вы можете использовать её также с Java и Scala.

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

Если вы просто разработчик, заинтересованный в использовании WebAssembly, то для знакомства с ним вы можете использовать Emscripten (SDK). Emscripten — это набор инструментов, который уже используется для компиляции C/C++ в asm.js. С Emscripten вам будет легче использовать ранее упомянутый Binaryen и интегрировать его со своим собственным набором инструментов.

После установки Emscripten или его компиляции из исходного кода необходимо установить binaryen:

# эти команды должны выполняться внутри папки emsdk
# для Linux или Mac OS X
# этот процесс может занять какое-то время
./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit
./emsdk activate --global --build=Release sdk-incoming-64bit binaryen-master-64bit
 
# для Windows
# этот процесс может занять какое-то время
# если вы используете Visual Studio 2017, добавьте --vs2017
emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit
emsdk activate --global --build=Release sdk-incoming-64bit binaryen-master-64bit

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

# команда должна выполняться внутри папки emsdkr
# для Linux или Mac OS X
source ./emsdk_env.sh --build=Release
 
# для Windows
emsdk_env.bat --build=Release

Наконец, вы можете писать ваш код:

//WebAssemblyExample.c
#include <stdio.h>
 
int factorial(int n) {
  if (n == 0)
    return 1;
  else
    return n * factorial(n-1);
}
 
int main(int argc, char ** argv) {
  int number = 5;
  int fact = factorial(number);
  printf("The factorial of %d is %d", number, fact);
}

И затем скомпилировать в WebAssembly и увидеть результат в браузере:

emcc WebAssemblyExample.c -s WASM=1 -o WebAssemblyExample.html
# запускаем локальный веб-сервер, включенный в emscripten
emrun --no_browser --port 8080 

Первая команда создаст три файла: модуль WASM, HTML-файл, который показывает код в действии, и JS-файл, который запускает модуль и отвечает за всё, что нужно для его запуска. Параметр WASM=1 говорит Emscripten, что мы хотим сгенерировать модуль WASM вместо asm.js-файла.

WebAssembly

Вы также можете вывести свой код в настраиваемый шаблон, используя флаг --shell-file. Emscripten SDK содержит базовый шаблон в этом месте: (папка с esmdk)/emscripten/incoming/src/shell_minimal.html. Скопируйте этот файл в свой проект и адаптируйте его под свои нужды (например, добавьте остальную часть вашего JS-кода).

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

Конечная цель состоит в том, чтобы подключать модуль WebAssembly так же легко, как подключается код JavaScript, с помощью HTML-тега <script type = "module">, но это ещё только впереди.

Взаимодействие между C и JavaScript

Взаимодействие между C и JavaScript — проблема для Emscripten. Первое, что вам нужно сделать — это подключить заголовочный файл emscripten:

#include <emscripten.h>

Самый простой способ вызвать JavaScript — использовать функцию emscripten_run_script:

// эквивалент вызова eval() в JavaScript
emscripten_run_script("alert('hello')");

Вызвать C-функцию из JavaScript немного сложнее. Во-первых, вы должны сделать её доступной из кода C/C++, потому что по умолчанию Emscripten делает недоступными все функции C, кроме основной (main). Необходимо добавить модификатор EMSCRIPTEN_KEEP_ALIVE ко всем функциям, которые вы хотите использовать в JavaScript:

int EMSCRIPTEN_KEEPALIVE factorial(int n) {
  if (n == 0)
    return 1;
  else
    return n * factorial(n-1);
}

Если вы пишете на C++, не забудьте поместить любую функцию, которую вы хотите сделать доступной, внутри блока extern "C", чтобы C++ не задекорировал имя функции (это то, что делает C++, а не баг WebAssembly или Emscripten).

Во-вторых, вам необходимо скомпилировать модуль WebAssembly с параметром NO_EXIT_RUNTIME, чтобы избежать остановки среды выполнения при выходе из основной функции, что сделало бы невозможным вызов кода C из JavaScript:

emcc -o WebAssemblyExample.html WebAssemblyExample.c -O3 -s WASM=1 -s NO_EXIT_RUNTIME=1 --shell-file WebAssemblyTemplate.html

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

Module.ccall('factorial', // имя C-функции
             'number', // возвращаемый тип
             ['number'], // типы аргументов
             [4] // аргументы
);

Есть три типа на выбор: number, string и array.

Если вам нужно использовать функцию в JS-коде несколько раз, вы можете обернуть её с помощью функции cwrap:

factorial = Module.cwrap('factorial', 'number', ['number'])
factorial(4);

Вы можете легко проверить, как это работает, в консоли:

WebAssembly

Подводим итоги

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

Его разработка поддерживается людьми из Mozilla, Microsoft, Google и Apple. Внимание к WebAssembly — ещё одно доказательство его важности.

Вы можете использовать WebAssembly уже сегодня и посмотреть подробную документацию на сайте MDN. Если вас интересует более подробная информация о формате, вы можете прочитать её на официальном сайте. Вы также можете посмотреть документацию Emscripten, чтобы понять детали, связанные с взаимодействием между JavaScript и C/C++.

Перевод статьи «Introduction to WebAssembly: why should we care?»

Вакансии в тему:

Лого компании «Клиника DOC+»
Full Stack Developer
Full Stack Developer
Клиника DOC+, Москва, до 150 000 ₽