Глава 31: Rust в экосистеме

Содержание:
  1. WebAssembly: wasm-bindgen
    1. Что такое WebAssembly и зачем нужен Rust?
    2. Установка и настройка окружения
    3. Создание первого проекта
    4. Как это работает?
    5. Передача сложных данных
    6. Нюансы и подводные камни
    7. Лучшие практики
    8. Дополнительные возможности
  2. Встраиваемые системы: no_std
    1. Что такое no_std и зачем он нужен?
    2. Настройка окружения
    3. Создание первого no_std проекта
    4. Работа с регистрами
    5. Использование HAL
    6. Нюансы и подводные камни
    7. Лучшие практики
    8. Дополнительные возможности
  3. Сетевые приложения: примеры библиотек
    1. Почему Rust для сетевых приложений?
    2. Tokio: Асинхронный runtime для сетевых приложений
    3. Hyper: HTTP-клиент и сервер
    4. Reqwest: HTTP-клиент для простоты
    5. Actix-web: Веб-фреймворк
    6. Практические советы
    7. Заключение
  4. Кроссплатформенные фреймворки
    1. Зачем нужны кроссплатформенные фреймворки?
    2. Популярные кроссплатформенные фреймворки в Rust
    3. Сравнение фреймворков
    4. Практические советы
    5. Заключение
  5. Примеры: сборка для браузера
    1. Почему Rust и WebAssembly для браузера?
    2. Инструменты для работы
    3. Пример 1: Простой "Hello, World!" в браузере
    4. Пример 2: Взаимодействие с DOM
    5. Пример 3: Обработка событий
    6. Практические советы
    7. Подводные камни
  6. Упражнение: Собрать проект для WebAssembly
    1. Введение в упражнение
    2. Шаг 1: Подготовка окружения
    3. Шаг 2: Создание проекта
    4. Шаг 3: Написание кода на Rust
    5. Шаг 4: Сборка проекта
    6. Шаг 5: Создание HTML-страницы
    7. Шаг 6: Запуск и тестирование
    8. Возможные проблемы и решения
    9. Расширенный пример
    10. Итог

Раздел 1: WebAssembly: wasm-bindgen

Добро пожаловать в углублённое изучение того, как Rust интегрируется с экосистемой WebAssembly (Wasm) через инструмент wasm-bindgen. Этот раздел посвящён созданию высокопроизводительных веб-приложений, где Rust выступает в роли языка для генерации WebAssembly, а wasm-bindgen обеспечивает бесшовное взаимодействие между Rust и JavaScript. Мы разберём всё: от основ до тонкостей, с примерами, скрытыми возможностями и практическими советами. Если вы новичок, не переживайте — начнём с азов. Если вы эксперт, найдёте здесь нюансы, которые, возможно, упустили.

Что такое WebAssembly и зачем нужен Rust?

WebAssembly — это бинарный формат, который позволяет запускать код на уровне, близком к машинному, прямо в браузере. Он был разработан как дополнение к JavaScript, чтобы повысить производительность для задач, где JS может быть слишком медленным: игры, обработка графики, сложные вычисления. Rust идеально подходит для WebAssembly благодаря своей скорости, безопасности памяти и отсутствию сборщика мусора, что критично для компактного и быстрого кода в Wasm.

Однако WebAssembly сам по себе не "знает" о DOM, JavaScript или других веб-API. Здесь на сцену выходит wasm-bindgen — инструмент, который генерирует связующий код (bindings) между Rust и JavaScript, позволяя вызывать функции в обе стороны и передавать сложные данные.

Установка и настройка окружения

Прежде чем начать, убедитесь, что у вас есть всё необходимое:

Примечание: Цель wasm32-unknown-unknown — это стандартная цель для WebAssembly, не зависящая от конкретной платформы. В отличие от wasm32-wasi, она не предполагает наличия системных вызовов, что идеально для браузера.

Создание первого проекта

