Rust и WebAssembly
WebAssembly (часто сокращают до Wasm) позволяет запускать код на языках вроде Rust прямо в браузере, рядом с JavaScript. Представь второй двигатель в браузере: страницей по-прежнему управляет JavaScript, но тяжёлую работу — фильтры для картинок, физику, разбор больших файлов — можно отдать быстрому скомпилированному коду на Rust.
На этой странице ты напишешь маленькую библиотеку на Rust, скомпилируешь её в Wasm и вызовешь со страницы в браузере. Здесь ничего не запускается в Rust Playground (нужны браузер и дополнительные инструменты), поэтому большинство примеров — обычные блоки кода для твоего компьютера. А вот части на чистом Rust можно попробовать сразу.
Что такое WebAssembly и почему Rust ему подходит
Заголовок раздела «Что такое WebAssembly и почему Rust ему подходит»WebAssembly — это компактный бинарный формат для программ. Его умеет запускать любой крупный браузер, со скоростью, близкой к нативной, в той же защищённой «песочнице», что и JavaScript. И это не только для браузеров: Wasm запускают и на серверах, и на edge-платформах.
Rust — один из самых популярных языков для Wasm, и на то есть причины:
- Нет сборщика мусора и тяжёлой среды выполнения. Rust не нужно тащить вместе с твоим кодом дополнительный механизм, поэтому файлы
.wasmполучаются маленькими. - Отличные инструменты.
wasm-bindgenсам пишет связующий код между Rust и JavaScript, аwasm-packсобирает всё одной командой. - Безопасность и скорость. Ты получаешь ту же безопасность памяти и ту же производительность, что и на обычном компьютере.
Устанавливаем инструменты
Заголовок раздела «Устанавливаем инструменты»Сначала скажи Rust, что он может компилировать под цель (target) Wasm. Цель — это «на какой машине будет работать код»; wasm32-unknown-unknown означает «32-битный Wasm, без конкретной операционной системы»:
rustup target add wasm32-unknown-unknownЗатем установи wasm-pack. Он компилирует твой крейт, запускает wasm-bindgen и складывает всё нужное в одну папку. Можно воспользоваться официальным установщиком или собрать его через Cargo:
# установщик для Linux / macOS с сайта wasm-packcurl https://wasm-bindgen.github.io/wasm-pack/installer/init.sh -sSf | sh
# или на любой системе, где установлен Rustcargo install wasm-pack --lockedПишем библиотеку на Rust для Wasm
Заголовок раздела «Пишем библиотеку на Rust для Wasm»Модуль Wasm — это библиотека, а не программа с main. Создай её:
cargo new --lib hello-wasmcd hello-wasmПотом отредактируй Cargo.toml. Строка crate-type говорит Cargo собрать библиотеку, пригодную для .wasm (cdylib), а wasm-bindgen — это крейт, который соединяет Rust и JavaScript:
[package]name = "hello-wasm"version = "0.1.0"edition = "2021"
[lib]crate-type = ["cdylib", "rlib"]
[dependencies]wasm-bindgen = "0.2"Теперь напиши две функции в src/lib.rs. Атрибут #[wasm_bindgen] помечает каждую как «вызываемую из JavaScript»:
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!("Hello, {name}! This string was built in Rust.")}Это весь код на Rust, который нужен. Обрати внимание: это обычные функции. wasm-bindgen сам превращает строку JavaScript в &str, а возвращённую String — обратно в строку JavaScript.
Сначала проверь логику на своём компьютере
Заголовок раздела «Сначала проверь логику на своём компьютере»Вот привычка, которая экономит много времени: держи настоящую работу в обычных функциях Rust и проверяй их на своём компьютере через cargo test или быстрый main. Отлаживать в браузере медленнее, чем в терминале. Та же логика, только без атрибута, работает где угодно:
fn add(a: i32, b: i32) -> i32 { a + b}
fn greet(name: &str) -> String { format!("Hello, {name}! This string was built in Rust.")}
fn main() { println!("{}", greet("Ferris")); println!("2 + 3 = {}", add(2, 3));}Вывод:
Hello, Ferris! This string was built in Rust.2 + 3 = 5Приветствие мы оставили на английском, чтобы оно совпадало с кодом библиотеки выше и с тем, что потом настоящая страница покажет в браузере («Привет, Ferris! Эта строка собрана в Rust.»).
Когда логика работает правильно, обёртка #[wasm_bindgen] — всего лишь тонкая дверь в браузер. (Тесты с пометкой #[test] тоже запускаются на твоём компьютере через cargo test — см. Тестирование.)
Собираем через wasm-pack
Заголовок раздела «Собираем через wasm-pack»В папке крейта выполни:
wasm-pack build --target web--target web означает «сделай файлы, которые браузер загрузит напрямую тегом <script type="module">», без сборщика вроде webpack. Когда мы это запустили, в новой папке pkg/ оказалось:
pkg/├── hello_wasm_bg.wasm скомпилированный код на Rust├── hello_wasm.js связующий JavaScript: загружает модуль и оборачивает каждую функцию├── hello_wasm.d.ts типы TypeScript для твоих функций├── hello_wasm_bg.wasm.d.ts└── package.jsonДефис в hello-wasm превратился в имени файлов в подчёркивание. По умолчанию wasm-pack build делает release-сборку (оптимизированную); добавь --dev, чтобы во время экспериментов собирать быстрее, но без оптимизаций.
Вызываем из JavaScript
Заголовок раздела «Вызываем из JavaScript»Создай index.html рядом с Cargo.toml (чтобы pkg/ лежала рядом с ним):
<!doctype html><html lang="en"><head> <meta charset="utf-8"> <title>Hello from Rust</title></head><body> <p id="message">Loading…</p>
<script type="module"> import init, { add, greet } from "./pkg/hello_wasm.js";
await init(); // скачать и запустить файл .wasm
const text = greet("Ferris"); const sum = add(2, 3); document.getElementById("message").textContent = `${text} 2 + 3 = ${sum}`; </script></body></html>Экспорт по умолчанию, init(), скачивает файл .wasm и запускает его. После этого add и greet — обычные функции JavaScript.
Браузеры не загружают модули с адреса file://, поэтому нужен маленький локальный веб-сервер. Если у тебя установлен Python, быстрее всего так:
python3 -m http.server 8080Открой http://localhost:8080 в браузере. У нас страница показала:
Hello, Ferris! This string was built in Rust. 2 + 3 = 5Это предложение собрал Rust, работающий внутри браузера. 🦀
Что дёшево проходит через границу (а что нет)
Заголовок раздела «Что дёшево проходит через границу (а что нет)»Rust и JavaScript работают с памятью по-разному. У модуля Wasm свой блок памяти, и значения приходится передавать между двумя сторонами. Некоторые поездки бесплатны, а за другие приходится платить копированием:
| Тип в Rust | В JavaScript | Цена |
|---|---|---|
i32, u32, f64, bool |
number, boolean |
Дёшево: передаётся напрямую |
&str, String |
string |
Копия плюс перевод из UTF-16 в UTF-8 (или обратно) |
&[u8], Vec<u8> |
Uint8Array |
Копия в память Wasm (или из неё) |
&mut [u8] |
Uint8Array |
Копируется туда, а после вызова — обратно |
Отсюда золотое правило: делай мало вызовов, и пусть каждый выполняет много работы. Один вызов Rust для обработки целой картинки — это быстро. Миллион вызовов, по одному на пиксель, тратит большую часть времени на пересечение границы.
Вот как это выглядит. Эта функция за один вызов делает ярче целый буфер значений пикселей:
use wasm_bindgen::prelude::*;
/// Делает каждый пиксель ярче. JS передаёт Uint8Array.#[wasm_bindgen]pub fn brighten(pixels: &mut [u8], amount: u8) { for p in pixels.iter_mut() { *p = p.saturating_add(amount); }}Из JavaScript после await init() ты вызовешь brighten(pixels, 20) с Uint8Array, и массив изменится на месте. Сам цикл — чистый Rust, так что его можно проверить на своём компьютере:
fn brighten(pixels: &mut [u8], amount: u8) { for p in pixels.iter_mut() { // saturating_add останавливается на 255, а не переполняется *p = p.saturating_add(amount); }}
fn main() { let mut pixels = [10, 100, 250]; brighten(&mut pixels, 20); println!("{pixels:?}");}Вывод:
[30, 120, 255]Заметь: 250 + 20 превратилось в 255, а не в падение или «прокрутку». saturating_add останавливается на максимуме. Для цветов это именно то, что нужно. Подробнее — в уроке Переполнение целых чисел.
Делаем файл .wasm маленьким
Заголовок раздела «Делаем файл .wasm маленьким»Каждый байт файла .wasm нужно скачать, прежде чем страница сможет им воспользоваться. Несколько советов:
- Всегда выкладывай release-сборку. Для нашего крошечного примера
wasm-pack build --devвыдал файл.wasmразмером 58 591 байт, а обычная release-сборка — 21 530 байт. (Это на нашей машине с выключеннымwasm-opt— у тебя цифры будут другими.) - Пусть wasm-pack запускает
wasm-opt. В release-режиме wasm-pack по умолчанию прогоняет результат через оптимизаторwasm-optиз Binaryen, и обычно файл становится ещё меньше. - Включи сжатие на сервере. Наш файл в 21 530 байт после gzip весил 8464 байта.
- Попробуй настройки, нацеленные на размер, и измерь результат:
[profile.release]opt-level = "z" # оптимизировать размер ("s" — более мягкий вариант)lto = truecodegen-units = 1Будь честен с собой и измеряй. Для нашего примера из двух функций эти настройки не помогли — файл вышел на 22 817 байт, то есть чуть больше. Обычно они окупаются в крейтах покрупнее. Вещи вроде format! и сообщений паники тоже тянут за собой код, поэтому крейт без них может оказаться намного меньше.
Где Wasm используют в реальном мире
Заголовок раздела «Где Wasm используют в реальном мире»Несколько примеров, и у каждого есть рассказ самой компании (на английском):
- Figma компилирует свой редактор дизайна на C++ в WebAssembly и сообщила, что время загрузки сократилось в 3 раза. (Это не Rust, но хорошо показывает, на что способен Wasm в большом приложении.)
- Cloudflare Workers поддерживает Rust через крейт
workers-rs: твой код на Rust компилируется в Wasm и работает на серверах Cloudflare по всему миру. - Shopify Functions — это модули WebAssembly, которые настраивают поведение магазинов, и Shopify советует Rust как самый производительный язык для них.
- Браузерное расширение 1Password перевело заполнение страниц на WebAssembly и сообщило, что оно стало работать как минимум вдвое быстрее.
Частые ошибки
Заголовок раздела «Частые ошибки»Забыть crate-type = ["cdylib"]. Без этой строки wasm-pack сразу останавливается. К счастью, сообщение об ошибке точно говорит, что добавить:
Error: crate-type must be cdylib to compile to WebAssembly. Add the following to your Cargo.toml file:
[lib]crate-type = ["cdylib", "rlib"]wasm-pack пишет, что crate-type должен быть cdylib, и показывает строки, которые нужно вставить в Cargo.toml.
Открыть index.html двойным щелчком. Страница так и показывает «Loading…», а в консоли браузера (в Chrome) написано:
Access to script at 'file:///…/pkg/hello_wasm.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: chrome, chrome-extension, chrome-untrusted, data, http, https, isolated-app.Браузер заблокировал загрузку скрипта политикой CORS, потому что адреса file:// для таких запросов не разрешены. Решение: раздай папку локальным веб-сервером, например python3 -m http.server, и открой адрес http://localhost.
Вызвать функцию до await init(). Модуль Wasm ещё не загружен, и связующему коду нечего вызывать. Когда мы убрали строку с init(), Chrome показал Cannot read properties of undefined (reading '__wbindgen_free'). Всегда сначала делай await init().
Дальше: целые веб-приложения на Rust
Заголовок раздела «Дальше: целые веб-приложения на Rust»Вызвать несколько функций Rust из JavaScript — отличное начало. Если хочешь написать на Rust всё веб-приложение, посмотри на эти фреймворки. Оба компилируются в Wasm и позволяют писать компоненты на Rust, немного похоже на React:
- Leptos (на английском) — быстрый современный фреймворк, который умеет ещё и отрисовывать страницы на сервере.
- Yew (на английском) — зрелый фреймворк на основе компонентов.
- WebAssembly — быстрый и безопасный бинарный формат, который умеют запускать браузеры (и серверы); Rust хорошо в него компилируется, потому что у него нет сборщика мусора.
- Добавь цель через
rustup target add wasm32-unknown-unknownи установиwasm-pack. - Помечай функции
#[wasm_bindgen], укажиcrate-type = ["cdylib", "rlib"]и собирай черезwasm-pack build --target web. - В JavaScript сначала
await init(), потом вызывай свои функции; открывай страницу поhttp://, а неfile://. - Числа проходят через границу дёшево, а строки и срезы копируются — так что делай мало вызовов, но крупных.
- Выкладывай release-сборки со сжатием и измеряй, прежде чем доверять настройкам размера.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Какая команда добавляет цель Rust для WebAssembly в браузере?
2. Что делает атрибут #[wasm_bindgen]?
3. Страница застряла на «Loading…», а в консоли ошибка CORS для адреса file:///. Как это исправить?
4. Что из этого дешевле всего передать из JavaScript в Rust?
Результат: 0 / 4
Что почитать
Заголовок раздела «Что почитать»- Компиляция Rust в WebAssembly — MDN (перевод на русский) — пошаговое руководство, в котором собирают и запускают проект на wasm-pack.
- The wasm-bindgen Guide (на английском) — все поддерживаемые типы, вызов API браузера и множество примеров.
- Документация wasm-pack (на английском) — все параметры
build, цели и профили. - wasm32-unknown-unknown — The rustc book (на английском) — что поддерживает эта цель и какие у неё ограничения.
- WebAssembly — MDN (перевод на русский) — что такое Wasm и как его выполняют браузеры, без привязки к Rust.