Скриптуем на WebAssembly, или WebAssembly без Web

в 16:47, , рубрики: binaryen, C, c++, Rust, webassembly, Анализ и проектирование систем, больше кода на хабре, Компиляторы, скрипты

Скриптуем на WebAssembly, или WebAssembly без Web - 1

Представлять WebAssembly не нужно — поддержка уже есть в современных браузерах. Но технология годится не только для них.

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

Далее — проверка концепции на прочность, инструментарий и первый скриптовый модуль.

Зачем?

Wasm-модули можно использовать в тех же случаях, что и скриптовые языки: для исполнения динамически загружаемой логики. Там, где используется Lua и Javascript. Но затраты на интерпретацию wasm меньше, чем у скриптовых языков, и на wasm можно применить больше оптимизаций. Ибо оптимизации эти делаются во время компиляции на исходной машине, а не интерпретации или JIT-компиляции на клиенте.

WebAssembly потенциально независим от исходного языка. В перспективе, скриптёр (тот, кто будет писать скрипты) может делать это на удобном для него языке, не привязываться к конкретному языку.

Кроме скриптовых языков технологию можно сравнить с LLVM-байткодом и Java-машиной.

Сравнение с LLVM-IR сделано уже в ходе разработки WebAssembly. Авторы аргументируют свой отказ ещё на этапе MVP так:

  • Портируемость: для различных архитектур целевой машины набор инструкций должен быть один. В общем случае для LLVM-IR это не так.
  • Стабильность: набор инструкций должен изменяться со временем только при сохранении обратной совместимости. LLVM такой цели не ставит.
  • Минимально возможный размер бинарного кода
  • Максимально быстрое декодирования
  • Быстрая компиляция: набор инструкций должен быть применим вместе с JIT-компиляцией, позволять достаточно быстрый процесс запуска приложения. LLVM-IR проектируется под ahead-of-time-компиляцию.
  • Минимальная неопределенность: поведение программ должно быть максимально предсказуемым. LLVM-IR же проектируется для возможностей оптимизации, значит, содержит множество вариантов неопределённого поведения (undefined behavior, UB)

По сравнению с Java и её виртуальной машиной:

  • wasm не привязан к конкретному языку (или группе языков, с учётом Scala и Kotlin)
  • Java-машина неотделима от своей инфраструктуры и стандартных библиотек
  • JVM достаточно массивна
  • JVM требует вызова нативных функций поверх уже имеющейся инфраструктуры, что для низкоуровневых функций часто сложнее, чем разработка “с нуля”.

WebAssembly может занять в инфраструктуре портируемого кода собственную нишу, где ни скриптовые языки, ни LLVM-IR, ни JVM не решают задачи эффективно.

А какие это задачи?

Идея использовать WebAssembly без web-окружения возникла из конкретной задачи. Необходимо создать модули с интерактивными графиками, которые бы работали на веб-сайте и в мобильном приложении. Первоначальный вариант решения: встроить Javascript-движок в мобильное приложение и передавать логику javascript-кодом. Но движки оказались достаточно массивными и сложными в устройстве (за исключением, пожалуй, JerryScript). Использование движка для одной небольшой задачи выглядело серьёзным оверинжинирингом. В этот момент мы пришли к выводу, что аналогичные модули на WebAssembly будут лучше из-за малого размера интерпретатора и более быстрой интерпретации.

Другой вариант использования: компилировать в WebAssembly шаблоны для веб-страниц. Для этого достаточно создать backend к любимому шаблонизатору. Такие шаблоны достаточно просто запускать как на сервере через интерпретатор, так и в браузере стандартными средствами. Создать backend к шаблонизатору проще, чем портировать любимый шаблонизатор на любимую систему. Формально, backend проще сделать для кода на C, который после будет компилироваться в wasm.

На сайте WebAssembly предлагаются и другие варианты использования:

  • Распространение игровых приложений в виде модулей (по аналогии с cocos2d-js, вместе с его обновлениями кода на ходу)
  • Исполнение ненадежного кода на стороне сервера (на самом деле, та же задача, что и с шаблонизаторами, но глобальнее)
  • Гибридные приложения для мобильных устройств
  • Запуск процессов сразу на нескольких вычислительных узлах

Как?

Собираем инструменты для сборки WebAssembly

Для реализации задуманного нам нужен экспериментальный модуль WebAssembly из LLVM версии 5 (текущей стабильной) или старше.

Будем использовать цепочку LLVM Webassembly backend -> LLVM байткод -> текстовое представление LLVM-IR -> Binaryen s2wasm -> Binaryen wasm-as

Собираем LLVM

Сборка занимает от 15 минут до часа и требует много памяти (особенно, при make -j8)

# download LLVM-5 sources
wget http://releases.llvm.org/5.0.0/llvm-5.0.0.src.tar.xz
tar -xJf llvm-5.0.0.src.tar.xz llvm-5.0.0.src
mv llvm-5.0.0.src llvm
rm llvm-5.0.0.src.tar.xz

cd llvm/tools

# download clang sources
wget http://releases.llvm.org/5.0.0/cfe-5.0.0.src.tar.xz
tar -xJf cfe-5.0.0.src.tar.xz cfe-5.0.0.src
mv cfe-5.0.0.src clang
rm cfe-5.0.0.src.tar.xz