Давайте создадим простой проект, который использует wasm-bindgen. Начнём с базового примера — функции, которая выводит сообщение в консоль браузера.

  1. Создайте новый проект:
    cargo new wasm-example --lib
    Перейдите в директорию: cd wasm-example.
  2. Отредактируйте 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.
  3. Напишите код в 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));
    }

    Разберём этот код:

  4. Соберите проект с помощью wasm-pack:
    wasm-pack build --target web
    Флаг --target web указывает, что мы собираем для браузера.
  5. Создайте HTML-файл для тестирования (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>
    
  6. Запустите локальный сервер (например, через 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>

Что здесь происходит?

После сборки и запуска вы увидите в консоли: "Меня зовут Алексей, мне 30 лет.", а затем возраст увеличится до 31.

Нюансы и подводные камни

Лучшие практики

Дополнительные возможности

wasm-bindgen поддерживает работу с промисами, DOM-объектами и даже TypeScript (через --typescript в wasm-pack). Например, для асинхронных операций можно использовать JsFuture. Это выходит за рамки базового примера, но мы вернёмся к этому в разделе 5.

На этом мы завершаем разбор wasm-bindgen. Вы узнали, как настроить проект, создать простые и сложные взаимодействия с JS, а также избежать типичных ошибок. В следующем разделе мы перейдём к no_std и встраиваемым системам.


Раздел 2: Встраиваемые системы: no_std

Добро пожаловать в мир встраиваемых систем с Rust! В этом разделе мы погрузимся в использование Rust без стандартной библиотеки (no_std), что делает его идеальным выбором для программирования микроконтроллеров, IoT-устройств и других систем с ограниченными ресурсами. Мы разберём всё: от основ до продвинутых техник, с примерами, скрытыми возможностями и практическими советами. Даже если вы новичок в embedded-разработке, этот раздел проведёт вас через все этапы. Эксперты найдут здесь тонкости и лучшие практики.

Что такое no_std и зачем он нужен?

По умолчанию Rust использует стандартную библиотеку (std), которая предоставляет удобные абстракции: коллекции, ввод-вывод, многопоточность и т.д. Однако std требует наличия операционной системы и сборщика мусора, что делает её непригодной для встраиваемых систем, где ресурсов мало, а среда выполнения — "голое железо" (bare-metal). Здесь на помощь приходит атрибут #![no_std], который отключает стандартную библиотеку, оставляя только core — минимальный набор инструментов, работающий без ОС.

no_std позволяет писать низкоуровневый код, сохраняя преимущества Rust: безопасность памяти, выразительность и отсутствие undefined behavior. Это делает Rust конкурентом C и C++ в embedded-разработке.

Настройка окружения

Для работы с no_std вам понадобится:

Примечание: Цель thumbv7m-none-eabi подходит для большинства ARM Cortex-M микроконтроллеров. Для других архитектур (RISC-V, AVR) выбирайте соответствующую цель, например, riscv32i-unknown-none-elf.

Создание первого no_std проекта

Давайте напишем простой проект для микроконтроллера, который мигает светодиодом. Мы начнём с минимального примера без внешних зависимостей.

  1. Создайте новый проект:
    cargo new blinky --bin
    Перейдите в директорию: cd blinky.
  2. Отредактируйте Cargo.toml:
    [package]
    name = "blinky"
    version = "0.1.0"
    edition = "2021"
    
    [profile.release]
    opt-level = "s"  # Оптимизация для размера
    

    Опция opt-level = "s" минимизирует размер бинарника, что важно для встраиваемых систем.

  3. Создайте файл .cargo/config.toml для указания цели:
    [build]
    target = "thumbv7m-none-eabi"
  4. Напишите код в 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 {}
    }

    Разберём код:

  5. Соберите проект:
    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 {}
}

Что здесь происходит?

Соберите и прошейте код на STM32F1 (например, через openocd и arm-none-eabi-objcopy для конверсии в .bin). Светодиод на PA5 начнёт мигать.

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

"Сырой" доступ к регистрам сложен и опасен. В реальных проектах используют библиотеки абстракции: 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();
    }
}

Что изменилось?

Нюансы и подводные камни

Лучшие практики

Дополнительные возможности

no_std поддерживает асинхронное программирование через embassy — фреймворк для встраиваемых систем. Также есть alloc для динамической памяти в no_std с кастомным аллокатором (например, wee_alloc).

На этом мы завершаем разбор no_std. Вы узнали, как писать bare-metal код, работать с регистрами и использовать HAL. В следующем разделе мы перейдём к сетевым приложениям.


Раздел 3: Сетевые приложения: примеры библиотек

Rust благодаря своей производительности, безопасности и богатой экосистеме библиотек (crates) стал популярным выбором для разработки сетевых приложений. В этом разделе мы углубимся в мир сетевого программирования на Rust, рассмотрим ключевые библиотеки, их возможности, типичные сценарии использования, а также разберем примеры кода. Мы не ограничимся поверхностным обзором — вы получите полное представление о том, как писать надежные, масштабируемые и безопасные сетевые приложения, избегая распространенных ошибок.

