WebAssembly (Wasm) позволяет запускать код Rust в браузерах со скоростью, близкой к нативной. Это открывает возможности для вычислительно интенсивных задач — таких как игры, обработка изображений и криптография, — с которыми JavaScript не справляется эффективно. В этом руководстве рассматривается сборка модулей Wasm на Rust с точки зрения senior-разработчика.
Почему Rust + WebAssembly
Эта комбинация превосходна, потому что:
- Производительность: скорость, близкая к нативной, в браузерах
- Безопасность: гарантии Rust сохраняются и в Wasm
- Размер: бинарные файлы меньше, чем у C/C++
- Interop: бесшовная интеграция с JavaScript
- Портативность: работает везде, где работает Wasm
Сценарии использования: игры, обработка аудио/видео, криптография, симуляции, парсеры и любая CPU-интенсивная работа.
Настройка
Установите Rust
# Установите Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Добавьте target Wasm
rustup target add wasm32-unknown-unknown
# Установите wasm-pack (рекомендуется)
cargo install wasm-pack
# Установите wasm-bindgen-cli (для ручных сборок)
cargo install wasm-bindgen-cli
Структура проекта
# Создайте новый проект
cargo new --lib my-wasm
cd my-wasm
my-wasm/
├── Cargo.toml
├── src/
│ └── lib.rs
├── pkg/ # Сгенерировано wasm-pack
│ ├── my_wasm.js
│ ├── my_wasm_bg.wasm
│ └── package.json
└── www/ # Опционально: веб-демо
└── index.html
Конфигурация
Cargo.toml
[package]
name = "my-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3" # Типы JavaScript
web-sys = "0.3" # Web API
# Для отладки через console.log
console_error_panic_hook = "0.1"
[dev-dependencies]
wasm-bindgen-test = "0.3"
[profile.release]
# Оптимизация под размер
opt-level = "s"
lto = true
Базовый пример
Простая функция
src/lib.rs:
use wasm_bindgen::prelude::*;
// Вызывается при инстанцировании wasm-модуля
#[wasm_bindgen(start)]
pub fn main() {
// Установите panic hook для более понятных сообщений об ошибках
console_error_panic_hook::set_once();
}
// Экспортируйте простую функцию
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// Экспортируйте функцию, которая принимает и возвращает числа
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// Экспортируйте функцию, которая работает с массивами
#[wasm_bindgen]
pub fn sum(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}
Сборка
# Сборка для web (рекомендуется)
wasm-pack build --target web
# Сборка для bundler’ов (webpack и т. п.)
wasm-pack build --target bundler
# Сборка для Node.js
wasm-pack build --target nodejs
# Release-сборка с оптимизациями
wasm-pack build --target web --release
Использование в HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Rust Wasm Demo</title>
</head>
<body>
<h1>Rust WebAssembly</h1>
<div id="output"></div>
<script type="module">
import init, { greet, add, sum } from './pkg/my_wasm.js';
async function run() {
// Initialize the Wasm module
await init();
// Call Rust functions
const greeting = greet("World");
console.log(greeting); // "Hello, World!"
const result = add(5, 3);
console.log("5 + 3 =", result); // 8
const total = sum(new Int32Array([1, 2, 3, 4, 5]));
console.log("Sum:", total); // 15
document.getElementById('output').textContent = greeting;
}
run();
</script>
</body>
</html>
Interop с JavaScript
Вызов JavaScript из Rust
use wasm_bindgen::prelude::*;
// Импортируйте функции JavaScript
#[wasm_bindgen]
extern "C" {
// console.log
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
// alert
fn alert(s: &str);
// Пользовательская функция JavaScript
#[wasm_bindgen(js_namespace = window)]
fn customCallback(value: i32);
}
#[wasm_bindgen]
pub fn rust_function() {
log("Called from Rust!");
alert("Hello from Rust!");
}
Работа с DOM
Включите функции web-sys в Cargo.toml:
[dependencies.web-sys]
version = "0.3"
features = [
"Document",
"Element",
"HtmlElement",
"Window",
"console",
]
use wasm_bindgen::prelude::*;
use web_sys::{Document, Element, Window};
#[wasm_bindgen]
pub fn manipulate_dom() -> Result<(), JsValue> {
let window: Window = web_sys::window().expect("no window");
let document: Document = window.document().expect("no document");
// Создайте элемент
let element: Element = document.create_element("div")?;
element.set_inner_html("<h1>Created by Rust!</h1>");
element.set_attribute("class", "rust-created")?;
// Добавьте в body
let body = document.body().expect("no body");
body.append_child(&element)?;
Ok(())
}
Callback’и и замыкания
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn set_timeout_rust(callback: &js_sys::Function, delay: i32) {
let window = web_sys::window().expect("no window");
window.set_timeout_with_callback_and_timeout_and_arguments_0(
callback,
delay
).unwrap();
}
// Замыкание Rust в JavaScript
#[wasm_bindgen]
pub fn create_callback() -> JsValue {
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
web_sys::console::log_1(&format!("Clicked at: {}, {}", event.client_x(), event.client_y()).into());
}) as Box<dyn FnMut(_)>);
let js_value = closure.as_ref().clone();
closure.forget(); // Предотвратите освобождение замыкания Rust
js_value
}
Struct’ы и классы
Экспорт Struct как класса
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct Counter {
value: i32,
}
#[wasm_bindgen]
impl Counter {
#[wasm_bindgen(constructor)]
pub fn new(initial: i32) -> Counter {
Counter { value: initial }
}
pub fn increment(&mut self) {
self.value += 1;
}
pub fn decrement(&mut self) {
self.value -= 1;
}
pub fn get(&self) -> i32 {
self.value
}
pub fn set(&mut self, value: i32) {
self.value = value;
}
}
Использование в JavaScript:
import init, { Counter } from './pkg/my_wasm.js';
await init();
const counter = new Counter(10);
console.log(counter.get()); // 10
counter.increment();
counter.increment();
console.log(counter.get()); // 12
counter.free(); // Очистите память Wasm
Практический пример: обработка изображений
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn grayscale(data: &mut [u8]) {
// Обработайте данные пикселей RGBA
for pixel in data.chunks_exact_mut(4) {
let r = pixel[0] as f32;
let g = pixel[1] as f32;
let b = pixel[2] as f32;
// Метод яркости (luminosity)
let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
pixel[0] = gray;
pixel[1] = gray;
pixel[2] = gray;
// pixel[3] — это alpha, оставьте без изменений
}
}
#[wasm_bindgen]
pub fn invert(data: &mut [u8]) {
for pixel in data.chunks_exact_mut(4) {
pixel[0] = 255 - pixel[0];
pixel[1] = 255 - pixel[1];
pixel[2] = 255 - pixel[2];
}
}
#[wasm_bindgen]
pub fn brightness(data: &mut [u8], adjustment: i32) {
for pixel in data.chunks_exact_mut(4) {
pixel[0] = ((pixel[0] as i32 + adjustment).clamp(0, 255)) as u8;
pixel[1] = ((pixel[1] as i32 + adjustment).clamp(0, 255)) as u8;
pixel[2] = ((pixel[2] as i32 + adjustment).clamp(0, 255)) as u8;
}
}
Интеграция с JavaScript:
import init, { grayscale } from './pkg/my_wasm.js';
async function processImage() {
await init();
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Получите данные изображения
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Обработайте с помощью Rust
grayscale(imageData.data);
// Запишите обработанные данные обратно
ctx.putImageData(imageData, 0, 0);
}
Тестирование
Unit-тесты
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}
Wasm-тесты
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn test_greet() {
assert_eq!(greet("Test"), "Hello, Test!");
}
#[wasm_bindgen_test]
fn test_in_browser() {
let window = web_sys::window().expect("no window");
assert!(window.location().href().is_ok());
}
Запуск тестов:
# Запустите unit-тесты
cargo test
# Запустите Wasm-тесты в headless-браузере
wasm-pack test --headless --chrome
wasm-pack test --headless --firefox
Оптимизация
Минимизация размера
[profile.release]
opt-level = "z" # Оптимизация под размер
lto = true # Оптимизация на этапе линковки (LTO)
codegen-units = 1 # Один codegen unit
panic = "abort" # Abort при panic
strip = true # Удалите символы
# Используйте wasm-opt для дополнительной оптимизации
wasm-pack build --release
# Или вручную
wasm-opt -Oz pkg/my_wasm_bg.wasm -o pkg/my_wasm_bg.wasm
Измерение размера
# Проверьте размер файла
ls -lh pkg/*.wasm
# Проанализируйте секции
wasm-objdump -h pkg/my_wasm_bg.wasm
Интеграция с инструментами сборки
Webpack
// webpack.config.js
module.exports = {
experiments: {
asyncWebAssembly: true
}
};
// В вашем приложении
import * as wasm from 'my-wasm';
wasm.greet("Webpack");
Vite
// vite.config.js
import wasm from 'vite-plugin-wasm';
export default {
plugins: [wasm()]
};
Ключевые выводы
- Используйте wasm-pack: упрощает сборку и упаковку
- Начинайте с малого: сначала экспортируйте простые функции
- Управление памятью: вызывайте
.free() для Rust struct’ов - Оптимизируйте под размер: release-сборки с корректными настройками
- Тестируйте в браузере: некоторые API работают только в браузере
- Выбирайте правильный target: web, bundler или nodejs
WebAssembly с Rust открывает новые возможности для производительности веба — используйте это для вычислительно интенсивных задач, которые в JavaScript были бы слишком медленными.