Работа с WebAssembly и JavaScript: от А до Я в Node.js

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в WebAssembly и его роль в Node.js

WebAssembly (WASM) — это низкоуровневый бинарный формат инструкций, который выполняется в виртуальной машине с почти нативной производительностью. В экосистеме Node.js WebAssembly открывает новые горизонты: позволяет выполнять код, написанный на C, C++, Rust или Go, прямо в серверной среде. Это особенно актуально для задач, требующих высокой производительности: обработка изображений, криптография, парсинг данных, работа с большими массивами чисел.

В этой статье мы разберем полный цикл работы с WebAssembly в Node.js: от компиляции исходного кода до интеграции готового модуля в JavaScript-приложение.

Основные понятия и подготовка окружения

Что нужно знать перед началом

WebAssembly модуль — это бинарный файл с расширением .wasm. Он не имеет доступа к DOM или файловой системе напрямую — все взаимодействие происходит через "импортируемые функции", которые вы передаете из JavaScript. В Node.js это ограничение снимается с помощью специальных оберток.

Установка инструментов

Для компиляции кода в WASM нам понадобится Emscripten SDK. Установка на Linux/macOS:

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh

Для Rust-проектов используйте wasm-pack:

npm install -g wasm-pack

Создание и компиляция первого WASM-модуля

Пример на C — функция сложения

Создадим простейшую функцию на C, которая складывает два числа:

// math.c
#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
    return a + b;
}

EMSCRIPTEN_KEEPALIVE
int multiply(int a, int b) {
    return a * b;
}

Компилируем в WASM с помощью Emscripten:

emcc math.c -o math.js -s EXPORTED_FUNCTIONS="['_add', '_multiply']" -s WASM=1 -s MODULARIZE=1 -s EXPORT_NAME="createMathModule"

На выходе получаем два файла: math.wasm и math.js — обертку для загрузки.

Загрузка модуля в Node.js

Теперь используем модуль в JavaScript:

// index.js
const createMathModule = require('./math.js');

createMathModule().then(module => {
    const add = module._add;
    const multiply = module._multiply;

    console.log('5 + 3 =', add(5, 3));        // 8
    console.log('4 * 7 =', multiply(4, 7));  // 28
});

Запускаем:

node index.js

Работа с памятью и сложными типами данных

Передача строк и массивов

WASM оперирует только числами. Для передачи строк или массивов нужно выделять память в линейной памяти модуля. Рассмотрим пример на C, который принимает строку и возвращает ее длину:

// string_ops.c
#include <string.h>
#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int string_length(char* str) {
    return strlen(str);
}

EMSCRIPTEN_KEEPALIVE
void to_upper(char* str) {
    for (int i = 0; str[i]; i++) {
        if (str[i] >= 'a' && str[i] <= 'z') {
            str[i] -= 32;
        }
    }
}

Компилируем:

emcc string_ops.c -o string_ops.js -s EXPORTED_FUNCTIONS="['_string_length', '_to_upper']" -s WASM=1 -s MODULARIZE=1 -s EXPORT_NAME="createStringModule"

JavaScript-код для работы с памятью:

const createStringModule = require('./string_ops.js');

createStringModule().then(module => {
    const { _string_length, _to_upper, heap } = module;
    
    // Выделяем память под строку
    const input = "hello world";
    const ptr = module._malloc(input.length + 1);
    
    // Копируем строку в память WASM
    for (let i = 0; i < input.length; i++) {
        heap[i + ptr] = input.charCodeAt(i);
    }
    heap[ptr + input.length] = 0; // null-terminator
    
    console.log('Длина строки:', _string_length(ptr)); // 11
    
    _to_upper(ptr);
    let result = '';
    for (let i = ptr; heap[i] !== 0; i++) {
        result += String.fromCharCode(heap[i]);
    }
    console.log('Результат:', result); // HELLO WORLD
    
    module._free(ptr); // Освобождаем память
});

Оптимизация производительности и best practices

Измерение производительности

Сравним выполнение чисто JavaScript-функции и WASM-функции на задаче вычисления чисел Фибоначчи:

// fib.c
EMSCRIPTEN_KEEPALIVE
int fib(int n) {
    if (n <= 1) return n;
    return fib(n-1) + fib(n-2);
}

Тест в Node.js:

// test_fib.js
const createFibModule = require('./fib.js');

// JavaScript версия
function fibJS(n) {
    if (n <= 1) return n;
    return fibJS(n-1) + fibJS(n-2);
}

async function test() {
    const module = await createFibModule();
    const fibWASM = module._fib;
    
    console.time('WASM fib(40)');
    console.log(fibWASM(40));
    console.timeEnd('WASM fib(40)');
    
    console.time('JS fib(40)');
    console.log(fibJS(40));
    console.timeEnd('JS fib(40)');
}

test();

Результат может показать прирост скорости в 2-5 раз в зависимости от задачи.

Советы по оптимизации