Почему Rust для сетевых приложений?

Прежде чем погрузиться в библиотеки, давайте разберем, почему Rust так хорош в этой области:

Теперь перейдем к обзору ключевых библиотек и их применению.

1. Tokio: Асинхронный runtime для сетевых приложений

Tokio — это асинхронный runtime, который стал стандартом де-факто для сетевых приложений в Rust. Он предоставляет инструменты для работы с TCP/UDP-сокетами, таймерами, потоками и многим другим.

Пример: Простой TCP-сервер

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),
            }
        });
    }
}

Объяснение:

Совет: Используйте cargo add tokio --features full для полной функциональности.

2. Hyper: HTTP-клиент и сервер

Hyper — это низкоуровневая библиотека для работы с HTTP. Она часто используется как основа для более высокоуровневых фреймворков (например, Axum).

Пример: HTTP-сервер

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);
    }
}

Объяснение:

Совет: Для реальных приложений используйте Axum поверх Hyper.

3. Reqwest: HTTP-клиент для простоты

Reqwest — это высокоуровневый HTTP-клиент, построенный на Hyper, но с удобным API.

Пример: GET-запрос с JSON

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(())
}

Объяснение:

Совет: Добавьте cargo add serde serde_json для работы с JSON.

4. Actix-web: Веб-фреймворк

Actix-web — это мощный веб-фреймворк для создания серверов REST API и веб-приложений.

Пример: 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
}

Объяснение:

Совет: Используйте cargo add actix-web и изучите документацию для middleware.

Практические советы

  1. Выбор библиотеки:
  2. Обработка ошибок: Всегда используйте Result и логируйте ошибки с log или tracing.
  3. Производительность: Избегайте блокирующих операций в асинхронном коде.
  4. Тестирование: Используйте tokio::test для асинхронных тестов.

Заключение

Сетевые приложения в Rust — это сочетание мощи и безопасности. Выбор библиотеки зависит от ваших задач: от простых клиентов (Reqwest) до сложных серверов (Actix-web, Hyper, Tokio). В следующих разделах мы рассмотрим другие аспекты экосистемы Rust, но уже сейчас вы можете начать экспериментировать с примерами выше.


Раздел 4: Кроссплатформенные фреймворки

Rust, благодаря своей производительности, безопасности и гибкости, стал популярным выбором для разработки кроссплатформенных приложений. В этом разделе мы подробно разберём, как использовать кроссплатформенные фреймворки в экосистеме Rust, какие инструменты доступны, их сильные и слабые стороны, а также практические примеры их применения. Мы рассмотрим как графические интерфейсы (GUI), так и кроссплатформенные решения для мобильных и десктопных приложений. Этот раздел поможет вам понять, как Rust может быть использован для создания приложений, работающих на Windows, macOS, Linux, Android, iOS и даже в браузере через WebAssembly.

Зачем нужны кроссплатформенные фреймворки?

Кроссплатформенные фреймворки позволяют разработчикам писать код один раз и запускать его на разных платформах с минимальными изменениями. В мире Rust это особенно важно, так как язык изначально ориентирован на низкоуровневую производительность, что делает его конкурентом C++ в таких областях, как разработка GUI или мобильных приложений. Однако, в отличие от языков вроде JavaScript или Python, где кроссплатформенность часто достигается за счёт интерпретаторов или виртуальных машин, Rust полагается на компиляцию в нативный код, что требует от фреймворков особой гибкости.

Популярные кроссплатформенные фреймворки в Rust

Экосистема Rust предлагает несколько фреймворков для кроссплатформенной разработки. Мы рассмотрим основные из них, их особенности и сценарии использования.

1. Tauri

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). Убедитесь, что ваша целевая аудитория имеет совместимую ОС.

2. Dioxus

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.

3. Druid

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 Десктоп Очень высокая Средний Высокая

Практические советы

Заключение

Кроссплатформенные фреймворки в Rust открывают широкие возможности для создания приложений, работающих на разных устройствах. Tauri, Dioxus и Druid — это лишь вершина айсберга, и экосистема продолжает расти. Выбор подходящего инструмента зависит от ваших целей, но все они демонстрируют силу Rust: безопасность, производительность и гибкость.


Раздел 5: Примеры: сборка для браузера

Когда мы говорим о сборке Rust-проектов для браузера, мы имеем в виду использование WebAssembly (WASM) как целевой платформы. WebAssembly — это низкоуровневый байт-код, который выполняется в современных браузерах с почти нативной скоростью. Rust идеально подходит для этой задачи благодаря своей производительности, безопасности и гибкости. В этом разделе мы разберем несколько примеров сборки Rust-кода для браузера, начиная с простого "Hello, World!" и заканчивая более сложными сценариями, такими как взаимодействие с DOM и обработка событий. Мы также рассмотрим инструменты, настройки и потенциальные "подводные камни".

Почему Rust и WebAssembly для браузера?

Rust позволяет писать высокопроизводительный код, который компилируется в WASM, избегая накладных расходов JavaScript там, где это возможно. При этом инструменты вроде wasm-bindgen упрощают интеграцию с JavaScript и DOM, делая Rust мощным выбором для веб-разработки. Сценарии использования включают:

Инструменты для работы

Для сборки Rust-проектов под браузер вам понадобятся:

  1. Rustup — для управления версиями Rust и добавления цели wasm32-unknown-unknown.
  2. wasm-pack — утилита для упрощения сборки WASM и генерации JavaScript-оберток.
  3. npm или другой пакетный менеджер — для управления веб-проектом.
  4. Браузер с поддержкой WebAssembly (все современные браузеры подходят).

Установите их, если еще не сделали:

rustup target add wasm32-unknown-unknown cargo install wasm-pack npm install -g npm # Если 
npm еще не установлен

Пример 1: Простой "Hello, World!" в браузере

Давайте начнем с минимального примера, который выводит сообщение в консоль браузера.

1. Создание проекта

Создайте новый проект Rust:

cargo new wasm-hello --lib
cd wasm-hello

2. Настройка 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.

3. Код в 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).

4. Сборка с wasm-pack

Соберите проект:

wasm-pack build --target web

Флаг --target web указывает, что мы создаем модуль для прямого использования в браузере.

5. Создание HTML-файла

Создайте файл 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>

6. Запуск

