Урок-введение по WebAssembly на примере игры «Жизнь»

Аватар Евгений Туренко
Отредактировано

13К открытий14К показов
Урок-введение по WebAssembly на примере игры «Жизнь»

В этом уроке мы пройдём путь по портированию библиотеки JavaScript в WebAssembly (wasm) на примере игры «Жизнь», созданной английским математиком Джоном Конвеем. Этот урок отлично подойдёт начинающим, чтобы понять, что стоит за банальным «Hello World!» в WebAssembly.

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

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

Следует отметить, что для написания урока использовалась операционная система Ubuntu 17.04, возможно, процесс будет отличаться, если у вас другая операционная система. Предполагается, что вы можете быть незнакомы с WebAssembly, поэтому всё будет создаваться с нуля. В статье не будет акцентироваться внимание на ES6 и Webpack. В Интернете вы сможете найти более развёрнутые руководства, благодаря которым вы сможете детально ознакомиться с данными инструментами. Например, в этой статье.

Прим. перев.Также предлагаем вам прочитать нашу статью про Webpack.

План урока:

  1. Установка набора инструментальных средств.
  2. Интеграция JavaScript.
  3. Оптимизация движка.
  4. Тестирование.
  5. Заключение.

Установка набора инструментальных средств

Для быстрой установки последней версии LLVM просто загрузите и установите портативный SDK Emscripten для Linux и OS X (emsdk-portable.tar.gz).
Извлеките архив и откройте терминал в папке.

			$ ./emsdk update
$ ./emsdk install latest
		

Теперь осталось немного подождать, если, конечно, у вас хорошая скорость загрузки.

Примечание SDK Emscripten предоставляет обширный набор инструментальных средств (Clang, Python, Node.js и Visual Studio) в одном простом для установки пакете со встроенной поддержкой обновлений до новых версий SDK.

Теперь у нас есть всё необходимое для старта. Следует активировать SDK.

			$ ./emsdk activate latest
$ source ./emsdk_env.sh #  можете добавить эту строку в ваш файл с расширением .bashrc
		

Теперь нужно создать тестовый файл на C — counter.c:

			int counter = 100;
int count() {
    counter += 1;
    return counter;
}
		

Скомпилируйте это в код WebAssembly с помощью команды emcc, которая используется для вызова компилятора Emscripten из командной строки:

			$emcc counter.c -s WASM=1 -s SIDE_MODULE=1 -o counter.wasm
		

После компиляции получился файл counter.wasm.

Интеграция JavaScript

Отдельный файл с расширением .wasm не сможет ничего выполнить самостоятельно, поэтому нужно интегрировать его в код на JavaScript. Это можно сделать с помощью Webpack и wasm-loader. Рекомендуется ознакомиться с документацией, чтобы посмотреть на разные примеры. А пока давайте приступим к интеграции:

			import Counter from ‘./wasm/counter’
const wasmHelloWorld = () => {
const counter = new Counter();
console .log(“count function result is : ” + counter.exports._count());
}
window.onload = wasmHelloWorld
		

При загрузке этого кода на пробной HTML-странице у вас должно отобразиться число 101 в консоли. Кроме этого ничего другого быть не должно. В браузере Mozilla Firefox 53 у вас должно появиться такое сообщение:

			import object field 'DYNAMICTOP_PTR' is not a Number
		

Если что-то пошло не так и вы чувствуете, что впустую тратите время, то посмотрите обсуждение на StackOverflow.

Теперь давайте вернёмся к коду. Нам нужно скомпилировать код на C с флагом оптимизации:

			$ emcc counter.c -01 -o counter.wasm -s WASM=1 -s SIDE_MODULE=1
		

Теперь, когда мы выполняем функцию new Counter(), wasm-loader вызывает new WebAssembly.Instance(module, importObject);

  • module является правильным образцом WebAssembly.Module;
  • importObject — это значение wasm-loader по умолчанию, которое по некоторым причинам не работает.

Но есть решение этой проблемы:

			import Counter from ‘./wasm/counter’
const wasmHelloWorld = () => {
const counter = new Counter({
‘env’: {
‘memoryBase’: 0,
‘tableBase’: 0,
‘memory’: new WebAssembly.Memory({initial: 256})
‘table’: new Webassembly.Table({initial: 0, element ‘anyfunc’})
}
})
Console.log(“count function result is:” + counter.exports._count());
}
window.onload = wasmHelloWorld
		

Теперь при перезагрузке страницы всё отображается правильно:

