# Расширение возможностей веб-сервера при помощи WASM на примере Angie

*09.04.2025*

Комплексный обзор интеграции WebAssembly в веб-сервер Angie: архитектура
WASM-расширений, системное окружение, взаимодействие с хост-модулем
и практические примеры использования изолированного выполнения кода.

Наверное не секрет, что одна из причин популярности [nginx](http://nginx.org/)
- это развитая экосистема [сторонних модулей](https://angie.software/angie/docs/installation/oss_packages/#install-dynamicmodules-oss).
Модули позволяют не просто настраивать какие-то детали обработки запроса, но и
глубоко изменять поведение сервера.

Помимо модулей, которые решают конкретные задачи, существуют модули, которые
добавляют поддержку расширений на различных языках программирования: [perl](https://angie.software/angie/docs/configuration/modules/http/http_perl/), [lua](https://github.com/openresty/lua-nginx-module), [javascript](https://angie.software/angie/docs/configuration/modules/http/http_js/#http-js)
и [других](https://github.com/arut/nginx-python-module).

Теперь в этот набор добавляется ещё и модуль [WASM](https://angie.software/news/articles/wasm), который мы разработали для нашего
веб-сервера [Angie](https://angie.software/angie/) (здесь и далее ссылки на
нашу документацию). Зачем понадобился WASM на сервере, чем нас не устраивают
существующие методы расширения и что в итоге получилось?

Зачем нам WASM? Все просто — мы хотим, чтобы у разработчков были возможности
писать приложения под Angie.  В nginx такой возможности толком реализовано не
было, а все попытки подружить его с WASM закончились на проекте UNIT. Сейчас он,
в свою очередь, свое развитие прекратил.

Ну а данный текст был подготовлен для вас ведущим разработчиком Angie Владимиром
Хомутовым. Ранее Владимир уже [выступал с докладом про WASM](https://highload.ru/moscow/2024/abstracts/13031) в Angie на конференции
Highload++. Владимира «загнать» на Хабр не удалось, поэтому выкладываем текст от
гендиректора Angie Software. То есть от моего аккаунта.

Видео снято при помощи компании **Evrone Development**

<a id="nedostatki-sushchestvuiushchei-sistemy-rasshirenii-2"></a>

## Недостатки существующей системы расширений

Как известно, модули для nginx (и, соответственно, Angie) принято писать на C.
Это не значит, что вы не сможете написать его на C++ или чём-то ещё, но в любом
случае у вас на выходе будет динамическая библиотека, которой придётся
взаимодействовать с ядром сервера при помощи существующих C-интерфейсов.

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

В чём выражаются такие проблемы на практике? Если в проекте используется
множество модулей, то начинаются проблемы со стабильностью. Сервер может начать
падать, а выяснение того, какой модуль всё испортил - может затянуться. Ведь
адресное пространство - общее, и любой из модулей мог испортить память - либо
из-за ошибки в самом модуле, либо из-за какой-то несовместимости.

![image](../../_images/news/articles/898022/ba7f4a8632019395171a52b5fa8ad460.png)

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

Возможность писать модули на интерпретируемых языках программирования несколько
смягчает проблему, но в целом её не решает. Сам интерпретатор по-прежнему
крутится в том же адресном пространстве, что и сервер, а достаточно сложный код
всё-равно пытается звать C-функции через какой-то аналог [FFI](https://en.wikipedia.org/wiki/Foreign_function_interface), поскольку
нуждается в сторонних библиотеках, которые не всегда доступны в выбранном
окружении. Да и предоставляемые для интерпретаторов интерфейсы достаточно
ограничены и не дают в полной мере реализовать желаемые возможности. Конечно,
это компромисс - меньше возможностей, но также и меньше шансов сделать ошибку.

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

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

Ну и наконец - вопросы безопасности. Даже при отсутствии плохих намерений ошибка
в C модуле - это возможный инцидент безопасности. При достаточно длинной цепочке
зависимостей, кто поручится за то, что установленный вами с гитхаба модуль не
украдёт секретных ключей или не будет майнить [биткойн](https://www.coinbase.com/ru/price/tonshitcoin)? Проводите ли вы тщательный
аудит всех зависимостей рекурсивно при добавлении в проект и при каждом
обновлении? Вопрос риторический.

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

Итак, основная проблема с модулями - это  *отсутствие изоляции выполняемого
стороннего кода* и  *отсутствие стабильных интерфейсов* для взаимодействия с ним.

А если разговор идёт про изоляцию и запуск стороннего кода, то [WASM](https://webassembly.org/) выглядит здесь уместным решением.

<a id="dostoinstva-wasm-kak-platformy-dlia-sozdaniia-rasshirenii-2"></a>

## Достоинства WASM как платформы для создания расширений

Что такое WASM с точки зрения разработчика? Это виртуальная машина, которая
может исполнять загруженный в неё код. Причём эта виртуальная машина обладает
множеством подходящих свойств:

- **Предоставляет изоляцию исполняемого кода** Код на этапе компиляции
  собирается под архитектуру WASM-машины и выполняется в своём адресном
  пространстве. Код общается с внешним миром путём вызова набора функций,
  ограниченных запускающим хостом, и никак иначе. Соответственно, модуль может
  обрабатывать только те данные, которые были ему переданы разработчиком явным
  образом.
- **Нет привязки к конкретному языку** Каждая команда разработки может
  использовать знакомые инструменты с понятной инфраструктурой и жизненным
  циклом. Для разработчиков расширяемого продукта это резко упрощает жизнь:
  теперь вместо поддержки N языков можно сосредоточиться на создании единого
  стабильного интерфейса для WASM.
- **Платформонезависимый** WASM-код избавляет от зависимости от конкретной
  версии сервера, архитектуры операционной системы. Для сборки не нужны
  исходники сервера, достаточно спецификации интерфейсов.
- **Стандартизован: множество реализаций** Для WASM существует [стандарт](https://webassembly.github.io/spec/). Бинарный формат портабелен,
  существует множество реализаций, можно выбрать любую подходящую под наши
  критерии — маленькую, быструю, функциональную, безопасную и т.д.
- **Быстрый: компактный и оптимизированный** Бинарное представление WASM
  достаточно эффективно и поддаётся оптимизации. При запуске кода не нужно
  парсить мегабайты условного javascript, потому что всё это делается на этапе
  компиляции — с использованием современного оптимизирующего компилятора.
  WASM-машина — это достаточно продвинутая среда и с производительностью у неё
  всё достаточно хорошо. Как минимум, производительность не хуже
  интерпретаторов, а как максимум — сравнима с машинным кодом.
- **Обратная сторона изоляции: расходы на коммуникацию** Естественно, ничто не
  даётся даром: за изоляцию придётся платить производительностью. Необходимо
  копировать данные, передавая их в виртуальную машину, и забирая их обратно.
  Существуют накладные расходы на выполнение WASM-кода, на запуск экземпляров
  виртуальной машины и затраты памяти на сопутствующие нужды.
- **Системное окружение ещё не финализировано** Поддержка среди языков разнится.
  Поддержка в [LLVM](https://lld.llvm.org/WebAssembly.html) и наличие [clang](https://clang.llvm.org/) позволяет легко собирать C и C++, всё хорошо у
  Rust, а вот с интерпретаторами всё несколько сложнее. Поскольку окружение
  WASM-программы — это нечто отличное от того, к чему привыкли обычные
  разработчики, то придётся как-то адаптировать старые программы для него или
  разбираться, как писать новые. И здесь есть определённые проблемы, о которых
  чуть позже.

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

<a id="sistemnoe-okruzhenie-dlia-wasm-programm-2"></a>

## Системное окружение для WASM-программ

Итак, код модуля переезжает в виртуальную машину. И сразу возникает вопрос: а
как это вообще работает?

Ведь обычный модуль запускается в контексте процесса сервера, ему доступны как
внутренние API, так и внешние библиотеки, он может обратиться к операционной
системе (в которой запущен сервер) или даже выполнить ассемблерный код.

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

Поскольку WASM-код будет запускаться в виртуальной машине, то подразумевается
ли, что там доступна операционная система, библиотеки, «железо» наконец?

![image](../../_images/news/articles/898022/d546742c21be489fcda864ce5e07defe.png)

И здесь мы подходим к важной особенности WASM-машины: она минималистична и не
занимается эмуляцией железа (дисков, сетевых карт, таймеров и т.п.). Всё, что
там есть, — это процессор и память. То есть это такой чёрный ящик. Но поскольку
вещь в себе по Канту была бы несколько непрактичной, то общение с внешним миром
всё-таки возможно. Единственный предусмотренный для этого механизм — вызов
функций. Причём эти функции принимают только численные (i32, i64, f32, f64)
аргументы.

Функции эти либо экспортируются (тогда их можно позвать снаружи), либо
импортируются (тогда они должны быть предоставлены хост-окружением). В последнем
случае хост-функции служат аналогом системных вызовов операционной системы. Если
WASM-программа использует какую-то функцию, то она должна быть определена либо в
ней самой, либо в библиотеке, либо быть предоставленной хост-окружением.
Получается, что наш код запускается в так называемом [free-standing](https://en.cppreference.com/w/cpp/freestanding) окружении без операционной
системы, что накладывает на программу известные ограничения.

Значит ли это, что существующие программы запустить не получится? Очевидно, что
любую произвольную программу — нет, но если хост-окружение предоставит
достаточное количество системообразующих функций, то мы сможем запускать
имеющийся софт. Остаётся только понять, какие функции нам нужны от системы.
Хорошо бы иметь какой-то стандарт на системное окружение! В мире операционных
систем такой стандарт давно придуман и называется [POSIX](https://posix.opengroup.org/). Авторы WASM решили особо не мудрствовать,
взяли его подмножество и назвали его [WASI](https://wasi.dev/).

<a id="wasi-wasm-system-interface-preview-1-2019-2"></a>

### WASI = WASM System Interface (preview 1, 2019)

Что же попало в это подмножество и какого рода программы возможно запускать без
модификаций?

- Есть **стандартная библиотека С**, файлы и базовое окружение с
  stdin/stdout/stderr Наличие стандартной C-библиотеки и POSIX-функций,
  гарантирующих её работу и немного вокруг, даёт возможность смело собирать
  простые POSIX-программы под WASM. Классический "hello, world" будет работать.
  Можно писать фильтры, читающие со stdin и пишущие на stdout.
- **Отсутствует поддержка** нелокальных переходов Так как в WASM-машине нет
  инструкции JMP (намеренно, в целях упрощения как самой машины, так и
  генерируемого кода и его анализа), то это приводит к тому, что программы,
  нуждающиеся в таком функционале, имеют проблемы, либо не могут быть собраны.
  Например, вместо исключения в C++ вы получаете просто остановку программы. \*
  нет setjmp()/longjmp() \* нет поддержки исключений и раскрутки стека из C++ \*
  нет тредов Комитет работает над тем, как добавить многопоточность, но на
  сегодняшний день это экспериментальные возможности конкретных реализаций
  WASM-машин.
- **Нет сокетов**
- **Нет процессов**, fork/exec и связанных вещей: сигналов и т.п.
- Традиционные **POSIX-функции** для работы с процессами **отсутствуют**,
  поскольку их добавление потребовало бы собственно операционной системы: ведь
  процесс — это уже мощная абстракция со своей непреложной семантикой, которую
  нельзя было бы игнорировать. По тем же причинам не были добавлены и сокеты:
  сетевой стек — это уже довольно серьёзная привязка к операционной системе (или
  специфичному сетевому стеку), которая может сильно усложнить реализацию
  WASM-машины.

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

Фундаментально проблема заключается в модели безопасности: традиционный POSIX
предполагает изоляцию на уровне процесса и различение между ядром и
пользовательскими программами. Такое различение слишком широкое и не отвечает
требованиям реалий современности — когда приложение состоит из десятков модулей
от независимых разработчиков, причём код не является доверенным. Целью на уровне
дизайна платформы является возможность ограничить доступ на уровне компонента и
дать гарантии, что компонент может обрабатывать только переданные ему данные.

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

Наконец, в 2024 была представлена следующая версия WASI.

<a id="komponentnaia-model-wasi-preview-2-2024-2"></a>

### Компонентная модель: WASI preview 2 (2024)

Модель окружения WASM-программ была кардинально пересмотрена, и на свет явился
WASI preview2 aka [Wasm Component Model](https://component-model.bytecodealliance.org/). Каковы её ключевые
особенности?

- **Interface types** — язык описания интерфейсов. Во-первых, поскольку
  компоненты пишутся на разных языках, необходим ещё один язык, который будет
  описывать эти интерфейсы, чтобы никому не было обидно. Ниже приведён пример
  такого описания.
  ```text
  package "docs:calculator@0.1.0";
  interface calculate {
      enum op { add, }
      eval-expression: func(op: op, x: u32, y: u32) -> u32;
  }
  world calculator {
      export calculate;
      import docs:adder/add@0.1.0;
  }
  world app {
      import calculate;
  }
  ```

  Использовать в целевом языке это предполагается через кодогенераторы.
- [Canonical ABI](https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md)
  – **спецификация** представления этих интерфейсов. Во-вторых, для API
  естественно требуется ABI, т.к. необходимо компоновать программы, написанные
  на разных языках. Соответственно, был создан подробный документ, описывающий,
  как все эти типы представляются в памяти, как передаются аргументы и подобные
  вопросы.
- **Composition** — механизм динамического связывания модулей в компоненты. Ну и
  наконец, был представлен механизм связывания модулей в компоненты. Он
  определяет, как разрешаются зависимости, добавляет метаданных про
  импортируемые и экспортируемые функции и гарантирует, что модуль имеет доступ
  только к тому функционалу, что прописан в его интерфейсе.

Каковы же были практические последствия для разработчиков, которые за эти годы
научились как-то жить в парадигме WASIp1 и использовать WASM-модули?

- **Бинарный формат**, отличный от простых WASM-модулей. Компонент — это другой
  тип файла, отличный от модуля. По факту он включает, собственно, обычный
  модуль плюс набор метаданных. Нельзя просто так взять и заменить существующий
  модуль на компонент, даже если там всё те же функции.
- **POSIX API** объявлены устаревшими. Они больше не в моде.
  Новые правильные API для всего-всего будут разработаны с нуля: от командной
  строки до HTTP и тредов, от XML до GRPC. К счастью, существует набор так
  называемых "адаптеров", которые предоставляют доступ к интерфейсам wasip1 в
  рамках wasip2, так что существующие программы продолжат работать (по крайней
  мере, какое-то время).
- **Долгострой**: большинство анонсированных API на нынешний день находятся на
  стадии разработки. Вот [список](https://wasi.dev/interfaces#wasi-02) того,
  что доехало до стадии реализации.

Очевидно, столь радикальные изменения требуют ресурсов на их воплощение, поэтому
нынешний WASM являет собой некоторое промежуточное состояние: множество API
находятся в состоянии разработки или долгого рассмотрения комитетом и внесения
правок. Реально платформа поддерживается только в референсном wasmtime движке.

Таким образом, если вы пишете сегодня программу под WASM, вы можете рассчитывать
на простое подмножество POSIX (доступное в WASIp2 через адаптеры), доступные
WASIp2 API (долгострой), и тот API, который предоставляет ваш хост. Насколько
хороши эти интерфейсы — время покажет, но в любом случае это нечто новое, что не
позволит использовать их со старыми программами.

В целом, API, предоставляемое хостом, и является наиболее интересным для того,
кто пишет расширение на WASM. Рассмотрим, как wasm-машина коммуницирует с
хостом.

<a id="vstraivanie-wasm-mashiny-v-veb-server-na-primere-angie-2"></a>

## Встраивание WASM-машины в веб-сервер на примере Angie

Не будем пытаться написать свой уникальный WASM-движок, а воспользуемся
существующими, которые имеют SDK для встраивания и предоставляют C-интерфейс.
Сейчас мы поддерживаем референсный [wasmtime](https://wasmtime.dev/)
и [micro-wasm-runtime](https://github.com/bytecodealliance/wasm-micro-runtime).

Высокоуровнево, встраивание WASM-машины в веб-сервер выглядит так:

![image](../../_images/news/articles/898022/abc2135dbfdd95d08aa415b47c0bb815.png)
1. Библиотека WASM-рантайма загружается в рабочий процесс и инициализируется.
   Это происходит на старте. Теперь сервер может загружать WASM-код и порождать
   виртуальные машины используя интерфейс, который абстрагирует конкретные
   реализации WASM-машин.
2. Указанные пользователем в конфигурации модули загружаются. При старте они
   валидируются, а это значит, что они вызывают только доступные (в нашем хосте)
   функции. Если модуль вызывает какие-то сторонние функции, то такой модуль не
   будет загружен.
3. Загруженный модуль используется для обработки каких-то данных, например,
   HTTP-запросов. Для этого нужно породить экземпляр виртуальной машины. Это
   можно сделать либо на старте, либо по требованию. Тут всё зависит от
   намерений разработчика и сущности выполняемого кода.
4. По указанному в конфиге имени необходимо найти функцию в загруженном модуле и
   передать ей управление. При этом надо предоставить ей каким-то образом доступ
   к данным, которые она должна обработать, и получить их обратно. После чего
   можно уничтожить виртуальную машину либо оставить её для последующего
   использования. В последнем случае надо быть аккуратным с использованием
   памяти внутри машины и следить за ресурсами на хосте.

Отдельно отметим, что отсутствие поддержки тредов в WASM-машинах не является для
нас проблемой. Дело в том, что парадигма обработки запросов в веб-сервере — это
создание неблокирующихся однопоточных обработчиков. Все расширения работают в
рабочем процессе, а эффективное использование нескольких CPU обеспечивается тем,
что самих рабочих процессов запущено несколько. WASM-обработчик всегда
обрабатывает только один запрос и не запускает никаких тредов. Он может породить
асинхронную операцию, и в этом случае он может передать управление обратно
хосту. После наступления нужного события хост вновь позовёт эту виртуальную
машину и код продолжит своё выполнение. Попытки увеличить параллельность внутри
WASM-машины скорее навредят производительности. В будущем мы рассмотрим
возможность интеграции WASM и существующих thread pools.

Теперь рассмотрим более подробно, как код из виртуальной машины будет
взаимодействовать с хостом:

![image](../../_images/news/articles/898022/b95b223b44131fc192e89a527ea50673.png)
- Хост представляет собой много сложных и ссылающихся друг на друга объектов в
  памяти — открытые файлы, запросы, соединения... В загруженном WASM-модуле
  существует функция, которая должна как-то манипулировать этими объектами.
  Однако сделать это сложно: нельзя передать объект по-указателю: указатели с
  хоста не имеют смысла для гостя, это разные адресные пространства; нельзя
  передать сложный объект по-значению: функции принимают только численные
  аргументы. Получается, 2 плюс 2 сложить можно, а что делать с каким-нибудь
  списком строк?
- WASM-машина: отдельная память с неизвестным содержимым. Хост имеет доступ к
  памяти WASM-машины (поскольку это просто выделенная под её цели область
  памяти), но содержимое её ему непонятно: внутри виртуальной машины идёт своя
  жизнь, и мы не можем просто взять и написать по какому-то адресу, не учитывая
  внутреннего состояния рантайма машины (про который мы ничего не знаем, ведь
  модуль мог быть написан на любом языке). В целом, вывод отсюда простой —
  содержимым памяти машины управляет гость. Поэтому и выделять её тоже должен
  он. В принципе, возможно использовать определённые возможности WASM-движка и
  выделять память на хосте, но это делает наш код более зависимым от реализации
  и делает управление памятью менее явным. Также надо учитывать, что разные
  языки относятся к выделению памяти совершенно по-разному.
- Данные необходимо копировать
- Отдельно заметим, что никак не выйдет избавиться от копирования данных в
  память машины и обратно.
- WASM-машина не имеет доступа к памяти хоста
- Хост не знает о структуре памяти WASM-машины

В целом, ситуация с асимметричными правами доступа к памяти слишком напоминает
отношения ядра и пользовательской программы, чтобы её игнорировать. Для того,
чтобы организовать взаимодействие двух компонентов, достаточно предоставить
давно знакомый интерфейс с дескрипторами объектов и функциями для чтения/записи,
то есть UNIX-подобный API.

<a id="arkhitektura-wasm-rasshirenii-2"></a>

## Архитектура WASM-расширений

Как же будет выглядеть такой интерфейс для пользователя?

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

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

Таким образом, у нас появляется достаточно низкоуровневый системный интерфейс —
UNIX-подобный интерфейс, который позволяет передавать данные между объектами в
ядре сервера и пользовательскими программами. Этот интерфейс реализуется
хост-модулем непосредственно в ядре веб-сервера.

Все интересующие нас объекты внутри ядра веб-сервера для пользователя
идентифицируются при помощи дескрипторов. Набор дескрипторов управляется
хост-модулем, который предоставляет функции open() и close(), и позволяет
идентифицировать объекты по имени.

Для передачи данных между ядром и программой используем аналоги read(),
write() и ioctl().

```c
/* обращаемся к объекту по имени и получаем дескриптор */
int fd = open("http.request");

/* получаем данные от объекта */
read(fd, buf, size);

/* пересылаем данные в объект */
write(fd, buf, size);

/* завершаем работу */
close(fd);
```

Семантика и способы работы c таким интерфейсом широко известны и не вызывают
проблем у разработчиков. Интерфейс хорошо ложится на ограничения WASM — нам не
нужны сложные типы. Состояние программы определяется набором открытых
дескрипторов и их свойствами. Однако для создания непосредственно
пользовательских расширений такой интерфейс не подходит:

- низкоуровневый: примитивные типы данных, много лишних деталей
- зависит от формата сериализации и набора внутренних объектов ядра

Высокоуровневый пользовательский интерфейс реализован на стороне гостя и
представляет собой библиотеку функций предметной области. Это даёт следующие
преимущества:

- развязывает внутреннее представление вещей и внешние интерфейсы
- позволяет не усложнять системный интерфейс деталями
- возможность использовать язык высокого уровня
- возможность обновляться независимо от сервера

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

![image](../../_images/news/articles/898022/72482f01ea795d675be93a59380054bb.png)

Немаловажно отметить, что все интерфейсы описаны при помощи WIT и используют
Canonical ABI. Это позволяет иметь их документированное описание в виде
интерфейсных файлов и позволяет не зависеть от языка и метода их реализации.

Рассмотрим на примере, как выглядит использование API для работы с объектом на
хосте и при помощи каких модулей оно реализуется. Допустим, модуль обращается к
стандартной библиотеке с целью как-то взаимодействовать с сервером. Это обычный
вызов функции в любом языке программирования. Внутри библиотеки происходит
обращение к системному интерфейсу — вызов функции, определённой на хосте. Что же
происходит дальше?

- **Первичная обработка на хосте** В момент вызова open()/read()/write()/close()
  пересекается граница между WASM-гостем и хостом. WASM-адреса необходимо
  транслировать в адреса на хосте и проверить, что предоставленные буферы не
  выходят за границы памяти, выделенной WASM-машине. Обработка этих вызовов
  происходит в модуле, который реализует поддержку конкретного WASM-движка. Этот
  модуль непосредственно реализует набор функций, составляющих системный
  интерфейс. Системный интерфейс является минималистичным и лишь предоставляет
  абстракцию для обмена данными между ядром сервера и пользовательскими
  программами. Поэтому после первичной обработки данные передаются в
  хост-модуль, который отвечает за работу с объектами.
- **Выбор API в хост-модуле** Каждый запуск виртуальной машины происходит в
  каком-то конкретном окружении, например блок HTTP или Stream. Для каждого
  такого окружения существует отдельный модуль, который предоставляет набор
  доступных API. Например, HTTP-модуль будет предоставлять работу с запросами.
  При обращении к какому-либо API будет проверено его наличие и права доступа
  модуля: разрешил ли пользователь в конфигурации данному модулю доступ к
  данному API. Если всё хорошо, то для объекта создаётся дескриптор и вызывается
  модуль, реализующий непосредственно работу с интерфейсом.
- **Реализация функционала в API-модуле** — определяет семантику обмена данными
  Модуль, который реализует API, проводит необходимые действия с объектом;
  например, откроет файл или соединение с удалённым хостом, или просто
  инициализирует какой-то счётчик.

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

![image](../../_images/news/articles/898022/f5644a662092d182540d00fea378dd04.png)

<a id="prakticheskii-primer-vychisliaem-peremennye-2"></a>

### Практический пример: вычисляем переменные

Рассмотрим [модуль](https://git.angie.software/web-server/angie-wasm-examples/src/branch/main/c/ngx-http-vars-sqlite),
который позволяет создавать переменные запроса при помощи WASM-обработчика.
Такой обработчик получает на вход строки (которые могут быть вычислены из других
переменных), и на выходе у него опять же строка. Это не самый сложный модуль, но
даже в таком простом случае использование WASM может быть полезно и существенно
упростить жизнь сопровождающим такого модуля.

В данном примере обработчик переменной будет трактовать свои аргументы как
SQL-запрос с параметрами. Итак, вот наш конфиг:

```nginx
load_module "modules/ngx_wasmtime_module.so";

wasm_modules {

   load "ngx_http_vars.wasm"
         id=http_vars
         fs=/home/demo/tmp:/data;
}

http {
   wasm_var http_vars                    # module ID
      "ngx:wasi/var-utils#sqlite"        # function to call
       $db_res                           # new variable name

                                         # function args:
       "/data/demo.db"                   #  0: database

       "SELECT name FROM people
       WHERE id>?1 AND id"

       $arg_id $arg_id2;                 # 2+: query arguments
}

http {
    server {
        listen 127.0.0.1:8080;
        location / {
            return 200 "sql( $arg_id .. $arg_id2 )= $db_res \n";
        }
    }
}
```

Прокомментируем конфигурацию:

- 1 — загружаем обычный модуль — поддержку среды исполнения wasmtime
- 5 — загружаем скомпилированный WASM-модуль с диска
- 6 — даём ему id, чтобы обращаться к нему из других мест конфига
- 7 — разрешаем ему доступ к одной директории на файловой системе под именем       /data
- 11 — в директиве wasm_var мы указываем ID загруженного модуля
- 12 — вызываемая WASM-функция в указанном модуле
- 13 — имя результирующей переменной
- 16 — имя файла базы данных на диске (доступ к которой мы явно разрешили).
- 18-19 — SQL-запрос
- 21 — аргументы запроса к базе берутся из аргументов HTTP-запроса
- 28 — возвращаем клиенту ответ с использованием результата вычислений

Теперь запустим сервер и попробуем к нему обратиться:

```bash
$ curl "http://127.0.0.1:8080?id=1&id2=5"
sql(1..5)=foo2,foo3,foo4

$ ls -lh "conf/ngx_http_vars.wasm"
-rwxr-xr-x 1 demo demo 1.3M Nov  2 11:34 conf/ngx_http_vars.wasm
```

Аргументы HTTP-запроса были подставлены в SQL-запрос, и наш ответ содержит
релевантную выборку из базы данных.

Остаётся прояснить вопрос — откуда в модуле взялся [SQLite](https://www.sqlite.org/) и что происходит внутри WASM-модуля. А внутри
WASM-модуля находится обычная POSIX-программа на языке C, слинкованная
статически с SQLite. Весь SQLite целиком был собран под WASM и добавлен в
модуль.

Отметим несколько особенностей этого демо:

- вот таким нехитрым образом мы фактически получили HTTP-интерфейс к SQLite базе
- ни строчки кода не запатчено, ни в коде веб-сервера, ни в SQLite
- мы явно разрешили доступ только к одной директории на файловой системе
- мы не добавили никаких внешних зависимостей, только самодостаточный файл
  модуля; его можно без изменений использовать как на ARM, так и на x86

<a id="chem-eshchio-mozhet-byt-polezen-wasm-v-veb-servere-2"></a>

## Чем ещё  может быть полезен WASM в веб-сервере?

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

Например, в качестве эксперимента мы взяли MP4-модуль, который подарил нам
несколько CVE, и вынесли парсинг MP4 в WASM. Оригинальный код парсинга на C был
практически без изменений собран под WASM и интегрирован обратно в модуль.    Ну
и конечно, можно создавать свои API, доступные для ваших WASM-модулей.

<a id="obzor-realizovannogo-v-ekosisteme-angie-funktsionala-2"></a>

## Обзор реализованного в экосистеме Angie функционала

> Базовые модули для работы с WASM:
- Модули поддержки различных WASM-движков ([1](https://git.angie.software/web-server/angie-wasm/src/branch/main/ngx_wasmtime_module),
  [2](https://git.angie.software/web-server/angie-wasm/src/branch/main/ngx_wamr_module))
- [Создание и работа](https://git.angie.software/web-server/angie-wasm/src/branch/main/ngx_wasm_module)
  с WASM-машиной и загрузка модулей

Создание HTTP-расширений:

- [Обработчики переменных](https://git.angie.software/web-server/angie-wasm-examples/src/branch/main/c/ngx-http-vars)
  на WASM
- [Обработчики фаз](https://git.angie.software/web-server/angie-wasm-examples/src/branch/main/c/ngx-http-handler)
  обработки запроса
- [Фильтры тела](https://git.angie.software/web-server/angie-wasm-examples/src/branch/main/c/ngx-http-filter)
  запроса и ответа

[SDK](https://git.angie.software/web-server/angie-wasm-sdk):

- Все доступные интерфейсы, предоставляемые в SDK, описаны в [WIT-файлах](https://git.angie.software/web-server/angie-wasm-sdk/src/branch/main/wit/wasi).
- Например, работа с HTTP [запросом](https://git.angie.software/web-server/angie-wasm-sdk/src/branch/main/wit/wasi/http_request.wit),
  [логгирование](https://git.angie.software/web-server/angie-wasm-sdk/src/branch/main/wit/wasi/log.wit),
  доступ к [параметрам конфигурации](https://git.angie.software/web-server/angie-wasm-sdk/src/branch/main/wit/wasi/http_env.wit),
  сетевым [соединениям](https://git.angie.software/web-server/angie-wasm-sdk/src/branch/main/wit/wasi/connection.wit)
  и другие.
- Примеры доступны в отдельном [репозитории](https://git.angie.software/web-server/angie-wasm-examples).

<a id="plany-razvitiia-wasm-v-angie-2"></a>

## Планы развития WASM в Angie

- **Интегрировать поддержку WASM** в базу веб-сервера по умолчанию
- **Расширение количества доступных интерфейсов** и стандартной
  библиотеки, а также улучшение и отладка существующих модулей
- **Переход на использование компонентной модели** при появлении её поддержки в
  C-интерфейсах WASM-движков. Это сразу упростит процесс линковки при
  использовании отличных от C языков программирования. На сегодня это главный
  блокирующий фактор, не позволяющий с лёгкостью     интегрировать библиотеки с
  пользовательским кодом.
- **Увеличение числа мест**, где возможно добавить **пользовательскую логику**,
  в том числе на WASM. Исторически в nginx и Angie механизм обеспечения динамики
  в конфигурации –     это переменные. Однако обработчики переменных —
  синхронные,     и переменную нельзя вычислить поэтапно, перемежая это с
  обработкой других     данных. Если сделать возможной асинхронную обработку
  переменных, то это позволит     кардинально расширить возможности
  конфигурации: например, обработчик     переменной сможет сходить в сеть или
  провернуть ещё какие-то сложные вещи     без изменения кода, который эту
  переменную использует.

<a id="obratnaia-sviaz-2"></a>

## Обратная связь

Мы очень заинтересованы в обратной связи, поэтому если вы — автор расширения,
или активный пользователь каких-то сторонних модулей и у вас есть, что сказать
по теме — [пишите нам](https://angie.software/service/). Нам было бы интересно
знать:

- Какие сторонние модули вы используете и для чего?
- Какие проблемы с модулями у вас существуют?
- Какие языки вам интересны для написания модулей?
- Какие нужны API в первую очередь?
- Какие библиотеки необходимы?
