Урок-вступ до WebAssembly на прикладі гри “Життя”


Дізнайтесь більше про нові кар'єрні можливості в EchoUA. Цікаві проекти, ринкова оплата, гарний колектив. Надсилайте резюме та приєднуйтеся до нас.

У цьому уроці ми пройдемо шлях по портированню бібліотеки 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_KEEPALIVEchar *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 здається складніше, ніж є, проте, все виглядає багатообіцяюче. Але все-таки потрібна оптимізація і доопрацювання, оскільки доводиться використати SDK Emscripten в якості проміжного шару.

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

Переклад статті “WebAssembly 101: a developer’s first steps”

Київ, Харків, Одеса, Дніпро, Запоріжжя, Кривий Ріг, Вінниця, Херсон, Черкаси, Житомир, Хмельницький, Чернівці, Рівне, Івано-Франківськ, Кременчук, Тернопіль, Луцьк, Ужгород, Кам'янець-Подільський, Стрий - за статистикою саме з цих міст програмісти найбільше переїжджають працювати до Львова. А Ви розглядаєте relocate?


Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *