Загрузка и запуск Wasm из JavaScript

Учимся подключать Wasm-модуль из JavaScript и вызывать его функции.

WebAssembly.instantiate — это функция JS, которая компилирует байты модуля и создаёт работающий экземпляр с доступными экспортами.

Три шага загрузки

Чтобы запустить Wasm из браузера, нужно: получить байты .wasm, скомпилировать и инстанцировать их, затем вызвать экспортированные функции. Самый современный способ — потоковая компиляция прямо из ответа сети:

// браузер: загрузка и запуск .wasm
const response = await fetch("add.wasm");
const { instance } = await WebAssembly.instantiateStreaming(response, {});
const result = instance.exports.add(2, 3);
console.log(result); // 5

Помечаем этот блок как language-text, потому что fetch и WebAssembly требуют браузерного окружения и реального файла — в песочнице урока он не запустится. Но логика именно такая.

Универсальный способ через байты

Если потоковая загрузка недоступна, сначала берут ArrayBuffer, потом инстанцируют:

const response = await fetch("add.wasm");
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes, {});
console.log(instance.exports.add(10, 20)); // 30

Передача импортов

Второй аргумент instantiate — это объект импортов. Если модуль импортирует console.log, мы передаём его реализацию здесь:

const importObject = {
  console: {
    log: (x) => console.log("из Wasm:", x)
  }
};
const { instance } = await WebAssembly.instantiate(bytes, importObject);
instance.exports.run();

Понимаем вызов изнутри на JS

Сам вызов Wasm-функции из JS выглядит как обычный вызов JS-функции. Покажем эквивалент того, что делает instance.exports.add, обычной функцией — числа передаются напрямую и быстро:

// то, как выглядит вызов экспорта Wasm со стороны JS
const exportsAdd = (a, b) => a + b;   // имитация instance.exports.add
console.log("add(2, 3) =", exportsAdd(2, 3));
console.log("add(100, 250) =", exportsAdd(100, 250));

Вывод:

add(2, 3) = 5
add(100, 250) = 350

В Node.js

В Node всё похоже, только байты читают из файла через модуль fs:

const fs = require("fs");
const bytes = fs.readFileSync("add.wasm");
const { instance } = await WebAssembly.instantiate(bytes, {});
console.log(instance.exports.add(2, 3));

Как работает под капотом

За кулисами instantiate делает два дела: компилирует байты в машинный код (это можно сделать заранее через WebAssembly.compile) и инстанцирует — создаёт экземпляр с собственной памятью, таблицами и связанными импортами. instantiateStreaming начинает компиляцию ещё до того, как файл полностью скачался, экономя время. Возвращается объект с module (скомпилированный код, можно переиспользовать) и instance (живой экземпляр с exports).

Частые ошибки

  • Неверный MIME-тип для Streaming — сервер должен отдавать .wasm с Content-Type: application/wasm, иначе instantiateStreaming ругается.
  • Забыли awaitinstantiate возвращает Promise; без await получите не результат, а сам Promise.
  • Не передали импорты — если модуль их ждёт, инстанцирование упадёт.

Итоги

  • Поток: получить байты → WebAssembly.instantiate(Streaming) → вызвать instance.exports.
  • Импорты передаются вторым аргументом как объект.
  • instantiateStreaming компилирует на лету, экономя время.
  • Вызов экспорта из JS выглядит как обычный вызов функции.
Проверьте себя
1. Что возвращает WebAssembly.instantiate?
AТолько массив байт
BPromise с объектом, содержащим module и instance (с exports)
CГотовую строку с результатом
DНичего, работает синхронно
2. Зачем нужен второй аргумент instantiate (importObject)?
AДля ускорения
BЧтобы передать модулю реализации функций, которые он импортирует
CЧтобы задать имя файла
DОн не нужен никогда
3. Чем хорош instantiateStreaming по сравнению с обычным путём?
AОн не требует сети
BНачинает компиляцию ещё до полной загрузки файла, экономя время
CОн работает без экспортов
DОн не требует await