Представлять 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


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

Комментарии (12)


  1. CyberSoft
    08.12.2017 21:10

    Не знаком с WebAssembly, и поэтому есть вопросы:


    WebAssembly — кроссплатформенный байткод

    Разве это не позволяет интерпретирующему использовать всю ту же JIT-компиляцию?


    wasm не привязан к конкретному языку (или группе языков, с учётом Scala и Kotlin)

    JVM тоже не привязана к Java, она понимает свой байткод, в который могут компилироваться и другие языки (выше озвученные например). Группе языков? Есть интерпретаторы Python/Ruby/etc, но да, всё же это интерпретатор в интерпретаторе.
    Или я что-то не понимаю?


    JVM достаточно массивна

    Java 9 не спасёт ситуацию?


    Кстати, гуглятся интерпретаторы WebAssembly под .NET и JVM.


    1. SBKarr Автор
      08.12.2017 21:33

      Разве это не позволяет интерпретирующему использовать всю ту же JIT-компиляцию?
      JIT-компиляция замечательна, когда на неё есть ресурсы. У FGPA и большей части мобилок их нет. С WebAssembly во время AOT-компиляции можно сделать значительно более сложные и затратные компиляции, чем позволяют ограничения по времени и ресурсам при JIT. Аналогию можно провести с asm.js, который по сути есть AOT-скомпилированный Javascript. Или с LuaJIT, который, в пику названию, предлагает возможность распространять AOT-скомпилированные модули. WebAssembly — шаг дальше в том же направлении.

      JVM привязана больше к инфраструктуре, а не к языку. Были причины, почему Java в своё время не заняла нишу WebAssembly, несмотря на попытки. И воз поныне там же. Подробнее можно здесь.

      Java 9 не спасёт ситуацию?
      Нет. Например, вам нужно сделать исполняемые модули с исключительно математическими функциями. На WebAssembly вы можете сделать подмножество, которое умеет только в математику. С JVM придётся тянуть рантайм, вне зависимости от версии Java.

      Кстати, гуглятся интерпретаторы WebAssembly под .NET и JVM.
      Так точно. Байткод и интерпретатор для WebAssembly MVP очень простые, буквально за пару вечеров можно написать. Это ещё одна причина, почему у технологии есть все шансы развиться. Порог входа намного ниже, чем в большинство системных разработок. Вся сложность на уровне компиляторов и средств разработки. Мы взяли интерпретатор WABT из соображений портируемости: С/С++ достаточно просто запускается практически везде.

      Фича как раз в том, что собранные модули можно запускать на любом интерпретаторе любой конструкции. В том числе, нативном, браузерном, или построенном поверх JVM и .NET.


      1. khim
        09.12.2017 12:44

        Были причины, почему Java в своё время не заняла нишу WebAssembly, несмотря на попытки.
        Причина, собственно, одна: Ларри сильно денег хотел.

        Все недостатки CLR/C# и JVM/Java можно было бы исправить — но только совместными усилиями. А их владельнцы хотели их полностью контролировать, «бить по рукам» всех конкуретном и «снимать сливки».

        В результате «сливок» никому не досталось, и подходы, основанные на JVM (а это не только апплеты — были и другие интересные идеи) и подходы, основанные на CRL (Silverlight и прочее) оказались выкинутыми «на свалку истории», а мы остались с JavaScript'ом и, вот теперь уже, пытаемся переизобрести всё в третий раз…

        Посмотрим, что выйдет…


  1. CyberSoft
    08.12.2017 23:28

    Спасибо за ответы.


    JVM привязана больше к инфраструктуре, а не к языку. Были причины, почему Java в своё время не заняла нишу WebAssembly, несмотря на попытки. И воз поныне там же. Подробнее можно здесь.

    Да, песенка спета, да и к лучшему что так закончилось. Аплеты выкинут и целый груз с плеч.
    А вобще все эти нашпигованные платформы, лезущие в интернет — изобилуют дырами. Как флеш. Может с WebAssembly такого не будет


  1. ReklatsMasters
    09.12.2017 01:21

    Создать более-менее универсальную стандартную библиотеку

    Есть вполне себе универсальный musl, который используется в emscripten.


    Rust ночной сборки пока не может корректно собрать библиотеку с wasm32-unknown-unknown, а для сборки исполняемого пакета нужна main. Которая идёт в экспорт.

    Всё вполне нормально собирается с ключом --crate-type=cdylib.


    Rust добавляет в импортируемые функции rust_begin_unwind

    Не заметил этого поведения. Видел только добавление rust_eh_personality в экспорт.


    ===


    В очередной раз напомню про свою сборку примеров по webassembly.


    1. SBKarr Автор
      09.12.2017 02:08

      --crate-type=cdylib — помогло от проблемы с main, спасибо. Исправил в статье.


      Проблемы с rust_begin_unwind в вашем примере нет, поскольку её убирает wasm-gc. Так же, как и кучу функций из std без #![no_std]. Собирать таким образом минимальный вариант для отладки не слишком рационально. Для релизной сборки, понятное дело, оптимизатор нужен, и он проблему решит.


      Теперь про стандартную библиотеку. Идея не в том, чтобы взять и что-то портировать под интерпретатор. Это как раз одни из тех граблей, которые попались Java и Flash при вытаскивании их в браузер. Нужен способ легко и просто определять свою песочницу, а внутри неё сделать набор самых базовых функций. libc это таки значительно больше, чем базовые.


      На самом деле, здесь есть два различных подхода. Первый подход — сделать полностью функциональную систему на wasm-модулях. В таком случае портирование libc — хороший вариант.


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


      1. ReklatsMasters
        09.12.2017 12:32

        Вы хотите сделать какой то новый язык, который собирается в wasm? Как минимум есть assemblyscript. Хотя я считаю, что это не нужно. Есть emscrpten для сборки существующих проектов на c и плюсах, есть rust для новых проектов, а в будущем добавится go, D и другие. Всё это уже собирается в wasm и решает свои задачи.


        1. SBKarr Автор
          09.12.2017 14:58

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

          Собственно, мнение, что это нафиг не нужно, как и нафиг не нужна архитектура wasm32-unknown-unknown в Rust уже высказано, прочитано и учтено. Сходу не найду, но в rust internals про это была не одна простыня.


  1. firk
    09.12.2017 14:54

    Не надо забывать что webassembly появился как костыль для js.


    1. SBKarr Автор
      09.12.2017 15:00

      А C++ и ObjectiveC как костыли к C. А С как костыль к B. Noted.


      1. babylon
        09.12.2017 17:36

        Сразу видно, что статью писал грамотный системныый архитектор. Видно по количеству минусов, поставленных хабралишенцами.


        1. SBKarr Автор
          09.12.2017 19:58

          Если инженерия софта будет конкурсом популярности — софт будет исключительно красиво падать.