Rust в Wasm: wasm-pack и wasm-bindgen

Разбираем, почему Rust считается лучшим языком для Wasm.

wasm-bindgen — это инструмент, который автоматически генерирует «клей» между Rust и JS, скрывая ручную работу с памятью.

Почему Rust и Wasm — пара

Rust подходит к Wasm почти идеально по трём причинам. Во-первых, у Rust нет сборщика мусора — он управляет памятью через систему владения на этапе компиляции, что точно ложится на безGC-модель Wasm и даёт компактные модули. Во-вторых, Rust безопасен по памяти без накладных расходов — нет утечек и обращений за границу. В-третьих, у Rust первоклассная поддержка Wasm как цели компиляции прямо «из коробки».

Функция на Rust

Вот функция на Rust, готовая к экспорту в Wasm. Исходник Rust помечаем language-text — он компилируется тулчейном, а не запускается в браузере:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Привет, {}!", name)
}

Атрибут #[wasm_bindgen] помечает функции для экспорта. Обратите внимание на greet: она принимает и возвращает строку — и нам не пришлось вручную возиться с адресами и длинами. Это и есть магия wasm-bindgen.

Сборка одной командой

Инструмент wasm-pack компилирует Rust в Wasm и упаковывает с JS-обёрткой и типами:

wasm-pack build --target web
# создаст pkg/ с .wasm, .js-обёрткой и .d.ts типами

Использование из JS

После сборки строковая функция вызывается из JS естественно, будто это обычная JS-функция:

import init, { add, greet } from "./pkg/my_module.js";
await init();
console.log(add(2, 3));        // 5
console.log(greet("Аня"));     // "Привет, Аня!"

Что делает wasm-bindgen за вас

Помните ручную возню из урока про строки — кодировать в UTF-8, выделять память, передавать адрес и длину, потом освобождать? wasm-bindgen генерирует весь этот код автоматически. Проверим эквивалент того, что происходит внутри greet, на JS:

function greet(name) {            // как Rust-функция после клея bindgen
  return "Привет, " + name + "!";
}
console.log(greet("Аня"));
console.log(greet("мир"));

Вывод:

Привет, Аня!
Привет, мир!

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

На уровне Wasm функция greet всё равно принимает указатель и длину, а возвращает указатель на результат. wasm-bindgen генерирует две части: Rust-код, который раскодирует входные байты в &str и закодирует результат, и JS-код, который кодирует строку в память перед вызовом и декодирует результат после. Эти две стороны согласованы по формату. Вы пишете обычный Rust и обычный JS, а весь «протокол памяти» между ними генерируется.

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

  • Забыть #[wasm_bindgen] — функция не попадёт в экспорт и не будет видна из JS.
  • Не дождаться init() — обёртка асинхронно грузит .wasm; вызов до инициализации упадёт.
  • Передавать тяжёлые структуры на каждый кадр — даже с bindgen копирование строк не бесплатно; для горячих циклов держите данные в памяти Wasm.

Итоги

  • Rust идеален для Wasm: нет GC, безопасность памяти, поддержка из коробки.
  • #[wasm_bindgen] экспортирует функции и включает обмен сложными типами.
  • wasm-pack build собирает .wasm + JS-обёртку + типы.
  • wasm-bindgen автоматизирует протокол памяти для строк и структур.
Проверьте себя
1. Почему Rust особенно хорошо подходит для Wasm?
AУ Rust большой рантайм с GC
BНет GC, безопасность памяти без накладных расходов, поддержка Wasm из коробки
CRust компилируется в JS, а не в Wasm
DRust медленнее C, что удобно для отладки
2. Что автоматизирует wasm-bindgen?
AТолько сжатие .wasm
BГенерацию клея для обмена сложными типами (строки, структуры) между Rust и JS
CЗапуск Rust на сервере
DПеревод Rust в Python
3. Почему нужно дождаться init() перед вызовом функций?
AЭто требование синтаксиса Rust
BОбёртка асинхронно загружает .wasm; до инициализации экземпляра нет
Cinit() очищает память
DБез init() функции работают медленно