По просьбам трудящихся, пишу о внутреннем устройстве WebAssembly. WebAssembly — байткод для стековой виртуальной машины. Значит, для запуска кода такой нужны интерпретатор, стек и хранилище кода. Если мы хотим взаимодействовать с внешним миром, нужен интерфейс к внешней машине, хосту. Дополнительно стандарт определяет две структуры: непрерывную память и таблицы. В версии MVP стандарта их может быть по одной штуке каждого, или не быть вовсе. В итоге, наш технобордель выглядит так: Займёмся делом! Есть два базовых вида машинных архитектур: стековая и регистровая. Нас интересует стековая.  Все операции стековой машины, очевидно, работают со стеком переменных. У виртуальной машины WebAssembly две особенности: Остальные принципы работы машины обычны: прежде, чем выполнить операцию, операнды нужно положить в стек. Операция забирает аргументы из стека, выполняется и кладёт результат в стек. Когда вы вызываете функцию из WebAssembly, интерпретатор: В стандарте всего 4 типа переменных: Не определена ни знаковость, ни типы 16-битных и 8-битных значений. Всё это отдано на откуп компилятору исходного языка. В случае в C/C++/Rust локальные переменные будут расширены до 32-битных значений. Указатели представляются как i32, что позволяет адресовать только 4гб памяти. Версия wasm64 с 64-битными указателями в разработке. Посмотрим на примере: Синтаксис подсвечен для ocaml, который построен на таких же S-выражениях, как и текстовый формат WebAssembly. Вдруг такой грязный хак кому пригодится. Как видно, компилятор создаёт довольно много ненужного кода по управлению локальными переменными. Для устранения этого, совсем недавно в binaryen добавлен проход оптимизатора. Примеры привожу не в рукописном виде S-выражений, а в виде декомпиляции бинарного формата с помощью WABT. Так проще для понимания с непривычки, ИМХО. Код в WebAssembly это команды для виртуальной машины, управляющие положением дел на стеке. Традиционно они называются опкодами (от operation code). Стандартный набор кодов меньше 255, потому при бинарном кодировании такие коды занимают один байт. Наборы расширений (SIMD, потоки, исключения) определяют свои наборы опкодов, для которых используются однобайтные префиксы. То есть, опкод расширения занимает два байта. Опкодами представлены все основные операции: арифметика, преобразования типов, работа с локальными переменными и памятью, вызовы функций, управление потоком исполнения. Опкоды сгруппированы в функции. У каждой функции есть порядковый номер и сигнатура: список параметров и возвращаемых значений с указанием типа. На основе сигнатуры определяется, сколько значений нужно взять из стека, и сколько вернуть. Функции вызываются опкодами call и call_indirect. Cперва на стек нужно положить аргументы, а в результате аргументы будут сняты, а результат покладен добавлен на стек. В традиционном ассемблере циклы и условные переходы организуются с помощью аналогов goto. Но в WebAssembly из ненависти к goto пошли другим путём. Внутри функции код организован в блоки. Всего блоков три вида: К блокам есть команды перехода: Параметр команды перехода — блок, который она прерывает. Блок указывается по глубине вложенности: 0 — последний открытый блок, 1 — сразу перед последним открытым. Блоки служат ограничителями стека и могут возвращать значения. Внутри блока нельзя снять со стека аргументы, которые были на стеке во время входа в блок. Если возврат значения блоком не определён, размер стека на входе в блок будет соответствовать размеру на выходе из блока. Возвращаемые значения будут добавлены на стек сверху. Система вышла довольно непривычной и для тех, кто привык к традиционным if-else-end и while, и для тех, кто привык работать с goto. Выглядит это так: С переходом по таблице чуть сложнее: Для понимания, на C это выглядит так: Далее, циклы выглядят так: Хоть ты тресни, но конкретно в коде для исполнения все увиденные мной интерпретаторы преобразуют все контрольные инструкции в аналоги goto ещё на этапе чтения. Так быстрее и привычнее для машинного кода. Из каких соображений для байткода был выбран именно такой формат управляющих конструкций? Неужели только для красивого ручного написания? Сложные операции в WebAssembly выполняются через непрерывную память. Для WebAssembly из памяти хоста выделяются непрерывные блоки памяти страницами по 64 килобайта. По требованиям стандарта, блоки должны выделяться только для специальных нужд, в них не должны храниться ни стек, ни таблицы, ни внешние ресурсы. Адресация внутри памяти строго контролируется, получение или запись по адресу вне блока считаются ошибкой. Таким образом, создаётся песочница для безопасной работы с памятью. Чтобы зловреды на wasm не имели доступа к памяти хоста. Поскольку стек ограничен базовыми типами данных, все операции над сложными и составными типами выполняются через память. В этой же памяти хранятся статические и глобальные переменные, строковые литералы. Изнутри с памятью работают опкоды вида TYPE.storeN и TYPE.loadN. Первый записывает переменную или её часть в память по заданному смещению, второй — загружает по заданному смещению. Память — единственный способ передавать достаточно большие объекты составных типов между WebAssembly и хостом. На хосте память обычно представлена массивом байт. Указатели на память — переменные типа i32, хранящие смещение внутри этого массива. WebAssembly может запросить больше памяти операцией grow_memory. В таком случае, обычно, выделяется новый массив с новым размером, старые данные копируются в новый массив. Имея знания об устройстве памяти, можно, наконец, написать самую известную программу: Таблица — непрерывный контейнер для элементов определённого типа (одного из базовых типов WebAssembly). Таблицы, как и память, могут использоваться для связи с хостом. В отличии от памяти, таблицы строго типизированы. С точки зрения дизайна, на базе таблиц могут быть сделаны списки ссылок для сборки мусора, списки внешних ресурсов вроде дескрипторов файлов и сокетов. Сейчас таблица допускается только одна и используется она со строго определённой целью: для работы call_indirect. call_indirect, он же непрямой или виртуальный вызов — инструмент для вызова кода, имя которому не может быть дано при компиляции. Показать это можно на примере виртуальных функций в C++. При компиляции мы определяем смещение функции в таблице виртуальных функций, а саму таблицу можно определить во время работы программы. call_indirect используется и для вызовов указателей на функции. Таким способом можно организовать вызов указателя на функцию хоста, которая не была импортирована в модуль напрямую: поместить эту функцию в таблицу во время исполнения кода и использовать call_indirect. Разбираться проще на примере: Глобальные переменные это как локальные, только для всех. В модуле есть список глобальных переменных, получить доступ к которым может любая функция в модуле с помощью опкодов get_global и set_global. Глобальные переменные бывают изменяемыми и постоянными. Тип глобальных переменных, как и локальных, ограничен базовыми. Если нужно хранить нечто более сложное — используется память, а переменная будет указателем на неё. Основное назначение глобальных переменных — передача глобального состояния хосту и от хоста. Если вы используете глобальную переменную при компиляции своего модуля, компилятор, скорее всего, разместит её в линейной памяти и не будет создавать глобальный объект. Мне вообще не удалось заставить LLVM создать глобальную переменную внутри модуля, только импортируемую. С одной стороны, это понятно, и глобальные переменные в общем смысле — глобальное зло. Но если есть механизм — должен быть и способ его использовать. А то ж*па есть, а слова нет. Ну, поехали... Высшая форма организации WebAssembly — модуль. WebAssembly распространяется, загружается и исполняется в виде модулей.  Всего у нас набралось 4 типа объектов в WebAssembly: функции, память, таблицы и глобальные переменные. Каждый из этих объектов может быть импортирован и экспортирован (и даже одновременно импортирован и экспортирован). Модуль содержит все определённые в нём объекты, и описывает структуру импортов и экспортов. Как можно было заметить, все объекты (и ещё сигнатуры, которые type) имеют свои порядковые номера. Пространство номеров у каждого типа своё. То есть, функция 2 не имеет никакого отношения ни к глобальной переменной 2, ни к сигнатуре 2. Пространство номеров обычно начинается с импортированных объектов. С точки зрения кода, вызов функции хоста или получение глобальной переменной хоста не отличаются от обычных: так же call N или get_global N. Интерпретатор на основе метаданных модуля сам определит, нужно ли вызвать функцию хоста, или даже функцию другого модуля, этим хостом загруженного. С памятью и таблицами всё сложнее. При совместном использовании несколькими модулями одной памяти или одной таблицы нужно определиться с регионами, в которых каждый из модулей хранит свои глобальные данные. По этому поводу есть рекомендация, но пока она слабо поддерживается компиляторами. В итоге, для общения между модулем и хостом: С основными внутренностями я закончил. Практически к каждому пункту можно написать огромный список дополнений и особенностей компиляции, но это уже частные случаи. Как видно, хоть технология и называется WebAssembly, внутри к вебу никаких привязок нет. Для компиляции и декомпиляции примеров использовался пакет WABT и LLVM 5.0. Emscripten я не использую, ибо он, как раз, создаёт кучу всего, предназначенного исключительно для веб-модулей, запускаемых совместно с JavaScript. Далее будем рассуждать, чего нужно в стандартной библиотеке, как распределять память, и как запускать весь этот бедлам на мобильных платформах. P.S. Хотелось написать нечто для фронтэндеров, которые хотят разобраться с сей технологией. Не уверен, что получилось. Если вы — фронтендер, и вам что-то не понятно — пишите в комментарии смело, буду исправляться.