Запустите локальный сервер (например, с помощью 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 с сгенерированными файлами.

Пример 2: Взаимодействие с DOM

Теперь усложним задачу: изменим текст элемента на странице.

1. Обновление 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, чтобы обрабатывать ошибки (например, если элемент не найден).

2. Обновление Cargo.toml

Добавьте web-sys с нужными функциями:

[dependencies.web-sys]
version = "0.3" features = ["Window", "Document", "Element"]

3. Обновление 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>

4. Сборка и запуск

wasm-pack build --target web python3 -m 
http.server 8000

Текст "Original text" изменится на "Updated by Rust!".

Подводный камень: Если вы забудете включить нужные функции в features для web-sys, компилятор выдаст ошибку. Всегда проверяйте документацию web-sys на crates.io.

Пример 3: Обработка событий

Добавим кнопку, которая вызывает Rust-функцию при клике.

1. Обновление 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() предотвращает уничтожение замыкания после выхода из функции.

2. Обновление Cargo.toml

[dependencies.web-sys]
version = "0.3" features = ["Window", "Document", "HtmlButtonElement", "Event"]

3. Обновление 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>

4. Сборка и запуск

После клика по кнопке текст изменится на "Clicked via Rust!".

Совет: Использование Closure требует осторожности. Если вы забудете вызвать forget(), обработчик события не сработает, так как замыкание будет уничтожено.

Практические советы

Подводные камни


Раздел 6: Упражнение: Собрать проект для WebAssembly

Введение в упражнение

В этом разделе мы закрепим знания о WebAssembly, полученные в предыдущих частях главы, на практике. Мы создадим небольшой проект на Rust, скомпилируем его в WebAssembly и запустим в браузере. Упражнение будет самодостаточным: вы получите пошаговые инструкции, примеры кода, объяснение каждого шага, а также разбор возможных проблем и их решений. Мы не ограничимся тривиальным примером «Hello, World», а добавим интерактивность, чтобы продемонстрировать реальные возможности WebAssembly в связке с Rust.

Цель упражнения:

Шаг 1: Подготовка окружения

Прежде чем начать, убедитесь, что у вас установлены необходимые инструменты. Если вы следовали предыдущим разделам, они уже должны быть готовы, но давайте перепроверим:

  1. Rust и Cargo: Убедитесь, что Rust установлен и обновлен до последней версии. Выполните в терминале:
    rustup update
  2. wasm-pack: Это утилита для сборки WebAssembly из Rust. Установите её, если ещё не сделали:
    cargo install wasm-pack
  3. Цель wasm32: Добавьте поддержку компиляции для WebAssembly:
    rustup target add wasm32-unknown-unknown
  4. Локальный веб-сервер: Для тестирования нам понадобится сервер. Рекомендуем http-server из npm:
    npm install -g http-server
    Или используйте любой другой сервер, например, встроенный в Python: python -m http.server.

Если что-то из этого не работает, проверьте доступность инструментов командой which wasm-pack (Linux/macOS) или where wasm-pack (Windows). Если пути не найдены, убедитесь, что переменная PATH настроена корректно.

Шаг 2: Создание проекта

Создадим новый проект с помощью Cargo:

cargo new wasm-counter --lib cd wasm-counter

Откройте файл Cargo.toml и добавьте зависимости:

[package] name = "wasm-counter" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2"

Шаг 3: Написание кода на Rust

Откройте src/lib.rs и замените его содержимое на следующий код:

use wasm_bindgen::prelude::*;
// Состояние счётчика
#[wasm_bindgen]
pub struct Counter { value: i32,
}
#[wasm_bindgen]
impl Counter {
    // Конструктор
    #[wasm_bindgen(constructor)]
    pub fn new() -> Counter { Counter { value: 0 }
    }
    // Увеличение счётчика
    pub fn increment(&mut self) { self.value += 1;
    }
    // Уменьшение счётчика
    pub fn decrement(&mut self) { self.value -= 1;
    }
    // Получение текущего значения
    pub fn get_value(&self) -> i32 { self.value
    }
}
// Функция для логирования в консоль браузера
#[wasm_bindgen]
pub fn log_message(msg: &str) { web_sys::console::log_1(&msg.into());
}

Разбор кода:

Добавим зависимость web-sys в Cargo.toml для работы с консолью:

[dependencies.web-sys] version = "0.3" features = ["console"] 

Шаг 4: Сборка проекта

Соберём проект с помощью wasm-pack:

wasm-pack build --target web

После выполнения команды в директории появится папка pkg, содержащая:

Шаг 5: Создание HTML-страницы

Создайте файл index.html в корне проекта:

<!DOCTYPE html> <html> <head>
    <meta charset="UTF-8"> <title>WebAssembly Counter</title> </head> <body> <h1>Counter</h1> <p>Value: 
    <span id="counter-value">0</span></p> <button id="increment">Increment</button> <button 
    id="decrement">Decrement</button> <script type="module">
        import init, { Counter, log_message } from './pkg/wasm_counter.js'; async function run() { await init(); // Инициализация WebAssembly const counter 
            = Counter.new(); const valueDisplay = document.getElementById('counter-value'); const incrementBtn = document.getElementById('increment'); 
            const decrementBtn = document.getElementById('decrement');
            // Обновление значения на странице
            function updateDisplay() { valueDisplay.textContent = counter.get_value();
            }
            // Обработчики кнопок
            incrementBtn.addEventListener('click', () => { counter.increment(); updateDisplay(); log_message("Incremented!");
            });
            decrementBtn.addEventListener('click', () => { counter.decrement(); updateDisplay(); log_message("Decremented!");
            });
            updateDisplay(); // Начальное значение
        }
        run(); </script> </body> </html> 

Разбор HTML:

Шаг 6: Запуск и тестирование

Запустите локальный сервер:

http-server . -p 8080

Откройте браузер по адресу http://localhost:8080. Вы увидите счётчик с кнопками «Increment» и «Decrement». Нажмите их и проверьте консоль разработчика (F12) — там будут сообщения от log_message.

Возможные проблемы и решения

  1. Ошибка «Module not found»:
  2. CORS-ошибки:
  3. Счётчик не обновляется:

Расширенный пример

Добавим в lib.rs ограничение на счётчик:

#[wasm_bindgen] impl Counter { pub 
    fn increment(&mut self) {
        if self.value < 10 { self.value += 1;
        } else {
            log_message("Maximum value reached!");
        }
    }
    pub fn decrement(&mut self) { if self.value > -10 { self.value -= 1;
        } else {
            log_message("Minimum value reached!");
        }
    }
}

Пересоберите проект (wasm-pack build --target web) и проверьте, как счётчик останавливается на границах ±10.

Итог

Вы создали полноценный интерактивный проект на Rust и WebAssembly. Это базовый пример, но он демонстрирует ключевые концепции: настройку окружения, интеграцию с JavaScript и сборку. Попробуйте расширить его, добавив, например, сброс счётчика или визуальные эффекты через DOM.