Как видите, было не очень просто заставить «Hello World» работать. В следующем разделе будет рассмотрен более простой способ интеграции JavaScript и WebAssembly.

Оптимизация движка игры

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

Что нужно сделать:

  1. Реализовать логику игры на C;
  2. Компилировать логику с C на wasm;
  3. Перевести wasm в код на JavaScript;
  4. Найти способ взаимодействия между кодом на C и JavaScript.

Компиляция кода на C в wasm со связками JavaScript

Обратите внимание на то, как был скомпилирован предыдущий пример с -s SIDE_MODULE=1. Это обеспечивает единый модуль, который нужно интегрировать в клиентский код с нуля. Вам следует знать, что он не позволяет вызывать функцию malloc в коде на C, которая выделяет блок памяти размером sizemem байт и возвращает указатель на начало блока. Это не очень серьёзная проблема для нашей «Hello World», но это станет проблемой, если у вас будут более масштабные проекты. К счастью, есть возможность компилировать код на C с помощью SDK Emscripten, благодаря которому будет создан модуль wasm и модуль JavaScript, который будет служить в качестве соединяющего звена, позволяющего интегрировать WebAssembly в клиентский код. В нашем случае это позволит вызывать функцию malloc и считывать информацию с выделенной памяти со стороны JavaScript.

Компиляция выполняется следующим образом:

			emcc engine.c -03 -o engine.js -s WASM=1 -Wall -s MODULARIZE=1
		

Используя MODULARIZE, мы помещаем весь код на JavaScript в функуцию. К сожалению, это не совсем JS-модуль (ни AMDdefine, ни CommonJS, ни ES6), поэтому мы просто добавим export {Module as default} к engine.js (исходный код), а всё остальное уже сделает WebPack, что позволит нам импортировать модуль в наш клиентский код на ES6:

			import Module ‘./wasm/engine.js’
module = Module({wasmBinaryFile: ‘wasm/engine.wasm’})
		

Нужно указать расширение при импорте, так как в той же папке уже имеется engine.wasm
wasmBinaryFile — это URL-адрес, который используется для асинхронного извлечения wasm-кода. Таким образом мы сообщаем Webpack, что нужно обработать его с помощью copy-webpack-plugin.

Вызов функций wasm из JavaScript

Emscripten не отображает функции языка C по умолчанию (однако исключения имеют место быть), поэтому нам следует сообщить, что нужно это сделать:

			#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
char *init(int w, int h) {
width = w;
height = h;
current = malloc(width * height * sizeof(char));
next = malloc(width * height * sizeof(char));
return current;
}
		

EMSCRIPTEN_KEEPALIVE делает именно то, что нужно — экспортирует функции. Теперь можно вызывать функцию module.asm._init(40,40), чтобы задать размер сетки 40 на 40 px, например.

Доступ к памяти wasm-модуля из JavaScript

SDK Emscripten удобным образом предоставляет память модуля через переменные module.HEAP*. Рекомендуемый способ взаимодействия с памятью — использование module.getValue и setValue. Но так как этот способ является более медленным, поэтому в данном случае мы будем напрямую взаимодействовать с HEAP8, принимая во внимание, что свойства содержатся в массиве символов.

Примечание Доступ к незадокументированным свойствам может быть нарушен в будущем.

Сейчас, когда все части приложения собраны вместе, давайте протестируем пробную версию.

Тестирование

Тестирование — это сложный и интересный шаг для каждого разработчика, но, возможно, это слово не очень подходит к такой программе, как «Hello World», поэтому следующая операция не должна расцениваться, как оценка производительности wasm. Код на C не оптимизирован для быстрой производительности, но он написан в виде простой реализации JavaScript. При этом мы можем посмотреть, будет ли он работать быстрее, чем стандартный JavaScript. Была произведена проверка производительности в браузере Chrome 58.

Оригинальный JavaScript-код:

Код на WebAssembly:

В среднем computeNextState, которая запускалась за ~40 мс, теперь запускается за ~15 мс. Это, конечно, не заоблачные достижения, но этого достаточно, чтобы увеличить FPS с 18 до 40. Улучшения были менее заметны в Firefox 53.

Вы можете поэкспериментировать с настройками URL, а также есть возможность переключаться между движками для сравнения.

Урок-введение по WebAssembly на примере игры «Жизнь» 1

Заключение

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

Представляем вам список дополнительных ресурсов, на которых вы сможете найти информацию по WebAssembly:

Следите за новыми постами
Следите за новыми постами по любимым темам
13К открытий14К показов