
Дізнайтесь більше про нові кар'єрні можливості в EchoUA. Цікаві проекти, ринкова оплата, гарний колектив. Надсилайте резюме та приєднуйтеся до нас.
У цьому уроці ми пройдемо шлях по портированню бібліотеки JavaScript в WebAssembly (wasm) на прикладі гри “Життя”, створеною англійським математиком Джоном Конвеєм. Цей урок чудово підійде початківцям, щоб зрозуміти, що стоїть за банальним “Hello World”! у WebAssembly.
WebAssembly – новий стандарт, що дозволяє працювати веб-додаткам на рівні, наближеному до нативних настільних додатків. Оскільки стандарт новий, він активно розвивається. Саме через швидкий розвиток і старіння інформації початок роботи може виявитися досить важким.
Незважаючи на список відмінних посилань і статей, який є хорошим відправним пунктом у світ програмування на wasm, авторові все одно довелося витратити два дні для створення робочого коду. У кінці статті ви зможете ознайомитися з демо-версією гри, яку зараз ми намагатимемося повторити.
Слід зазначити, що для написання уроку використовувалася операційна система Ubuntu 17.04, можливо, процес відрізнятиметься, якщо у вас інша операційна система. Передбачається, що ви можете бути незнайомі з WebAssembly, тому все створюватиметься з нуля. У статті не акцентуватиметься увага на ES6 і Webpack. У Інтернеті ви зможете знайти більш розгорнуте керівництво, завдяки якому ви зможете детально ознайомитися з цими інструментами. Наприклад, у цій статті.
Прим. перев.Також пропонуємо вам прочитати нашу статтю про Webpack.
План уроку :
- Встановлення набору інструментальних засобів.
- Інтеграція JavaScript.
- Оптимізація движку.
- Тестування.
- Висновок.
Встановлення набору інструментальних засобів
Для швидкого встановлення останньої версії 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- модуля буде хорошою вправою.
Що треба зробити:
- Реалізувати логіку гри на C;
- Компілювати логіку з C на wasm;
- Перевести wasm в код на JavaScript;
- Знайти спосіб взаємодії між кодом на 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.
- Версія з 5 пікселями на одну клітину зі швидкістю до 60 FPS
- Версія з 1 пікселем на одну клітину зі швидкістю до 60 FPS
Ви можете поекспериментувати з налаштуваннями URL, а також є можливість перемикатися між движками для порівняння.
Висновок
Почати працювати з WebAssembly здається складніше, ніж є, проте, все виглядає багатообіцяюче. Але все-таки потрібна оптимізація і доопрацювання, оскільки доводиться використати SDK Emscripten в якості проміжного шару.
Представляємо вам перелік додаткових ресурсів, на яких ви зможете знайти інформацію, WebAssembly:
- Перелік пізнавальних ресурсів, книг і статтей.
- Взаємодія коду на JavaScript з кодом на C.
- Документація по WebAssembly.
- Чому WebAssembly може змінити хід подій в сучасному веб-просторі.
Переклад статті “WebAssembly 101: a developer’s first steps”
Київ, Харків, Одеса, Дніпро, Запоріжжя, Кривий Ріг, Вінниця, Херсон, Черкаси, Житомир, Хмельницький, Чернівці, Рівне, Івано-Франківськ, Кременчук, Тернопіль, Луцьк, Ужгород, Кам'янець-Подільський, Стрий - за статистикою саме з цих міст програмісти найбільше переїжджають працювати до Львова. А Ви розглядаєте relocate?