Что такое WebAssembly и откуда он взялся?

С тех пор, как интернет стал платформой не только для документов, но и для приложений, некоторые из самых продвинутых приложений выжимают максимум из веб-браузеров. Подход «ближе к железу» путём взаимодействия с низкоуровневыми языками для повышения производительности встречается во многих языках высокого уровня. Например, в Java есть Java Native Interface . Для JavaScript таким низкоуровневым языком является WebAssembly. В этой статье вы узнаете, что такое язык ассемблера и почему он может быть полезен в вебе, а также узнаете, как WebAssembly был создан с помощью промежуточного решения asm.js.

язык ассемблера

Вы когда-нибудь программировали на языке ассемблера? В компьютерном программировании язык ассемблера, часто называемый просто «Assembly» или сокращённо «ASM» или «asm», — это любой низкоуровневый язык программирования с очень строгим соответствием между инструкциями языка и инструкциями машинного кода архитектуры.

Например, если обратиться к архитектурам Intel® 64 и IA-32 (PDF), инструкция MUL (для умножения ) выполняет беззнаковое умножение первого операнда (операнда назначения) на второй операнд (операнд источника) и сохраняет результат в операнде назначения. Если очень упростить, операнд назначения — это неявный операнд, расположенный в регистре AX , а операнд источника — в регистре общего назначения, например, CX . Результат снова сохраняется в регистре AX . Рассмотрим следующий пример кода для x86:

mov ax, 5  ; Set the value of register AX to 5.
mov cx, 10 ; Set the value of register CX to 10.
mul cx     ; Multiply the value of register AX (5)
           ; and the value of register CX (10), and
           ; store the result in register AX.

Для сравнения, если бы вам поставили задачу умножить 5 и 10, вы, вероятно, написали бы на JavaScript код, подобный следующему:

const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;

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

Как следует из названия, код x86 зависит от архитектуры x86. Что, если бы существовал способ написания ассемблерного кода, не зависящего от конкретной архитектуры, но при этом унаследовавшего преимущества ассемблера в производительности?

asm.js

Первым шагом к написанию ассемблерного кода без архитектурных зависимостей стал asm.js — строгое подмножество JavaScript, которое можно было использовать в качестве низкоуровневого, эффективного целевого языка для компиляторов. Этот подъязык фактически описывал изолированную виртуальную машину для языков, небезопасных для памяти, таких как C или C++. Сочетание статической и динамической валидации позволяло движкам JavaScript применять стратегию оптимизации компиляции с опережением времени (AOT) для корректного asm.js-кода. Код, написанный на языках со статической типизацией и ручным управлением памятью (например, C), транслировался компилятором «из исходного кода в исходный», таким как ранний Emscripten (основанный на LLVM).

Производительность была улучшена за счёт ограничения возможностей языка теми, которые поддерживаются AOT. Firefox 22 стал первым браузером с поддержкой asm.js , выпущенным под названием OdinMonkey . В Chrome поддержка asm.js появилась в версии 61. Хотя asm.js всё ещё работает в браузерах, он был заменён WebAssembly. На данном этапе asm.js используется как альтернатива для браузеров, не поддерживающих WebAssembly.

WebAssembly

WebAssembly — это низкоуровневый язык программирования, подобный ассемблеру, с компактным двоичным форматом, который работает с производительностью, близкой к нативной, и предоставляет таким языкам, как C/C++ и Rust, а также многим другим, возможность компиляции для работы в вебе. Поддержка языков с управляемой памятью, таких как Java и Dart, находится в разработке и должна стать доступной в ближайшее время, или уже реализована, как в случае с Kotlin/Wasm . WebAssembly разработан для работы совместно с JavaScript, что позволяет им работать вместе.

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

Код WebAssembly (двоичный код, то есть байт-код) предназначен для запуска на портативной виртуальной стековой машине (ВМ). Байт-код разработан для более быстрого анализа и выполнения, чем JavaScript, и имеет компактное представление.

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

  • Управляющие инструкции , которые формируют управляющие конструкции и извлекают значения своих аргументов из стека, могут изменять счетчик программ и помещать результирующие значения в стек.
  • Простые инструкции , которые извлекают значения своих аргументов из стека, применяют оператор к значениям, а затем помещают результирующие значения в стек, после чего следует неявное увеличение счетчика команд.

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

i32.const 5  ; Push the integer value 5 onto the stack.
i32.const 10 ; Push the integer value 10 onto the stack.
i32.mul      ; Pop the two most recent items on the stack,
             ; multiply them, and push the result onto the stack.

В то время как asm.js полностью реализован программно, то есть его код может выполняться в любом JavaScript-движке (даже неоптимизированном), WebAssembly требовал новой функциональности, с которой согласились все производители браузеров. WebAssembly , анонсированный в 2015 году и впервые выпущенный в марте 2017 года, стал рекомендацией W3C 5 декабря 2019 года. W3C поддерживает этот стандарт, опираясь на вклад всех основных производителей браузеров и других заинтересованных сторон. С 2017 года он поддерживается всеми браузерами.

WebAssembly имеет два представления: текстовое и двоичное . Выше вы видите текстовое представление.

Текстовое представление

Текстовое представление основано на S-выражениях и обычно использует расширение файла .wat (формат текста Web Assembly). При желании его можно написать вручную. Взяв за основу пример с умножением, представленный выше, и сделав его более полезным, отказавшись от жёсткого кодирования множителей, вы, вероятно, сможете понять следующий код:

