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

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

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

Вы когда-нибудь программировали на ассемблере? В компьютерном программировании язык ассемблера, часто называемый просто ассемблером и обычно сокращаемый как 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 — это низкоуровневый ассемблерный язык с компактным двоичным форматом, который работает с производительностью, близкой к исходной, и предоставляет такие языки, как C/C++ и Rust, а также многие другие, с целью компиляции, чтобы они могли работать в Интернете. Поддержка языков с управлением памятью, таких как Java и Dart, находится в разработке и должна скоро стать доступной или уже реализована, как в случае с Kotlin/Wasm . WebAssembly предназначен для работы вместе с JavaScript, что позволяет им работать вместе.

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

Код WebAssembly (двоичный код, то есть байт-код) предназначен для запуска на портативной машине виртуального стека (VM). Байт-код спроектирован так, чтобы анализировать и выполнять его быстрее, чем 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 требовалась новая функциональность, с которой согласились все производители браузеров. Анонсированный в 2015 году и впервые выпущенный в марте 2017 года, WebAssembly стал рекомендацией 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 с веб-сервера, вы увидите "Hello World" напечатанную на консоли DevTools.

Также есть способ скомпилировать в 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 , где эксперты часто рады помочь, если вы вежливо попросите.

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

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