Задумался о том что бы прикрутить к своему пет проекту систему плагинов на WebAssembly. Это потенциально позволит переиспользовать существующий код на Go, C++, Rust, если конечно же он есть. А так же избавится от so/dll, что удобно при распространении плагинов, когда проект представляет собой десктопное приложение и собирается под Windows, OSX, GNU/Linux. Поэтому пошел смотреть как это сделано в Envoy. На начало 2019 Envoy представляет собой статический бинарник со всеми расширениями скомпилированными во время сборки, поэтому нужно поддерживать несколько бинарных сборок, вместо использования официального и не модифицированного бинарного файла Envoy. Для проектов, которые не контролируют свои деплой, еще более проблематично, потому что обновление расширений требует пересборки и деплоя всего Envoy.    Гибкость. Расширения можно доставлять и перезагружать во время выполнения. Любый изменения или исправления могут быть протестированы во время выполнения, без необходимости обновления и редиплоя нового бинарника. Надежность и изоляция. Расширения запускаются в песочнице и могут быть ограничены по потреблению CPU и памяти. Безопасность. Расширения запускаются в песочнице с четко определенным API для связи с прокси(envoy, nginx и т.д.). Имеют ограниченный доступ к свойствам, которые могут менять. Разнообразие. Большой выбор языков программирования, которые могут скомпилировать в WebAssembly, что позволяет разработчикам с любым опытом(C, Go, Rust, Java, TypeScript, и т.д.) писать расширения. Переносимость. Поскольку интерфейс между хост-средой и расширениями не зависит от прокси-сервера, расширения, написанные с использованием Proxy-Wasm, могут выполняться в различных прокси-серверах, например Envoy, NGINX, ATS  или даже внутри библиотеки gRPC (при условии, что все они реализуют стандарт). Более высокое потребления памяти из-за необходимости запуска множества виртуальных машин, каждая со своим блоком памяти. Более низкая производительность для расширений, преобразующий полезные данные, из-за необходимости копировать значительные объемы данных в песочницу и из нее. Более низкая производительность для CPU-bound задач. Ожидается, что замедление будет менее чем в 2 раза по сравнению с нативным кодом. Увеличенный размер бинарника из-за необходимости включать среду выполнения Wasm. Это ~20 MB для WAVM и ~10 MB для V8. Экосистема WebAssembly все еще молода, и в настоящее время разработка сосредоточена на использовании в браузере, где JavaScript считается хост-средой. В Envoy взяли C++ API, прикрутили Wasm VM и перенаправляют вызовы в wasm модуль.  В одном wasm модуле могут быть несколько фильтров. Экземпляры Wasm VM размножены и размещены в thread-local storage Коммуникация между экземплярами Wasm VM осуществляется примитивами shared data и message queue. Службы представляют из себя singleton и выполняется в основном потоке Envoy. Они выполняются параллельно фильтрам и осуществляют вспомогательные функции: логи, статистика и т.д. Wasm VM это один из следующих рантаймов V8 WAMR Wasmtime WAVM Спецификация ABI разбита на два больших блока: функции реализованные в модуле и функции реализованные в хост-среде. Выделю две функции: выделение памяти proxy_on_memory_allocate, точка входа _start. Спецификация представляет набор функций в формате proxy_log  аргументы: i32 (proxy_log_level_t) log_level i32 (const char*) message_data i32 (size_t) message_size возвращаемое значение: i32 (proxy_result_t) call_result i32 это числовой тип в wasm, а так она выглядит в разных SDK Наверное это одна из самых важных тем при построении такой системой, где есть память на стороне хоста и память в wasm модуле. Никакая управляемая память не передается в wasm модуль при вызове обработчиков. Вместо этого wasm модуль сам запрашивает данные.  Например proxy_on_http_request_body передает информацию о количестве доступных байтов в теле запроса, модуль должен запросить эти данные используя proxy_get_buffer. Когда это происходит, хост выделяет помять у wasm модуля вызовом proxy_on_memory_allocate, копирует туда данные и отдает память во владение wasm модулю, в надежде, что он освободит ее.  аргументы: i32 (size_t) memory_size возвращаемое значение: i32 (void*) allocated_ptr Реализация на AssemblyScript malloc.ts Обратное преобразование В AssemblyScript заложили поведение при котором объекты можно отдавать во внешнюю среду(в первую очередь в JS). Для этого есть __pin/__unpin, что бы сборщик мусора не собрал объекты на которых уже нет ссылок. В Go Обратное преобразование Что тут можно сказать? В спецификации Go ничего не сказано про работу сборщика мусора. Компилятор с https://go.dev/ запрещает передавать указатели на память из Go в Си. Есть пакет go-pointer, который немного напоминает pin/unpin из AssemblyScript. Внутри довольно простой НО разработчики используют TinyGo, в котором сборщик мусора попроще и запускается когда недостаточно места в куче. Если между вызовом proxy_on_memory_allocate и моментом возврата владения в Go нет выделения памяти, то это условно безопасно. А вот в C++ SDK proxy_on_memory_allocate не увидите. Идет поиск malloc, который экспортирует компилятор На стороне хоста выполняется поиск malloc, если нет, то ищется proxy_on_memory_allocate Написав в Go AssemblyScript C++ Нужна точка входа, которая инициализирует рантайм C++/Go/AssemblyScript и выполнит что-нибудь вида main. WASI для таких целей предлагает _start и _initialize. Хотя в спеке есть только _start, но на хосте доступны оба варианта Один из недостатков это копирование памяти. Может потребоваться не только копирование, но и преобразование. В Go строки это просто последовательность байт, но обычно там UTF-8. В AssemblyScriptэто последовательность UCS-2 и требует преобразования. А передача привычного контейнера map потребует дополнительной упаковки/распаковки На этом все. Полезные ссылки WebAssembly for Proxies (ABI specification) Extending Envoy with WebAssembly - John Plevyak & Dhi Aurrahman, Tetrate AssemblyScript SDK, C++ SDK, Go (TinyGo) SDK C++ Host, Go Host  Golang Developer