(module
  (func $mul (param $factor1 i32) (param $factor2 i32) (result i32)
    local.get $factor1
    local.get $factor2
    i32.mul)
  (export "mul" (func $mul))
)

Двоичное представление

Двоичный формат с расширением .wasm не предназначен для использования человеком, не говоря уже о его создании. С помощью инструмента, например, wat2wasm , вы можете преобразовать приведённый выше код в следующее двоичное представление. (Комментарии обычно не являются частью двоичного представления, но добавляются инструментом wat2wasm для лучшей понятности.)

0000000: 0061 736d                             ; WASM_BINARY_MAGIC
0000004: 0100 0000                             ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                    ; section code
0000009: 00                                    ; section size (guess)
000000a: 01                                    ; num types
; func type 0
000000b: 60                                    ; func
000000c: 02                                    ; num params
000000d: 7f                                    ; i32
000000e: 7f                                    ; i32
000000f: 01                                    ; num results
0000010: 7f                                    ; i32
0000009: 07                                    ; FIXUP section size
; section "Function" (3)
0000011: 03                                    ; section code
0000012: 00                                    ; section size (guess)
0000013: 01                                    ; num functions
0000014: 00                                    ; function 0 signature index
0000012: 02                                    ; FIXUP section size
; section "Export" (7)
0000015: 07                                    ; section code
0000016: 00                                    ; section size (guess)
0000017: 01                                    ; num exports
0000018: 03                                    ; string length
0000019: 6d75 6c                          mul  ; export name
000001c: 00                                    ; export kind
000001d: 00                                    ; export func index
0000016: 07                                    ; FIXUP section size
; section "Code" (10)
000001e: 0a                                    ; section code
000001f: 00                                    ; section size (guess)
0000020: 01                                    ; num functions
; function body 0
0000021: 00                                    ; func body size (guess)
0000022: 00                                    ; local decl count
0000023: 20                                    ; local.get
0000024: 00                                    ; local index
0000025: 20                                    ; local.get
0000026: 01                                    ; local index
0000027: 6c                                    ; i32.mul
0000028: 0b                                    ; end
0000021: 07                                    ; FIXUP func body size
000001f: 09                                    ; FIXUP section size
; section "name"
0000029: 00                                    ; section code
000002a: 00                                    ; section size (guess)
000002b: 04                                    ; string length
000002c: 6e61 6d65                       name  ; custom section name
0000030: 01                                    ; name subsection type
0000031: 00                                    ; subsection size (guess)
0000032: 01                                    ; num names
0000033: 00                                    ; elem index
0000034: 03                                    ; string length
0000035: 6d75 6c                          mul  ; elem name 0
0000031: 06                                    ; FIXUP subsection size
0000038: 02                                    ; local name type
0000039: 00                                    ; subsection size (guess)
000003a: 01                                    ; num functions
000003b: 00                                    ; function index
000003c: 02                                    ; num locals
000003d: 00                                    ; local index
000003e: 07                                    ; string length
000003f: 6661 6374 6f72 31            factor1  ; local name 0
0000046: 01                                    ; local index
0000047: 07                                    ; string length
0000048: 6661 6374 6f72 32            factor2  ; local name 1
0000039: 15                                    ; FIXUP subsection size
000002a: 24                                    ; FIXUP section size

Компиляция в WebAssembly

Как видите, ни .wat , ни .wasm не слишком удобны для восприятия человеком. Именно здесь в игру вступает компилятор вроде Emscripten . Он позволяет компилировать код из высокоуровневых языков, таких как C и C++. Существуют и другие компиляторы для других языков, например, Rust и многих других. Рассмотрим следующий код на языке C:

#include <stdio.h>

int main() {
  printf("Hello World\n");
  return 0;
}

Обычно эту программу на языке C можно скомпилировать с помощью компилятора gcc .

$ gcc hello.c -o hello

Установив Emscripten , вы компилируете его в WebAssembly, используя команду emcc и почти те же аргументы:

$ emcc hello.c -o hello.html

Это создаст файл hello.wasm и HTML-файл-оболочку hello.html . При загрузке файла hello.html с веб-сервера в консоли DevTools будет выведено сообщение "Hello World" .

Также есть способ компилировать в WebAssembly без HTML-оболочки:

$ emcc hello.c -o hello.js

Как и раньше, будет создан файл hello.wasm , но на этот раз вместо HTML-обёртки — файл hello.js . Для тестирования запустите полученный JavaScript-файл hello.js , например, с Node.js:

$ node hello.js
Hello World

Узнать больше

Это краткое введение в WebAssembly — лишь вершина айсберга. Узнайте больше о WebAssembly в документации WebAssembly на MDN и ознакомьтесь с документацией Emscripten . По правде говоря, работа с WebAssembly может немного напоминать мем «Как нарисовать сову» , особенно учитывая, что веб-разработчики, знакомые с HTML, CSS и JavaScript, не всегда разбираются в компилируемых языках, таких как C. К счастью, существуют каналы, такие как тег webassembly на StackOverflow, где эксперты часто готовы помочь, если вы вежливо попросите.

Благодарности

Эту статью рецензировали Якоб Куммероу , Дерек Шуфф и Рэйчел Эндрю .