Я ни в коем случае не являюсь экспертом в WebAssembly. Мне кажется, что я все еще едва понимаю его и как правильно его использовать. Но я собираюсь это изменить.
WebAssembly — это новая горячая технология, которая предполагает загрузку кучи кода, напоминающего классический язык Ассемблер. Отличие этой технологии от JavaScript, который браузер уже понимает как родной, заключается в том, что браузер сам способен интерпретировать код WebAssembly гораздо быстрее.
Причины, по которым WebAssembly работает намного быстрее, сводятся к нескольким основным причинам:
- WebAssembly по своей природе меньше, что означает, что он может быстрее передавать код по Интернету.
- WebAssembly уже разобрана и оптимизирована, поэтому она позволяет избежать этих шагов
- WebAssembly компилируется, нет фазы компиляции при выполнении инструкций в браузере
- В WebAssembly нет сборки мусора, все управление осуществляется вручную.
WebAssembly работает лучше, чем JavaScript, во многих областях. Так почему же он до сих пор не прижился у многих людей?
Генерация WebAssembly
Первым шагом в разработке приложения на WebAssembly является поиск способа его создания — не советую вам искать 1000-страничный учебник и начинать учиться писать WebAssembly самостоятельно. Если только вы не хотите создать следующий Roller Coaster Tycoon.
WebAssembly выбран в качестве цели для кросс-компиляции в нескольких компиляторах. Можно написать JavaScript для WebAssembly, но JavaScript может оказаться не совсем подходящим для работы с памятью вручную.
Вместо этого мы часто обращаемся к таким языкам, как Rust или TypeScript, чтобы использовать WebAssembly. В Rust есть много языковых средств, которые отлично подходят для WebAssembly, а именно: компилятор Rust строго следит за корректностью памяти и предотвращает многие пространственные и временные манипуляции с памятью с помощью одного только синтаксиса.
Однако я не очень заинтересован в Rust, он слишком сложен для меня. Rust — это язык, который шире, чем C++, и собирает слишком много молотков в одном ящике с инструментами. Абстрактные типы данных, макросы, согласование шаблонов, устранение времени жизни, черты, владение и заимствование, асинхронное программирование — достаточно тем, чтобы заполнить два семестра курса информатики.
Существует более простой язык, который называется Zig. Zig — это новый язык, который еще не достиг версии 1.0, сейчас он находится на уровне 0.9.1. Но Zig, на мой взгляд, очень хороший язык. Он понятен, прост, и вы можете легко прочитать документацию. Он похож на классический C, и в некоторых местах заимствует идеи из других языков.
Причина, по которой мне также не очень нравится Rust, заключается в том, что он сильно зависит от генерации привязок WASM с помощью пакета wasm_bindgen
. Это опять же место, где магия просто «происходит» на фоне Rust, и вы должны смириться с этим и считать, что это всегда правильно.
Прелесть Zig в том, что он поддерживает множество кроссплатформенных целей, используя shims из LLVM, и почти любая платформа, которую поддерживает LLVM, будет поддерживаться Zig. Давайте попробуем написать код Zig для сложения двух знаковых целых чисел.
// add_two.zig
pub fn add_two(a: i32, b: i32) i32 {
return a + b;
}
Вот как выглядит код Zig, и он очень похож на код Rust с аннотациями типов. Ключевое слово pub
говорит Zig раскрыть эту функцию для остального программного пространства Zig, в котором вы пишете код.
Когда мы хотим написать код Zig, который будет превращен в программу WASM, мы собираем его как библиотеку, что является частью набора команд для компиляции кода Zig через build-lib
.
$ zig build-lib add_two.zig -target wasm32-freestanding -dynamic
Эта команда создаст для вас WASM-файл, который мы можем использовать в браузере. Для уверенности мы можем проверить его размер.
$ ls -alh add_two.wasm
-rwxr-xr-x 1 steve steve 42 Aug 4 16:47 add_two.wasm*
^- size is 42 bytes
Подождите… Сорок два байта?! Это кажется немного маловато. Мы что-то сделали не так? Не может быть, чтобы WASM был таким маленьким. К сожалению, мы неправильно написали Zig, но ничего, мы учимся.
Перепишите наш код, только вместо pub
мы будем использовать export
.
// add_two.zig
export fn add(a: i32, b: i32) i32 {
return a + b;
}
Скомпилируйте его снова:
$ zig build-lib add_two.zig -target wasm32-freestanding -dynamic
Проверьте его содержимое с помощью:
$ ls -alh add_two.wasm
-rwxr-xr-x 1 steve steve 329 Aug 4 16:49 add_two.wasm*
Намного лучше, 329 байт! Причина, по которой нам понадобилось использовать export
, заключается в том, чтобы сообщить Zig, что мы хотим создать то, что фактически является объектом разделяемой библиотеки, который экспортирует определения и код. Раньше мы не указывали, что пытаемся создать библиотеку, поэтому Zig не видел экспортируемых привязок и экспортировал WASM-файл, лишенный всякого смысла.
В конце концов, было бы неплохо иметь возможность собирать проект Zig без необходимости самостоятельно писать команду zig
. Возможно, я расскажу об этом позже, но сейчас полезно понимать, что означают некоторые флаги.
Импортирование WASM
Вот раздел, который стоит просмотреть. У нас есть блоб, но как нам перенести его на нашу веб-страницу, запустить его и все такое?
Первый шаг заключается в том, что мы должны каким-то образом доставить код в целом. Это можно сделать одним из двух способов: либо с помощью сетевого запроса, либо путем преобразования в формат base-64 и встраивания в веб-страницу. Оба способа имеют свои преимущества и недостатки, но я буду использовать сначала сетевой метод, так как это более распространенный случай.
Когда-то давно в JavaScript появилась функция XMLHttpRequest
, которая позволяла выполнять сетевой вызов с текущей веб-страницы и произвольно получать или выполнять некоторую функцию удаленно. Это стало поворотным моментом для большинства, если не всех, современных веб-сайтов. XMLHttpRequest
некоторое время был королем, пока гораздо позже не появились новые методы.
Новый Fetch API — это обычная реализация браузера, призванная быть умнее XMLHttpRequest
и элегантнее, но я немного старожил. XMLHttpRequest
прост, поэтому сейчас я буду придерживаться его.
Сначала отредактируйте свою веб-страницу так, чтобы она выглядела следующим образом:
<!doctype html>
<html>
<head>
<title>WASM Demo</title>
</head>
<body>
<h3>WASM Demo</h3>
</body>
<script src="loader.js"></script>
</html>
Затем нам нужно отредактировать loader.js
. Мы помещаем тег <script>
после всего остального HTML на странице, потому что в терминах JavaScript проще работать с элементами DOM, если они действительно существуют, вместо того, чтобы ждать, пока их существование будет определено первым. Так он меньше раздражает. Но вы можете поместить его куда угодно, в зависимости от вашего приложения.
Внутри loader.js
нам нужно загрузить наш WASM-блок через специальный процесс инициализации через модуль WebAssembly
под названием initialize
. Это основной процесс для загрузки WASM, хотя существует и более новая функция для поддержки потокового буфера, но сейчас мы перейдем к классическому варианту.
// loader.js
request = new XMLHttpRequest();
request.open('GET', 'add_two.wasm');
request.responseType = 'arraybuffer';
request.send();
request.onload = function() {
var bytes = request.response;
WebAssembly.instantiate(bytes, {
env: {}
}).then(result => {
// do wasm things here!
var add_two = result.instance.exports.add_two;
console.log(add_two(3, 5));
});
};
Вуаля! Код WASM загружен на наш сайт, и JavaScript может его вызвать.
Функция instantiate
— это способ принять массив байтов, представляющий наш WASM-код, который мы запросили в виде массива в строке 3. Далее следует событие, которое мы определяем для запуска после успешной загрузки WASM-кода, это функция .then()
. Именно здесь мы и должны работать, поскольку этот код будет выполняться только тогда, когда WASM будет полностью готов к использованию.
Проблема в том, что вам нужен работающий HTTP-сервер, чтобы обслуживать это. Большинство браузеров имеют строгие настройки CORS, которые запрещают загружать файлы локально, поэтому нам нужно запустить быстрый HTTP-сервер. Если у вас установлен Python, вы можете сделать следующее, перейдите по адресу 0.0.0.0:8000
и найдите ваш индексный файл там, где вы его разместили.
$ python -m http.server
Все, теперь у нас есть работающее WASM-приложение. Ну, если можно назвать сложение двух чисел рабочим приложением.
Импорт функций JavaScript
Когда мы пишем код в Zig, мы имеем ограниченный доступ к функциям, которые существуют только в других местах браузера. Например, мы не можем использовать API Canvas или API WebAudio. Zig не имеет никакого отношения к этим API, и без какого-либо кода Zig для поддержки этих API наш компилятор Zig останется в пыли.
Однако не все потеряно. Давайте начнем с главного вопроса: как печатать текст изнутри Zig?
Традиционным способом печати текста в Zig является использование библиотеки std.io
, но этой библиотеке нужен какой-то поток вывода для записи текста, а у нас его нет. Вместо этого лучше импортировать ссылку на пространство имен JavaScript с помощью поля env
функции instantiate
, используемой для запуска кода WASM.
Это поле env
можно использовать для привнесения внешних ссылок в Zig (или любой другой язык), чтобы вы могли использовать собственные функции JavaScript в своей программе Zig.
Сначала давайте изменим add_two.zig
.
// add_two.zig
extern fn print(a: i32);
export fn add(a: i32, b: i32) i32 {
print(a + b); // try it here
return a + b;
}
Здесь мы используем ключевое слово extern
для ссылки на функцию, находящуюся вне области действия нашей программы, которая должна быть вызвана процессом Zig (или каким-то другим процессом). Обычно это важно при работе с библиотеками C, для которых нет собственного кода Zig, но которые мы можем использовать на основе их объектного файла на этапе сборки.
Теперь нам нужно ввести ссылку через секцию env
на какую-нибудь функцию принтера. Мы могли бы импортировать console.log
, но на всякий случай я создам обертку.
// loader.js
request = new XMLHttpRequest();
request.open('GET', 'life.wasm');
request.responseType = 'arraybuffer';
request.send();
request.onload = function() {
var bytes = request.response;
WebAssembly.instantiate(bytes, {
env: {
print: function(x) { console.log(x); }
}
}).then(result => {
// do wasm things here!
var add_two = result.instance.exports.add_two;
console.log(add_two(3, 5));
});
};
Для дальнейшей отладки не помешает всегда иметь при себе функцию печати, подобную этой. Когда ваш код превратится в WASM-файл, отладчик браузера поможет вам только до того, как вы столкнетесь с проблемами логики или другими проблемами. Используйте функцию печати для собственного спокойствия.
Режимы выпуска
Последняя часть, на которой я остановлюсь, — это режимы выпуска, доступные в Zig. Почти для всех компиляторов существуют различные режимы сборки, которые вы используете для выпуска своего проекта. Некоторые из моих небольших проектов WASM в итоге компилировались до тысяч байт, что обычно не вызывает у меня нареканий, потому что в среднем это все равно неплохо.
Однако я не использовал правильный режим выпуска для этого. В Zig есть несколько флагов, которые можно установить в компилятор для оптимизации бинарного релиза.
- Debug, быстрая компиляция, включены проверки безопасности, низкая производительность во время выполнения, очень большой размер.
- ReleaseFast, высокая производительность, проверки безопасности отключены, медленная компиляция, большой размер
- ReleaseSafe, средняя производительность, проверки безопасности включены, медленная компиляция, большой размер
- ReleaseSmall, средняя производительность, безопасность отключена, небольшой двоичный вывод, наименьший размер
Режим отладки идеально подходит для создания прототипов, но для выпуска релиза вам, вероятно, понадобится один из трех последующих. ReleaseSmall очень заманчив, но если вам нужна хорошая производительность, то вместо него лучше рассмотреть ReleaseFast. В приложениях, критичных к безопасности, вы, вероятно, должны стремиться использовать только ReleaseSafe.
В одной из моих небольших демонстраций, создающих генератор множества Мандельброта, я собираю файл Zig с помощью обычной команды build-lib
.
$ zig build-lib mandelbrot.zig -target wasm32-freestanding -dynamic
Исходный размер получается:
$ ls -alh mandelbrot.wasm
-rwxr-xr-x 1 steve steve 915 Aug 5 10:22 mandelbrot.wasm*
Неплохо, но после выполнения ReleaseFast:
$ zig build-lib mandelbrot.zig -target wasm32-freestanding -dynamic -O ReleaseFast
$ ls -alh mandelbrot.wasm
-rwxr-xr-x 1 steve steve 232 Aug 5 10:25 mandelbrot.wasm*
получается целых 232 байта. Очень круто!
В следующем посте я рассмотрю некоторые более продвинутые темы использования Zig с WASM, представлю больше возможностей Zig, таких как перечисления или структуры, и попытаюсь создать больше программ, используя Zig и WASM вместе.
Я надеюсь пролить немного больше света на Zig и WASM, поскольку я нахожу текущие ресурсы немного скудными. У меня есть интерес к этой области, и я с удовольствием исследую ее в настоящее время с помощью Zig.
Спасибо, что читаете!
(Предупреждения: Zig все еще находится на стадии бета-версии, поэтому в будущем все может измениться. Тем не менее, это все равно весело).