cd -

WORKDIR=`pwd`
INSTALLDIR=`pwd`

rm -rf llvm-build
mkdir llvm-build
cd llvm-build

# For Debug build:
# cmake -G "Unix Makefiles" -DCMAKE_INSTALL_PREFIX=$INSTALLDIR -DLLVM_TARGETS_TO_BUILD= -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly -DCMAKE_BUILD_TYPE=Debug $WORKDIR/llvm
cmake -G "Unix Makefiles" -DCMAKE_INSTALL_PREFIX=$INSTALLDIR -DLLVM_TARGETS_TO_BUILD= -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly $WORKDIR/llvm 
make

Собираем Binaryen

git clone https://github.com/WebAssembly/binaryen.git
cd binaryen
cmake .
make

Собираем WABT

git clone https://github.com/WebAssembly/wabt.git
cd wabt
git submodule update --init
make

Устанавливаем Rust

curl https://sh.rustup.rs -sSf | sh
source $HOME/.cargo/env
rustup toolchain install nightly
rustup target add wasm32-unknown-unknown
rustup default nightly

Пример сборки C/C++ с помощью makefile

Сборка Rust

rustc <source_file> --target=wasm32-unknown-unknown --crate-type=cdylib -C panic=abort -o <wasm_output>

Собираем интерпретатор

В качестве proof of concept можно использовать интерпретатор из WABT. Для подтверждения работы будем вызывать функцию из WebAssembly, которая вызывает функцию среды. Добавить импортируемые функции в WABT можно, например, вот так.

Код на C

extern void import_function(int);

int export_function(int i_test) {
  import_function(i_test * 3);
  return ++i_test;
}

Код на Rust

#![no_std]
#![no_main]
#![feature(lang_items)]

#[lang = "panic_fmt"]  fn panic_fmt() -> ! { loop {} }

mod wasm {
    pub fn _import_function(i: isize) -> isize {
        unsafe { import_function(i) }
    }

    extern {
        fn import_function(i: isize) -> isize;
    }
}

#[no_mangle]
pub fn export_function(i_test: isize) -> isize {
    wasm::_import_function(i_test*2);
    let result = i_test+1;
    result
}

Запустить модуль можно так:

<wasm-interp> <wasm-file> -E export_function

Эти же модули можно использовать и в вебе, если реализовать требуемые импортируемые функции. Например, вот так.

Замечания к коду

  • Rust добавляет в импортируемые функции rust_begin_unwind (хотя -C panic=abort гарантирует, что эта функция не будет вызвана). Потенциально проблему можно исправить на уровне rustc, на уровне оптимизации WebAssembly (через удаление неиспользуемых параметров). Как временное решение мы добавили rust_begin_unwind в список импорта в виде функции, которая не делает ничего.
  • Функции, возвращающие структуры будут преобразованы таким образом: Vec2 makeVec2(float x, float y) {...} в (func (; 2 ;) (param $0 i32) (param $1 f32) (param $2 f32). Возвращаемое значение было преобразовано в указатель (тип i32) на блок памяти, который будет хранить готовый объект. То есть, компилятор выполнил RVO. Для работы с такими функциями нужно предварительно распределить из памяти модуля требуемый блок, и вызывать функцию с указателем в качестве первого аргумента
  • Если хотите компилировать код с виртуальными функциями через Binaryen, вам пригодится патч:

Патч для weak-символов в Binaryen

--- a/src/s2wasm.h
+++ b/src/s2wasm.h
@@ -1320,7 +1320,7 @@ class S2WasmBuilder {
     }
     skipWhitespace();
     Address align = 4; // XXX default?
-    if (match(".globl")) {
+    if (match(".globl") || match(".weak")) {
       mustMatch(name.str);
       skipWhitespace();
     }

Проектируем стандартную библиотеку

WebAssembly модули умеют импортировать и экспортировать функции, но создать библиотеку предстоит самостоятельно: стандарт не определяет никакой стандартной библиотеки.

Стандартная библиотека создается для интерпретатора и компилируется вместе с интерпретатором. Если задача требует исполнения wasm и нативно, и в браузере, вам нужно будет портировать вашу стандартную библиотеку на Javascript для совместимости. Например, для задачи интерактивных графиков написать cmath-совместимую обёртку и для интерпретатора, и для javascript.

Что включать в стандартную библиотеку — отдельный сложный вопрос. В случае со скриптовыми языками, вам дают уже готовую универсальную библиотеку. Из которой некоторые функции вы будете вынуждены выключить (например, прямой доступ к файловой системе). В случае с wasm вы можете создать строгий специальный API, которым скрипты будут ограничены в их песочнице.

Заключение

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

Текущее (на ноябрь 2017 года) состояние инструментов довольно слабое, но они уже пригодны для использования. Выявленные проблемы исправляются достаточно быстро. В этой статье мы хотели показать возможность отдельного применения WebAssembly. А куда копать дальше — в опросе.

В соавторстве с strelok2010

Весь код здесь.

Автор: Роман Катунцев

Источник

Поделиться

* - обязательные к заполнению поля