Добро пожаловать в углублённое изучение того, как Rust интегрируется с экосистемой WebAssembly (Wasm) через инструмент wasm-bindgen
. Этот раздел посвящён созданию высокопроизводительных веб-приложений, где Rust выступает в роли языка для генерации WebAssembly, а wasm-bindgen
обеспечивает бесшовное взаимодействие между Rust и JavaScript. Мы разберём всё: от основ до тонкостей, с примерами, скрытыми возможностями и практическими советами. Если вы новичок, не переживайте — начнём с азов. Если вы эксперт, найдёте здесь нюансы, которые, возможно, упустили.
WebAssembly — это бинарный формат, который позволяет запускать код на уровне, близком к машинному, прямо в браузере. Он был разработан как дополнение к JavaScript, чтобы повысить производительность для задач, где JS может быть слишком медленным: игры, обработка графики, сложные вычисления. Rust идеально подходит для WebAssembly благодаря своей скорости, безопасности памяти и отсутствию сборщика мусора, что критично для компактного и быстрого кода в Wasm.
Однако WebAssembly сам по себе не "знает" о DOM, JavaScript или других веб-API. Здесь на сцену выходит wasm-bindgen
— инструмент, который генерирует связующий код (bindings) между Rust и JavaScript, позволяя вызывать функции в обе стороны и передавать сложные данные.
Прежде чем начать, убедитесь, что у вас есть всё необходимое:
rustup
).wasm-pack
— утилита для сборки Wasm-проектов. Установите её командой:
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
rustup target add wasm32-unknown-unknown
.Примечание: Цель wasm32-unknown-unknown
— это стандартная цель для WebAssembly, не зависящая от конкретной платформы. В отличие от wasm32-wasi
, она не предполагает наличия системных вызовов, что идеально для браузера.
Давайте создадим простой проект, который использует wasm-bindgen
. Начнём с базового примера — функции, которая выводит сообщение в консоль браузера.
cargo new wasm-example --lib
Перейдите в директорию: cd wasm-example
.
Cargo.toml
:
[package]
name = "wasm-example"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
Здесь crate-type = ["cdylib"]
указывает, что мы создаём динамическую библиотеку для WebAssembly.
src/lib.rs
:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
log(&format!("Привет, {}!", name));
}
Разберём этот код:
use wasm_bindgen::prelude::*
импортирует необходимые макросы и типы.#[wasm_bindgen]
над extern "C"
объявляет внешнюю функцию console.log
из JavaScript.#[wasm_bindgen]
над greet
делает функцию доступной для вызова из JS.wasm-pack
:
wasm-pack build --target web
Флаг --target web
указывает, что мы собираем для браузера.
index.html
) в корне проекта:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Wasm Example</title>
</head>
<body>
<script type="module">
import init, { greet } from './pkg/wasm_example.js';
async function run() {
await init();
greet("Мир");
}
run();
</script>
</body>
</html>
npx serve
) и откройте index.html
в браузере. В консоли вы увидите "Привет, Мир!".wasm-pack
компилирует Rust-код в .wasm
-файл и генерирует JS-обёртку в папке pkg/
. wasm-bindgen
создаёт "мост" между Rust и JS, преобразуя типы данных (например, &str
в JS-строку) и обеспечивая вызовы функций. В примере выше greet
принимает строку из JS, форматирует её и вызывает console.log
.
Теперь усложним задачу: передадим структуру из Rust в JS и обратно.
Обновите src/lib.rs
:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
#[derive(Debug)]
pub struct Person {
name: String,
age: u32,
}
#[wasm_bindgen]
impl Person {
#[wasm_bindgen(constructor)]
pub fn new(name: String, age: u32) -> Person {
Person { name, age }
}
pub fn introduce(&self) {
log(&format!("Меня зовут {}, мне {} лет.", self.name, self.age));
}
pub fn grow_older(&mut self) {
self.age += 1;
}
}
#[wasm_bindgen]
pub fn create_and_greet(name: String, age: u32) -> Person {
let person = Person::new(name, age);
person.introduce();
person
}
Обновите index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Wasm Complex Example</title>
</head>
<body>
<script type="module">
import init, { create_and_greet } from './pkg/wasm_example.js';
async function run() {
await init();
const person = create_and_greet("Алексей", 30);
console.log(person);
person.grow_older();
console.log("После grow_older:", person.age);
}
run();
</script>
</body>
</html>
Что здесь происходит?
#[derive(Debug)]
позволяет отображать структуру в JS через console.log
.#[wasm_bindgen(constructor)]
делает new
конструктором, доступным в JS как new Person()
.introduce
и grow_older
) автоматически становятся частью JS-объекта.После сборки и запуска вы увидите в консоли: "Меня зовут Алексей, мне 30 лет.", а затем возраст увеличится до 31.
wasm-bindgen
поддерживает базовые типы (числа, строки), но сложные структуры требуют аннотаций. Например, Vec<T>
нужно явно преобразовывать в JS-массив с помощью JsValue
.#[wasm_bindgen]
, функция не будет доступна в JS. Всегда проверяйте экспорт.wasm-bindgen
и wasm-pack
(см. документацию).wasm-pack
для автоматизации сборки и тестирования.///
, чтобы облегчить интеграцию с JS-разработчиками.wasm-bindgen
поддерживает работу с промисами, DOM-объектами и даже TypeScript (через --typescript
в wasm-pack
). Например, для асинхронных операций можно использовать JsFuture
. Это выходит за рамки базового примера, но мы вернёмся к этому в разделе 5.
На этом мы завершаем разбор wasm-bindgen
. Вы узнали, как настроить проект, создать простые и сложные взаимодействия с JS, а также избежать типичных ошибок. В следующем разделе мы перейдём к no_std
и встраиваемым системам.
Добро пожаловать в мир встраиваемых систем с Rust! В этом разделе мы погрузимся в использование Rust без стандартной библиотеки (no_std
), что делает его идеальным выбором для программирования микроконтроллеров, IoT-устройств и других систем с ограниченными ресурсами. Мы разберём всё: от основ до продвинутых техник, с примерами, скрытыми возможностями и практическими советами. Даже если вы новичок в embedded-разработке, этот раздел проведёт вас через все этапы. Эксперты найдут здесь тонкости и лучшие практики.
По умолчанию Rust использует стандартную библиотеку (std
), которая предоставляет удобные абстракции: коллекции, ввод-вывод, многопоточность и т.д. Однако std
требует наличия операционной системы и сборщика мусора, что делает её непригодной для встраиваемых систем, где ресурсов мало, а среда выполнения — "голое железо" (bare-metal). Здесь на помощь приходит атрибут #![no_std]
, который отключает стандартную библиотеку, оставляя только core
— минимальный набор инструментов, работающий без ОС.
no_std
позволяет писать низкоуровневый код, сохраняя преимущества Rust: безопасность памяти, выразительность и отсутствие undefined behavior. Это делает Rust конкурентом C и C++ в embedded-разработке.
Для работы с no_std
вам понадобится:
rustup
).thumbv7m-none-eabi
для микроконтроллеров Cortex-M. Установите её:
rustup target add thumbv7m-none-eabi
arm-none-eabi-gcc
) и утилиты вроде openocd
для прошивки.Примечание: Цель thumbv7m-none-eabi
подходит для большинства ARM Cortex-M микроконтроллеров. Для других архитектур (RISC-V, AVR) выбирайте соответствующую цель, например, riscv32i-unknown-none-elf
.
Давайте напишем простой проект для микроконтроллера, который мигает светодиодом. Мы начнём с минимального примера без внешних зависимостей.
cargo new blinky --bin
Перейдите в директорию: cd blinky
.
Cargo.toml
:
[package]
name = "blinky"
version = "0.1.0"
edition = "2021"
[profile.release]
opt-level = "s" # Оптимизация для размера
Опция opt-level = "s"
минимизирует размер бинарника, что важно для встраиваемых систем.
.cargo/config.toml
для указания цели:
[build]
target = "thumbv7m-none-eabi"
src/main.rs
:
#![no_std]
#![no_main]
use core::panic::PanicInfo;
// Точка входа для bare-metal
#[no_mangle]
pub extern "C" fn _start() -> ! {
loop {
// Здесь будет логика мигания светодиодом
}
}
// Обработчик паники (обязателен в no_std)
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
Разберём код:
#![no_std]
отключает стандартную библиотеку (в начале файла, так как это внутренняя директива).#![no_main]
убирает стандартную точку входа main
, заменяя её на _start
.#[no_mangle]
сохраняет имя функции _start
для компоновщика.!
как тип возврата указывает, что функция никогда не завершается.#[panic_handler]
определяет поведение при панике.cargo build --release --target thumbv7m-none-eabi
Вы получите бинарник в target/thumbv7m-none-eabi/release/blinky
.
Этот код пока ничего не делает, кроме бесконечного цикла. Чтобы он мигал светодиодом, нужно взаимодействовать с регистрами микроконтроллера — об этом ниже.
В реальных встраиваемых системах вы напрямую обращаетесь к аппаратным регистрам. Для этого можно использовать "сырой" доступ через указатели или библиотеки абстракции (PAC/HAL). Начнём с простого примера — мигание светодиода на STM32F1.
Обновите Cargo.toml
, добавив зависимость от cortex-m
:
[dependencies]
cortex-m = "0.7"
Обновите src/main.rs
:
#![no_std]
#![no_main]
use cortex_m::asm;
use cortex_m::peripheral::Peripherals;
// Адреса регистров для STM32F1 (пример для порта GPIOA)
const RCC_APB2ENR: *mut u32 = 0x4002_1018 as *mut u32; // Включение тактирования GPIOA
const GPIOA_CRL: *mut u32 = 0x4001_0800 as *mut u32; // Конфигурация порта
const GPIOA_BSRR: *mut u32 = 0x4001_0810 as *mut u32; // Установка/сброс пина
#[no_mangle]
pub extern "C" fn _start() -> ! {
unsafe {
// Включаем тактирование GPIOA
RCC_APB2ENR.write_volatile(1 << 2);
// Настраиваем PA5 как выход (светодиод на STM32F1 Blue Pill)
GPIOA_CRL.write_volatile(0x4444_4441); // 01: выход, 10 МГц
}
loop {
unsafe {
// Включаем светодиод (PA5)
GPIOA_BSRR.write_volatile(1 << 5);
delay(500_000); // Примитивная задержка
// Выключаем светодиод
GPIOA_BSRR.write_volatile(1 << (5 + 16));
delay(500_000);
}
}
}
// Простая задержка
fn delay(count: u32) {
for _ in 0..count {
asm::nop();
}
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
Что здесь происходит?
unsafe
используется для работы с сырыми указателями.write_volatile
обеспечивает запись без оптимизации компилятором.delay
— примитивная задержка через nop
(неточные тайминги, но для примера достаточно).Соберите и прошейте код на STM32F1 (например, через openocd
и arm-none-eabi-objcopy
для конверсии в .bin). Светодиод на PA5 начнёт мигать.
"Сырой" доступ к регистрам сложен и опасен. В реальных проектах используют библиотеки абстракции: Peripheral Access Crate (PAC) или Hardware Abstraction Layer (HAL). Например, для STM32 есть stm32f1xx-hal
.
Обновите Cargo.toml
:
[dependencies]
cortex-m = "0.7"
stm32f1xx-hal = { version = "0.10", features = ["stm32f103", "rt"] }
panic-halt = "0.2" # Простой обработчик паники
Обновите src/main.rs
:
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use stm32f1xx_hal::{pac, prelude::*, timer::Timer};
use panic_halt as _;
#[entry]
fn main() -> ! {
let dp = pac::Peripherals::take().unwrap();
let cp = cortex_m::Peripherals::take().unwrap();
let mut rcc = dp.RCC.constrain();
let mut gpioa = dp.GPIOA.split(&mut rcc.apb2);
let mut led = gpioa.pa5.into_push_pull_output(&mut gpioa.crl);
let mut timer = Timer::syst(cp.SYST, &mut rcc.apb1).start_count_down(1.hz());
loop {
led.set_high().unwrap();
timer.wait().unwrap();
led.set_low().unwrap();
timer.wait().unwrap();
}
}
Что изменилось?
#[entry]
из cortex-m-rt
заменяет _start
.#[panic_handler]
компиляция завершится ошибкой. Используйте panic-halt
или кастомный обработчик.no_std
минимизирует бинарник, но зависимости вроде HAL могут его увеличить. Используйте opt-level = "s"
или "z"
.std
нет println!
. Используйте UART или RTT (Real-Time Transfer) для логов.memory.x
) соответствует вашему устройству.defmt
или log
с RTT для отладки.cortex-m::asm::wfi
).no_std
поддерживает асинхронное программирование через embassy
— фреймворк для встраиваемых систем. Также есть alloc
для динамической памяти в no_std
с кастомным аллокатором (например, wee_alloc
).
На этом мы завершаем разбор no_std
. Вы узнали, как писать bare-metal код, работать с регистрами и использовать HAL. В следующем разделе мы перейдём к сетевым приложениям.
Rust благодаря своей производительности, безопасности и богатой экосистеме библиотек (crates) стал популярным выбором для разработки сетевых приложений. В этом разделе мы углубимся в мир сетевого программирования на Rust, рассмотрим ключевые библиотеки, их возможности, типичные сценарии использования, а также разберем примеры кода. Мы не ограничимся поверхностным обзором — вы получите полное представление о том, как писать надежные, масштабируемые и безопасные сетевые приложения, избегая распространенных ошибок.
Прежде чем погрузиться в библиотеки, давайте разберем, почему Rust так хорош в этой области:
async/await
позволяет эффективно обрабатывать тысячи соединений без лишних затрат.Теперь перейдем к обзору ключевых библиотек и их применению.
Tokio — это асинхронный runtime, который стал стандартом де-факто для сетевых приложений в Rust. Он предоставляет инструменты для работы с TCP/UDP-сокетами, таймерами, потоками и многим другим.
TcpStream
, UdpSocket
).tokio::runtime
.async/await
.use tokio::net::TcpListener; use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> tokio::io::Result<()> { let listener = TcpListener::bind("127.0.0.1:8080").await?; println!("Сервер запущен на
127.0.0.1:8080"); loop {
let (mut socket, addr) = listener.accept().await?; println!("Новое соединение: {}", addr); tokio::spawn(async move { let mut buffer = [0; 1024];
match socket.read(&mut buffer).await {
Ok(n) if n > 0 => { println!("Получено: {}", String::from_utf8_lossy(&buffer[..n]));
socket.write_all(&buffer[..n]).await.unwrap(); // Эхо
}
_ => println!("Соединение закрыто: {}", addr),
}
});
}
}
Объяснение:
TcpListener::bind
создает сервер на указанном адресе.accept
асинхронно ожидает входящие соединения.tokio::spawn
запускает обработку каждого клиента в отдельной задаче.Совет: Используйте cargo add tokio --features full
для полной функциональности.
Hyper — это низкоуровневая библиотека для работы с HTTP. Она часто используется как основа для более высокоуровневых фреймворков (например, Axum).
use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn}; use std::convert::Infallible; async fn handle(_req: Request<Body>) ->
Result<Response<Body>, Infallible> {
Ok(Response::new(Body::from("Hello, Hyper!")))
}
#[tokio::main]
async fn main() { let addr = ([127, 0, 0, 1], 3000).into(); let make_svc = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(handle))
});
let server = Server::bind(&addr).serve(make_svc); println!("Сервер запущен на http://{}", addr); if let Err(e) = server.await { eprintln!("Ошибка
сервера: {}", e);
}
}
Объяснение:
service_fn
оборачивает обработчик запросов.make_service_fn
создает
сервис для каждого соединения.Совет: Для реальных приложений используйте Axum поверх Hyper.
Reqwest — это высокоуровневый HTTP-клиент, построенный на Hyper, но с удобным API.
use reqwest::Client;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct Post { id: u32, title: String,
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> { let client = Client::new(); let post: Post = client
.get("https://jsonplaceholder.typicode.com/posts/1") .send() .await? .json() .await?;
println!("Получен пост: {:?}", post); Ok(())
}
Объяснение:
Client
— переиспользуемый клиент для экономии ресурсов.json()
десериализует ответ в структуру Post
.Совет: Добавьте cargo add serde serde_json
для работы с JSON.
Actix-web — это мощный веб-фреймворк для создания серверов REST API и веб-приложений.
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
async fn greet() -> impl Responder { HttpResponse::Ok().body("Hello, Actix!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .route("/", web::get().to(greet))
})
.bind("127.0.0.1:8080")? .run() .await
}
Объяснение:
route
задает маршрут для GET-запросов.HttpServer
запускает сервер
на указанном адресе.Совет: Используйте cargo add actix-web
и изучите документацию для middleware.
Result
и логируйте ошибки с log
или
tracing
.tokio::test
для асинхронных тестов.Сетевые приложения в Rust — это сочетание мощи и безопасности. Выбор библиотеки зависит от ваших задач: от простых клиентов (Reqwest) до сложных серверов (Actix-web, Hyper, Tokio). В следующих разделах мы рассмотрим другие аспекты экосистемы Rust, но уже сейчас вы можете начать экспериментировать с примерами выше.
Rust, благодаря своей производительности, безопасности и гибкости, стал популярным выбором для разработки кроссплатформенных приложений. В этом разделе мы подробно разберём, как использовать кроссплатформенные фреймворки в экосистеме Rust, какие инструменты доступны, их сильные и слабые стороны, а также практические примеры их применения. Мы рассмотрим как графические интерфейсы (GUI), так и кроссплатформенные решения для мобильных и десктопных приложений. Этот раздел поможет вам понять, как Rust может быть использован для создания приложений, работающих на Windows, macOS, Linux, Android, iOS и даже в браузере через WebAssembly.
Кроссплатформенные фреймворки позволяют разработчикам писать код один раз и запускать его на разных платформах с минимальными изменениями. В мире Rust это особенно важно, так как язык изначально ориентирован на низкоуровневую производительность, что делает его конкурентом C++ в таких областях, как разработка GUI или мобильных приложений. Однако, в отличие от языков вроде JavaScript или Python, где кроссплатформенность часто достигается за счёт интерпретаторов или виртуальных машин, Rust полагается на компиляцию в нативный код, что требует от фреймворков особой гибкости.
Экосистема Rust предлагает несколько фреймворков для кроссплатформенной разработки. Мы рассмотрим основные из них, их особенности и сценарии использования.
Tauri — это легковесный фреймворк для создания кроссплатформенных десктопных приложений с использованием веб-технологий (HTML, CSS, JavaScript) для интерфейса и Rust для бэкенда. Tauri позиционируется как альтернатива Electron, но с меньшим потреблением ресурсов и большей безопасностью.
Пример простого проекта с Tauri:
<!-- Файл src-tauri/src/main.rs --> use tauri::command; #[command] fn greet(name: &str) -> String { format!("Привет, {}! Это Rust в Tauri.", name) } fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect("Ошибка запуска Tauri"); }
В этом примере мы создаём команду greet
, которую можно вызвать из JavaScript фронтенда. Для запуска нужно настроить
src-tauri/tauri.conf.json
и добавить HTML-файл в src-tauri/index.html
.
Нюанс: Tauri требует установленного веб-движка на целевой системе (например, WebView2 на Windows). Убедитесь, что ваша целевая аудитория имеет совместимую ОС.
Dioxus — это фреймворк для создания кроссплатформенных приложений с единым кодом для десктопа, веба и мобильных устройств.
Он вдохновлён React и использует синтаксис, похожий на JSX (через макрос rsx!
).
Пример приложения с Dioxus:
use dioxus::prelude::*; fn main() { dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { let mut count = use_state(cx, || 0); cx.render(rsx! { h1 { "Счётчик: {count}" } button { onclick: move |_| count += 1, "Увеличить" } }) }
Этот код создаёт простое десктопное приложение с кнопкой и счётчиком. Для сборки под WebAssembly нужно изменить таргет на
wasm32-unknown-unknown
и использовать dioxus build --release
.
Подводный камень: Dioxus пока экспериментирует с мобильной поддержкой, поэтому для продакшена лучше протестировать стабильность на iOS/Android.
Druid — это нативный кроссплатформенный GUI-фреймворк, написанный на Rust. Он ориентирован на производительность и минимализм, предлагая инструменты для создания десктопных приложений.
Пример с Druid:
use druid::widget::{Button, Flex, Label}; use druid::{AppLauncher, LocalizedString, Widget, WindowDesc, Data}; #[derive(Clone, Data)] struct AppState { count: i32, } fn build_ui() -> impl Widget<AppState> { let label = Label::new(|data: &AppState, _env: &_| format!("Счётчик: {}", data.count)); let button = Button::new("Увеличить") .on_click(|_ctx, data: &mut AppState, _env| data.count += 1); Flex::column() .with_child(label) .with_child(button) } fn main() { let window = WindowDesc::new(build_ui()) .title(LocalizedString::new("Druid Пример")); AppLauncher::with_window(window) .launch(AppState { count: 0 }) .expect("Ошибка запуска"); }
Этот код создаёт окно с кнопкой и счётчиком. Druid использует декларативный подход к построению интерфейса, что упрощает поддержку.
Совет: Используйте druid::lens
для управления сложными состояниями, чтобы избежать избыточного кода.
Фреймворк | Платформы | Производительность | Размер бинарника | Сложность |
---|---|---|---|---|
Tauri | Десктоп, мобильные (экспериментально) | Высокая | Малый | Средняя |
Dioxus | Десктоп, веб, мобильные | Средняя | Средний | Низкая |
Druid | Десктоп | Очень высокая | Средний | Высокая |
cross
или Docker для сборки под разные платформы. Например, cross build --target aarch64-apple-darwin
для macOS
ARM.Кроссплатформенные фреймворки в Rust открывают широкие возможности для создания приложений, работающих на разных устройствах. Tauri, Dioxus и Druid — это лишь вершина айсберга, и экосистема продолжает расти. Выбор подходящего инструмента зависит от ваших целей, но все они демонстрируют силу Rust: безопасность, производительность и гибкость.
Когда мы говорим о сборке Rust-проектов для браузера, мы имеем в виду использование WebAssembly (WASM) как целевой платформы. WebAssembly — это низкоуровневый байт-код, который выполняется в современных браузерах с почти нативной скоростью. Rust идеально подходит для этой задачи благодаря своей производительности, безопасности и гибкости. В этом разделе мы разберем несколько примеров сборки Rust-кода для браузера, начиная с простого "Hello, World!" и заканчивая более сложными сценариями, такими как взаимодействие с DOM и обработка событий. Мы также рассмотрим инструменты, настройки и потенциальные "подводные камни".
Rust позволяет писать высокопроизводительный код, который компилируется в WASM, избегая накладных расходов JavaScript там, где это возможно. При
этом инструменты вроде wasm-bindgen
упрощают интеграцию с JavaScript и DOM, делая Rust мощным выбором для веб-разработки. Сценарии
использования включают:
Для сборки Rust-проектов под браузер вам понадобятся:
wasm32-unknown-unknown
.Установите их, если еще не сделали:
rustup target add wasm32-unknown-unknown cargo install wasm-pack npm install -g npm # Если
npm еще не установлен
Давайте начнем с минимального примера, который выводит сообщение в консоль браузера.
Создайте новый проект Rust:
cargo new wasm-hello --lib
cd wasm-hello
Cargo.toml
Добавьте зависимости и укажите, что это библиотека для WebAssembly:
[package]
name = "wasm-hello" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2"
crate-type =
["cdylib"]
указывает, что мы создаем динамическую библиотеку, совместимую с WASM. wasm-bindgen
позволяет взаимодействовать с
JavaScript.
src/lib.rs
Замените содержимое файла:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" { fn alert(s: &str); // Объявляем внешнюю функцию JavaScript
}
#[wasm_bindgen]
pub fn greet() { alert("Hello, World from Rust!");
}
#[wasm_bindgen]
— макрос, который делает функции доступными для JavaScript. extern "C"
— блок для объявления внешних
функций (в данном случае alert
из JavaScript).
wasm-pack
Соберите проект:
wasm-pack build --target web
Флаг --target web
указывает, что мы создаем модуль для прямого использования в
браузере.
Создайте файл index.html
в корне проекта:
<!DOCTYPE html>
<html> <head> <meta charset="UTF-8"> <title>Rust WASM Hello</title> </head> <body> <script type="module">
import init, { greet } from './pkg/wasm_hello.js'; async function run() {
await init(); // Инициализация WASM greet(); // Вызов функции
}
run(); </script> </body> </html>
Запустите локальный сервер (например, с помощью
python
):
python3 -m http.server 8000
Откройте http://localhost:8000
в браузере и проверьте
всплывающее окно с "Hello, World from Rust!".
Нюанс: Если вы видите ошибку "module not found", убедитесь, что путь к
pkg/wasm_hello.js
в index.html
правильный. После сборки wasm-pack
создает папку pkg
с
сгенерированными файлами.
Теперь усложним задачу: изменим текст элемента на странице.
src/lib.rs
use wasm_bindgen::prelude::*;
use web_sys::window; // Для работы с DOM
#[wasm_bindgen]
pub fn update_dom() -> Result<(), JsValue> { let window = window().ok_or("No global window exists")?; let document = window.document().ok_or("No
document exists")?; let element = document
.get_element_by_id("my-text") .ok_or("Element not found")?; element.set_text_content(Some("Updated by Rust!")); Ok(())
}
web_sys
— это часть wasm-bindgen
, предоставляющая привязки к веб-API. Мы используем Result
, чтобы
обрабатывать ошибки (например, если элемент не найден).
Cargo.toml
Добавьте web-sys
с нужными
функциями:
[dependencies.web-sys]
version = "0.3" features = ["Window", "Document", "Element"]
index.html
<!DOCTYPE html>
<html> <head>
<meta charset="UTF-8"> <title>Rust WASM DOM</title> </head> <body> <p id="my-text">Original text</p>
<script type="module">
import init, { update_dom } from './pkg/wasm_hello.js'; async function run() { await init(); update_dom();
}
run(); </script> </body> </html>
wasm-pack build --target web python3 -m
http.server 8000
Текст "Original text" изменится на "Updated by Rust!".
Подводный камень: Если вы забудете включить нужные функции в
features
для web-sys
, компилятор выдаст ошибку. Всегда проверяйте документацию web-sys
на crates.io.
Добавим кнопку, которая вызывает Rust-функцию при клике.
src/lib.rs
use wasm_bindgen::prelude::*;
use web_sys::{window, Event, HtmlButtonElement}; use wasm_bindgen::JsCast;
#[wasm_bindgen]
pub fn setup_button() -> Result<(), JsValue> { let window = window().ok_or("No window")?; let document = window.document().ok_or("No document")?;
let button = document
.get_element_by_id("my-button") .ok_or("Button not found")? .dyn_into::<HtmlButtonElement>()?; let closure = Closure::wrap(Box::new(|_event:
Event| {
button.set_text_content(Some("Clicked via Rust!"));
}) as Box<dyn FnMut(Event)>);
button.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())?; closure.forget(); // Предотвращаем освобождение памяти Ok(())
}
Closure
нужен для создания JavaScript-совместимых замыканий. forget()
предотвращает уничтожение замыкания после выхода из
функции.
Cargo.toml
[dependencies.web-sys]
version = "0.3" features = ["Window", "Document", "HtmlButtonElement", "Event"]
index.html
<!DOCTYPE html>
<html> <head> <meta charset="UTF-8"> <title>Rust WASM Events</title> </head> <body> <button
id="my-button">Click me!</button> <script type="module">
import init, { setup_button } from './pkg/wasm_hello.js'; async function run() { await init(); setup_button();
}
run(); </script> </body> </html>
После клика по кнопке текст изменится на "Clicked via Rust!".
Совет: Использование Closure
требует осторожности. Если вы забудете вызвать forget()
,
обработчик события не сработает, так как замыкание будет уничтожено.
wasm-opt
из Binaryen для уменьшения размера WASM-файла: wasm-opt -Os
pkg/wasm_hello_bg.wasm -o pkg/wasm_hello_bg.wasm
console_error_panic_hook
для вывода паник в консоль браузера: [dependencies]
console_error_panic_hook = "0.1"
#[wasm_bindgen] pub fn init() { console_error_panic_hook::set_once();
}
init()
асинхронна, не забывайте использовать await
.dyn_into
может завершиться ошибкой, всегда проверяйте результат.