Введение: зачем Rust и WebAssembly на продвинутом уровне
WebAssembly (Wasm) и Rust — это комбинация, которая позволяет перенести вычислительно тяжелые задачи из JavaScript в высокопроизводительный нативный код, работающий в браузере. На базовом уровне вы уже могли познакомиться с компиляцией простых функций. Однако для создания реальных продуктов — игровых движков, обработчиков изображений, парсеров или криптографических библиотек — требуется глубокое понимание управления памятью, передачи сложных структур данных и взаимодействия с внешней средой.
В этой статье мы рассмотрим продвинутые техники работы с WebAssembly на Rust: от ручного управления памятью до создания гибридных приложений, где Rust и JavaScript работают как единое целое. Материал ориентирован на разработчиков, уже знакомых с основами Rust и Wasm.
1. Управление памятью: линейная память и аллокаторы
В отличие от JavaScript, WebAssembly использует линейную память — плоский массив байтов. Rust-код, скомпилированный в Wasm, работает с этой памятью напрямую. Для эффективной работы необходимо понимать, как распределять и освобождать память вручную.
1.1. Ручное выделение памяти с wasm_bindgen
Библиотека wasm-bindgen предоставляет удобные абстракции, но на продвинутом уровне часто требуется прямой доступ к памяти. Рассмотрим пример:
use wasm_bindgen::prelude::*;
use std::mem;
use std::slice;
#[wasm_bindgen]
pub fn allocate_buffer(size: usize) -> *mut u8 {
let mut buf = Vec::with_capacity(size);
let ptr = buf.as_mut_ptr();
mem::forget(buf); // предотвращаем удаление вектора
ptr
}
#[wasm_bindgen]
pub fn deallocate_buffer(ptr: *mut u8, size: usize) {
unsafe {
let _ = Vec::from_raw_parts(ptr, 0, size);
}
}
#[wasm_bindgen]
pub fn fill_buffer(ptr: *mut u8, value: u8, len: usize) {
let slice = unsafe { slice::from_raw_parts_mut(ptr, len) };
for byte in slice.iter_mut() {
*byte = value;
}
}
Здесь мы выделяем память через Vec, "забываем" о нём (чтобы Rust не удалил буфер) и передаём указатель в JavaScript. Функция deallocate_buffer восстанавливает вектор и корректно освобождает память.
1.2. Использование пользовательского аллокатора
Для сложных проектов стоит заменить стандартный аллокатор на более предсказуемый. Например, wee_alloc — маленький аллокатор, оптимизированный для Wasm:
// в Cargo.toml
// [dependencies]
// wee_alloc = "0.4"
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
Это уменьшит размер итогового .wasm файла и ускорит выделение памяти в браузере.
2. Передача сложных структур данных между Rust и JavaScript
На продвинутом уровне вы не ограничиваетесь числами и строками. Необходимо передавать массивы, структуры и даже замыкания.
2.1. Структуры с #[wasm_bindgen]
Вы можете экспортировать целые Rust-структуры как классы JavaScript:
#[wasm_bindgen]
pub struct Matrix {
rows: usize,
cols: usize,
data: Vec<f64>,
}
#[wasm_bindgen]
impl Matrix {
#[wasm_bindgen(constructor)]
pub fn new(rows: usize, cols: usize) -> Matrix {
Matrix {
rows,
cols,
data: vec![0.0; rows * cols],
}
}
pub fn get(&self, row: usize, col: usize) -> f64 {
self.data[row * self.cols + col]
}
pub fn set(&mut self, row: usize, col: usize, val: f64) {
self.data[row * self.cols + col] = val;
}
pub fn multiply(&self, other: &Matrix) -> Result<Matrix, String> {
if self.cols != other.rows {
return Err("Несовместимые размеры".to_string());
}
// ... реализация умножения матриц
let result = Matrix::new(self.rows, other.cols);
Ok(result)
}
}
В JavaScript вы сможете использовать этот класс нативно:
const m1 = new Matrix(2, 3);
m1.set(0, 0, 1.0);
const m2 = new Matrix(3, 2);
const m3 = m1.multiply(m2);
2.2. Передача замыканий и колбэков
Иногда нужно, чтобы Rust вызывал JavaScript-функцию (например, для рендеринга или логирования). Для этого используйте Closure:
use wasm_bindgen::prelude::*;
use js_sys::Function;
#[wasm_bindgen]
pub fn run_with_callback(callback: &Function) {
let closure = Closure::wrap(Box::new(move || {
callback.call0(&JsValue::null()).unwrap();
}) as Box<dyn Fn()>);
// Запускаем асинхронную работу
closure.forget(); // предотвращаем удаление
}
Важно: closure.forget() предотвращает сборку мусора Rust, поэтому в JavaScript нужно явно вызывать .free(), когда замыкание больше не нужно.
3. Оптимизация производительности и сборка проекта
Продвинутая работа с Wasm требует тонкой настройки компилятора и профилирования.
3.1. Оптимизация размера и скорости
В Cargo.toml добавьте:
[profile.release]
opt-level = 'z' # оптимизация размера
lto = true # Link-Time Optimization
codegen-units = 1
Для максимальной производительности используйте opt-level = 3, но размер .wasm увеличится. Рекомендуется также включить wasm-opt:
wasm-pack build --release
wasm-opt -Oz -o pkg/optimized.wasm pkg/your_crate_bg.wasm
3.2. Асинхронные вызовы и Promise
Rust-функции могут возвращать Promise для асинхронной работы:
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use js_sys::Promise;
#[wasm_bindgen]
pub async fn fetch_data(url: String) -> Result<JsValue, JsValue> {
let promise = web_sys