Rust Экосистема и ресурсы: веб, встраиваемое, WASM, куда двигаться дальше
0%

Экосистема и ресурсы: веб, встраиваемое, WASM, куда двигаться дальше

Экосистема и ресурсы: веб, встраиваемое, WASM, куда двигаться дальше

Двенадцать предыдущих статей отвечали на вопрос «как устроен язык». Эта отвечает на вопрос «что я теперь могу на нём построить и чем за это заплачу». Разница принципиальная: язык вы уже знаете, а вот где именно Rust — рациональный выбор, а где дорогая ошибка, из синтаксиса не выводится. Это свойство экосистемы: какие крейты зрелые, какие домены закрыты, сколько стоит найм, сколько идёт сборка, кто починит библиотеку, если её автор пропадёт.

Здесь важно честно признать одну вещь. Стандартная библиотека Rust намеренно маленькая: в ней нет HTTP-клиента, нет асинхронного рантайма, нет генератора случайных чисел, нет сериализации, нет регулярных выражений, нет TLS. В Go, C# и Java половина этого лежит «в коробке» и сопровождается вендором. В Rust всё это — сторонние крейты, и решение «какой взять» вы принимаете сами, в первый же день проекта. Именно поэтому статья про экосистему в треке Rust — не приложение к курсу, а обязательная его часть: выбор зависимости в Rust имеет больший вес, чем в большинстве других языков.

Заодно закроем то, о чём принято молчать: скорость компиляции, кривая обучения, стоимость команды и список доменов, куда Rust тащить не надо.

Как устроена экосистема: одна регистратура, тонкая стандартная библиотека

Технически всё просто: единственный публичный реестр crates.io, единый клиент cargo, документация автоматически собирается на docs.rs для каждой опубликованной версии. Счёт крейтов идёт на сотни тысяч, но полезная часть куда меньше, и ориентироваться в ней помогают кураторские списки — blessed.rs и lib.rs (последний — альтернативный фронтенд к тому же реестру, с более честной статистикой по сопровождению).

Устройство реестра порождает три следствия, которые лучше знать заранее.

Имена глобальные и не возвращаются. Пространств имён вроде @scope/package в crates.io нет: имя http занято раз и навсегда. Отсюда тайпсквоттинг и путаница «а rand_core — это от тех же людей, что rand?». Проверять принадлежность приходится по репозиторию, а не по имени.

Версия 0.x — это мажорная версия. Cargo трактует 0.4.1 и 0.5.0 как несовместимые. Практический эффект: reqwest 0.11 и reqwest 0.12 спокойно живут в одном дереве зависимостей, каждая тянет свою версию hyper, и вы получаете два HTTP-стека в бинаре и загадочную ошибку «expected Body, found Body» — типы из разных версий одного крейта разные. Лечится cargo tree --duplicates.

Фичи (features) аддитивны и склеиваются по всему графу. Если ваш крейт просит tokio без rt-multi-thread, а зависимость просит с ним — в сборку попадёт объединение. Отсюда правило: default-features = false плюс явный список — не педантизм, а способ управлять и размером бинаря, и временем сборки.

Экосистема во времени: что уже устоялось, а что ещё нет

Ключевой вопрос при выборе технологии — не «есть ли крейт», а «устоялся ли он». В Rust полезно помнить хронологию: язык молодой, и часть слоёв застыла буквально недавно.

Читать эту шкалу надо так: слои, застывшие в 2019–2021 годах (веб-бэкенд, CLI, сериализация), сегодня скучно-надёжны. Слои 2024–2025 годов (async в трейтах, компонентная модель WASM, embedded-hal 1.0) уже пригодны, но экосистема вокруг них ещё догоняет: часть драйверов и библиотек висит на старых версиях. А GUI и мобильная разработка не застыли до сих пор.

Выбор крейта: пять вопросов до cargo add

Зависимость в Rust дорого стоит по двум статьям сразу: она попадает в ваше время сборки навсегда и, если у неё есть build.rs или процедурный макрос, выполняет произвольный код на вашей машине и в вашем CI в момент компиляции. Это не теоретический риск, а тот же класс угроз, что разбирается в статье «Цепочка поставок». Пять вопросов, которые дешевле задать сейчас:

  1. Кто сопровождает и как давно. Дата последнего релиза, число открытых issue без ответа, есть ли организация за репозиторием. Крейт из одного человека — нормально; крейт из одного человека в фундаменте платёжного сервиса — риск, который надо назвать вслух.
  2. Какой MSRV (minimum supported Rust version) и есть ли политика его смены. Если вы собираете в Debian stable или в закрытом контуре, крейт, поднимающий MSRV в патч-релизе, сломает вам сборку.
  3. Сколько unsafe и есть ли аудит. Смотрите на cargo geiger, на наличие тестов под Miri, на упоминание аудита. Для криптографии и парсеров это первое, на что стоит смотреть.
  4. Что он тянет за собой. cargo tree -e normal до добавления — привычка, экономящая минуты сборки. Крейт на 12 строк, тянущий syn и proc-macro2, стоит дороже, чем кажется.
  5. Есть ли он в базе уязвимостей. RustSec Advisory Database плюс cargo audit. Отдельная категория — крейты со статусом «unmaintained»: они не уязвимы, но чинить их некому.

Автоматизируется это одним файлом. cargo-deny проверяет лицензии, дубликаты версий, запрещённые крейты и уязвимости — и ставится в CI рядом с тестами:

# deny.toml — конфигурация cargo-deny, запускается как `cargo deny check`
[advisories]
# Уязвимость валит сборку, «заброшенный крейт» — предупреждение с явным списком исключений.
yanked = "deny"
ignore = ["RUSTSEC-2024-0000"]   # осознанные исключения только с комментарием и сроком

[licenses]
allow = ["MIT", "Apache-2.0", "BSD-3-Clause", "ISC", "Unicode-3.0"]   # только проверенное юристами

[bans]
multiple-versions = "warn"          # две версии крейта в дереве — лишние минуты сборки и мегабайты
deny = [{ name = "openssl-sys" }]   # в проекте только rustls, системный OpenSSL не тянем

[sources]
unknown-registry = "deny"
unknown-git = "deny"

Для более серьёзных требований есть cargo-vet от Mozilla: он ведёт журнал «эту версию этого крейта человек посмотрел глазами» и умеет импортировать чужие аудиты. Это единственный масштабируемый ответ на вопрос «мы правда прочитали 400 зависимостей?» — не прочитали, но знаем, кто прочитал.

Веб-бэкенд: самый скучный и потому самый готовый слой

Если вы пишете HTTP-сервис, экосистема Rust сегодня даёт полный комплект. Основной выбор — между axum (поверх tokio и tower, ведётся командой tokio) и actix-web (свой рантайм акторов внутри, исторически лидер синтетических бенчмарков). По умолчанию берите axum: у него меньше собственных абстракций, а tower::Service — общий интерфейс middleware, который переиспользуют десятки крейтов. Типовой сервис выглядит так:

use axum::{extract::{Path, State}, http::StatusCode, routing::get, Json, Router};
use serde::Serialize;
use sqlx::PgPool;
use std::time::Duration;
use tower_http::{timeout::TimeoutLayer, trace::TraceLayer};

#[derive(Serialize, sqlx::FromRow)]
struct Order { id: i64, total_cents: i64, status: String }

// Хендлер — обычная async-функция. axum разбирает аргументы по типам:
// State отдаёт общее состояние, Path вытаскивает параметр из маршрута.
async fn get_order(
    State(pool): State<PgPool>,
    Path(id): Path<i64>,
) -> Result<Json<Order>, StatusCode> {
    let order = sqlx::query_as::<_, Order>("SELECT id, total_cents, status FROM orders WHERE id = $1")
        .bind(id)
        .fetch_optional(&pool)
        .await
        .map_err(|e| {
            // Ошибку БД логируем целиком, наружу отдаём только код: детали клиенту не показываем.
            tracing::error!(error = %e, "запрос к базе не удался");
            StatusCode::INTERNAL_SERVER_ERROR
        })?
        .ok_or(StatusCode::NOT_FOUND)?;
    Ok(Json(order))
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt::init();
    let pool = PgPool::connect(&std::env::var("DATABASE_URL")?).await?;

    let app = Router::new()
        // Внимание: в axum 0.8 параметр пути пишется в фигурных скобках.
        // В 0.7 и раньше было "/orders/:id" — это самая частая ошибка при обновлении.
        .route("/orders/{id}", get(get_order))
        .layer(TimeoutLayer::new(Duration::from_secs(3)))
        .layer(TraceLayer::new_for_http())
        .with_state(pool);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
    axum::serve(listener, app).await?;
    Ok(())
}

Ключевая идея, ради которой стоит понять tower: middleware — это не «список хуков фреймворка», а обёртка одного Service вокруг другого. Слои композируются как функции, порядок — снаружи внутрь, и любой слой может завершить обработку, не вызывая внутренний сервис.

Обратите внимание на строку с Note: отмена в Rust-асинхронности — это дроп future, а не сигнал внутрь задачи. Половина продовых сюрпризов на axum растёт отсюда, и разбирались они в статье «Асинхронный Rust».

Данные

Три подхода, и выбор между ними не эстетический:

  • sqlx — обычный SQL плюс проверка запросов на этапе компиляции: макрос query! ходит в живую базу и сверяет типы столбцов. Плата: для сборки в CI нужен либо доступ к БД, либо закоммиченный офлайн-кеш (cargo sqlx prepare, каталог .sqlx). Забыли обновить кеш после миграции — сборка падает, и это правильно.
  • diesel — типобезопасный DSL и синхронный API, самый строгий и самый «своя вселенная». Хорош там, где схема стабильна, а запросы сложные.
  • SeaORM — привычная ORM поверх sqlx, ближе к тому, что ожидает человек из мира Django или EF Core.

Смежные вопросы (индексы, планы, изоляция) язык не решает: они разбираются в треке «Базы данных», в частности в статье «Индексы и планы запросов».

Грабли веб-стека: как читать «нереализованный трейт Handler»

Самое пугающее сообщение новичка в axum выглядит так:

error[E0277]: the trait bound `fn(Path<i64>, State<PgPool>) -> impl Future<Output = ...> {get_order}: Handler<_, _>` is not satisfied
   --> src/main.rs:38:32
    |
38  |         .route("/orders/{id}", get(get_order))
    |                                --- ^^^^^^^^^ the trait `Handler<_, _>` is not implemented
    |                                |
    |                                required by a bound introduced by this call
    |
    = note: Consider using `#[axum::debug_handler]` to improve the error message

Сообщение почти не помогает, потому что Handler реализован через набор blanket-impl для функций с подходящими аргументами: не подошёл один аргумент — «не реализован» весь трейт. Реальных причин ровно четыре, и они перечисляются быстрее, чем читается ошибка:

  1. Экстрактор, потребляющий тело запроса (Json, String, Bytes, Form), стоит не последним. Тело можно прочитать один раз, поэтому такой экстрактор обязан быть последним аргументом.
  2. Тип возврата не реализует IntoResponse. Result<Json<T>, MyError> работает только если у MyError есть impl IntoResponse.
  3. Future хендлера не Send. Держите Rc, RefCell или MutexGuard через .await — и всё разваливается ровно здесь, хотя причина в другой строке.
  4. Тип состояния не совпадает с тем, что задан в with_state.

Практический рецепт: повесить #[axum::debug_handler] на функцию. Макрос разворачивает проверку в понятное сообщение с указанием конкретного аргумента. Это общий приём для Rust: когда ошибка приходит от blanket-impl, ищите инструмент, который проверяет условия по одному.

Честно: где Rust на бэкенде выигрывает, а где нет

Выигрывает, когда важен хвост распределения задержек и предсказуемость: нет сборщика мусора — нет пауз, о которых рассказывает статья «Сборка мусора». Классический публичный кейс — переход Discord с Go на Rust: проблема была не в средней скорости, а в периодических всплесках задержки от GC. Выигрывает на прокси и сетевых узлах, где важен расход памяти на соединение — так появился Pingora от Cloudflare. Выигрывает на serverless из-за времени холодного старта и на CPU-bound обработке (парсинг, сжатие, криптография, конвертация форматов).

Проигрывает на обычном CRUD с быстро меняющимися требованиями. Каждое изменение схемы данных в Rust — это правка типов, а компилятор потребует обработать все случаи. Там, где в Python вы за час собираете эндпоинт и правите его пять раз в неделю, Rust будет стоить дороже — и это не недостаток языка, а несовпадение с задачей. Проигрывает там, где нужны богатые зрелые интеграции с корпоративными системами: SDK для «редкого» вендора чаще есть под Java и C#, чем под Rust. Проигрывает при найме: инженеров с продовым Rust-опытом на рынке в разы меньше.

WebAssembly: не одна технология, а две

Слово «WASM» в резюме скрывает два почти не пересекающихся мира. Они отличаются целью сборки, набором доступного API и типом решаемых задач.

Граница wasm: числа через границу, данные в линейной памяти

Схема объясняет главное ограничение обоих миров: через границу проходят только числа. Строка передаётся как пара (указатель, длина), а сами байты хост читает прямо из линейной памяти модуля. Отсюда следуют и стоимость (копирование на каждом вызове), и безопасность (модуль не может дотянуться ни до чего, что ему не передали импортом).

Мир первый: браузер, цель wasm32-unknown-unknown

Задача — унести в браузер вычисление, которое на JS слишком медленное или которое уже написано на Rust: обработка изображений, разбор бинарных форматов, шифрование, физика, шахматный движок, редактор с CRDT. Инструменты: wasm-bindgen генерирует клей между Rust и JS, wasm-pack собирает npm-пакет, web-sys и js-sys дают типизированный доступ к браузерным API.

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    // Импорт: функция хоста, которую модуль сможет вызвать.
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

/// Экспорт: из JS вызывается как обычная функция после await init().
#[wasm_bindgen]
pub fn normalize_csv(input: &str) -> String {
    log(&format!("на входе {} байт", input.len()));
    input.lines().map(|l| l.trim().to_lowercase()).collect::<Vec<_>>().join("\n")
}
[lib]
crate-type = ["cdylib", "rlib"]   # без cdylib файл .wasm просто не появится

[profile.release]
opt-level = "z"      # оптимизируем размер, а не скорость: в браузере важнее вес
lto = true           # межмодульная оптимизация выкидывает неиспользуемое
codegen-units = 1
panic = "abort"      # раскрутка стека в wasm стоит килобайтами кода
strip = true

Три грабли, на которые наступают все:

Размер. «Hello, world» на wasm-bindgen — это не 5 КБ, а десятки килобайт, и без настроек профиля легко получить пару сотен. Порядок действий: профиль как выше → wasm-opt -Oz из binaryen → анализ через twiggy, которая покажет, кто именно занял место. Чаще всего виноваты форматирование строк, паники с сообщениями и serde_json. Отдельно: wee_alloc больше не рекомендуется — крейт не сопровождается и имеет известные проблемы; берите системный аллокатор по умолчанию либо talc/lol_alloc.

Паника без текста. С panic = "abort" паника превращается в инструкцию unreachable, и в консоли вы увидите RuntimeError: unreachable executed без единого слова о причине. Лечится крейтом console_error_panic_hook, подключённым в debug-сборке.

Потоков нет по умолчанию. wasm-потоки требуют SharedArrayBuffer, а он требует заголовков кросс-изоляции Cross-Origin-Opener-Policy и Cross-Origin-Embedder-Policy на вашем сайте. Это инфраструктурное решение, а не флаг компилятора; wasm-bindgen-rayon помогает, но заголовки всё равно ставить вам.

Отдельная ветка — фронтенд-фреймворки целиком на Rust: Leptos, Dioxus, Yew. Они работают и умеют SSR с гидратацией, но платите вы размером бандла и незрелостью экосистемы компонентов. Здравая позиция: на Rust имеет смысл писать вычислительное ядро, а UI оставить тому, для чего браузер создавался — соображения из статьи «Веб-производительность» никуда не деваются, и лишние 300 КБ wasm легко съедят весь выигрыш.

Мир второй: WASI, цель wasm32-wasip1 и компонентная модель

Здесь wasm — не «ускоритель для браузера», а переносимая песочница на сервере. Модуль запускается в Wasmtime или WasmEdge, доступ к файлам, сети и часам получает через WASI, причём только к тому, что явно разрешил хост. Это делает wasm удобным форматом плагинов: пользовательский код исполняется внутри вашего процесса, но не может ни читать чужие файлы, ни ходить в сеть. Так работают фильтры Envoy (proxy-wasm), пользовательские трансформации в Vector, плагины Zellij и функции в Spin. Первое, обо что тут спотыкаются: цель надо доустановить, и она была переименована.

# Старое имя wasm32-wasi удалено в Rust 1.84; актуальные — wasm32-wasip1 и wasm32-wasip2.
rustup target add wasm32-wasip1
cargo build --release --target wasm32-wasip1
wasmtime run --dir=. target/wasm32-wasip1/release/tool.wasm

Без установленной цели компилятор скажет ровно то, что нужно сделать:

error[E0463]: can't find crate for `core`
  |
  = note: the `wasm32-wasip1` target may not be installed
  = help: consider downloading the target with `rustup target add wasm32-wasip1`

Это, кстати, хорошая иллюстрация общего свойства: сообщения rustc почти всегда содержат следующий шаг. Привычка дочитывать до help: экономит часы.

Компонентная модель (wasm32-wasip2, wit-bindgen, cargo-component) — следующий слой: интерфейсы описываются на языке WIT, а не «числами в линейной памяти», и компоненты на разных языках соединяются без ручного клея. Технология уже пригодна, но экосистема догоняет; закладываться на неё стоит осознанно, а не «потому что будущее».

Встраиваемое: no_std — это отдельный диалект

Здесь Rust претендует на территорию C, и претендует всерьёз: те же байты, та же прошивка, но без переполнений буфера и с типами, которые не дают перепутать пины. Чтобы туда войти, надо сначала понять, из чего состоит стандартная библиотека.

Слои core, alloc и std и их доступность на разных целях

Крейт с #![no_std] теряет std, но не теряет язык: трейты, дженерики, итераторы, проверка заимствований работают в прошивке так же, как на сервере. Отсюда лучшая практика для библиотек: писать на core, а alloc и std включать фичами — тогда один крейт едет и в прошивку, и в браузер, и на сервер.

Слоистость драйверов зафиксирована крейтом embedded-hal, версия 1.0 которого вышла в 2024 году и наконец дала стабильный контракт: драйвер датчика пишется один раз против трейтов и работает на любом чипе, у которого есть их реализация.

Ценность схемы — в стрелке от bme280 к трейтам: драйвер не знает про ваш чип. Это ровно та развязка, которую в C делают через указатели на функции и void*, только здесь она проверяется компилятором и не стоит ни байта на этапе выполнения — мономорфизация схлопывает всё в прямые вызовы.

Асинхронный код в прошивке — не роскошь: Embassy даёт исполнителя, который вместо ожидания в цикле усыпляет ядро до прерывания, что напрямую снижает потребление.

#![no_std]      // std недоступен: нет ОС
#![no_main]     // точку входа даёт cortex-m-rt, а не рантайм std

use embassy_executor::Spawner;
use embassy_stm32::gpio::{Level, Output, Speed};
use embassy_time::{Duration, Timer};
// Крейты подключаются «ради побочного эффекта»: логгер и обработчик паники.
use {defmt_rtt as _, panic_probe as _};

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());
    let mut led = Output::new(p.PC13, Level::High, Speed::Low);

    loop {
        led.toggle();
        // Здесь нет busy-wait: исполнитель уводит ядро в сон до срабатывания таймера,
        // и ток потребления падает на порядок по сравнению с пустым циклом задержки.
        Timer::after(Duration::from_millis(500)).await;
    }
}

Две ошибки компилятора, которые встречает каждый в первый день, и что они значат на самом деле:

error[E0463]: can't find crate for `std`
  |
  = note: the `thumbv7em-none-eabihf` target does not support the standard library
  = note: `std` is required by `firmware` because it does not declare `#![no_std]`

Значение: вы (или любая ваша зависимость) просите std на цели, где ОС нет. Ищите виновника через cargo tree и default-features = false — чаще всего это serde без no_std-фичи или крейт логирования.

error: `#[panic_handler]` function required, but not found

Значение: в std обработчик паники даёт рантайм; здесь его нет, и решение — ваше. panic-halt (зависнуть), panic-probe (сообщить отладчику), panic-reset (перезагрузиться) — это разные продуктовые решения, а не разные библиотеки. На реальном устройстве выбор между «зависнуть» и «перезагрузиться» обсуждается с теми, кто отвечает за безопасность изделия.

Рабочий инструментарий: probe-rs вместо связки OpenOCD и GDB (cargo embed, probe-rs run прошивает и выводит логи), defmt вместо форматирования строк на устройстве (форматирование делает хост, во флеш едут только идентификаторы), cargo size и cargo bloat для контроля образа. Для Cortex-M альтернатива Embassy — RTIC, который строит статическое расписание на прерываниях и доказывает отсутствие гонок на этапе компиляции. Espressif официально поддерживает Rust для ESP32 (esp-rs), для Raspberry Pi Pico есть embassy-rp.

Аппаратная база, прерывания, питание и протоколы — не тема языкового трека; они разбираются в треке «Встраиваемые системы», начиная с обзора, и особенно в статьях «Прерывания и тайминг» и «RTOS». Сравнение с тем, как то же самое делается на C, — в «C для встраиваемых».

Честная оговорка: во встраиваемом Rust вы регулярно упираетесь в отсутствие драйвера для конкретной микросхемы, в PAC, сгенерированный из кривого SVD, и в необходимость написать unsafe-обёртку самому. Механику этого разбирала статья «unsafe и FFI». Сертифицируемые применения закрывает Ferrocene — квалифицированный тулчейн для ISO 26262 и IEC 61508.

Остальная карта: где Rust уже стал нормой

  • Инструменты командной строки. clap для аргументов, ratatui для TUI, indicatif для прогресса. Один статически слинкованный файл без рантайма — причина, по которой ripgrep, fd, bat, eza, zoxide, difftastic написаны именно так.
  • Инструменты для других экосистем. ruff и uv (Python), swc, oxc, biome, rolldown (JavaScript), turbopack. Показательно: узкое место инструментария динамических языков закрывают на Rust, потому что там время запуска и потребление памяти видны разработчику каждую минуту.
  • Данные и аналитика. Polars как замена pandas, Apache Arrow и DataFusion как движок запросов, rayon для параллельных итераторов в одну строку. Смежные вопросы форматов — в статье «Хранение и форматы».
  • Инфраструктура. Firecracker (микровиртуальные машины AWS), TiKV, Vector, linkerd2-proxy, Meilisearch, Qdrant, InfluxDB. Здесь Rust выигрывает по той же причине, что и на прокси: память и хвосты задержек. Контекст — треки «Операционные системы» (например, «Виртуализация и контейнеры») и «Производительность систем».
  • Криптография и сеть. rustls как TLS-стек без OpenSSL, quinn и s2n-quic для QUIC. Теория — в статьях «TLS» и «UDP и QUIC».
  • Блокчейн. Solana, Substrate, Foundry, reth — целый пласт индустрии на Rust; см. трек Web3 и статью «Смарт-контракты».
  • Игры и графика. Bevy как ECS-движок, wgpu как переносимый графический API (он же лежит в основе WebGPU в Firefox). Готовность — «можно делать инди», а не «можно делать AAA».
  • Машинное обучение. candle, burn, привязки tch-rs и ort. Обучение моделей живёт в Python и там останется; ниша Rust — инференс и обвязка (tokenizers, safetensors от HuggingFace написаны на Rust), то есть та часть, что описана в статье «MLOps».
  • GUI и десктоп. Tauri (веб-UI, нативный бэкенд), egui, iced, slint. Состояние честно отражает сайт areweguiyet.com: «ещё нет».

Встраивание в чужой стек: самый дешёвый способ принести Rust в компанию

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

Python. PyO3 и maturin дают нативный модуль, который импортируется как обычный пакет и ставится через pip. Так устроены pydantic-core, polars, tokenizers, orjson, cryptography.

use pyo3::prelude::*;

/// Тяжёлая функция, ради которой всё затевалось.
#[pyfunction]
fn count_ngrams(py: Python<'_>, text: &str, n: usize) -> PyResult<usize> {
    // Отпускаем GIL на время счёта: пока Rust работает, другие потоки Python живут.
    // Забыть эту строку — самая частая причина «переписали на Rust, а быстрее не стало».
    Ok(py.allow_threads(|| text.as_bytes().windows(n).filter(|w| !w.contains(&b' ')).count()))
}

#[pymodule]
fn fasttext_utils(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(count_ngrams, m)?)?;
    Ok(())
}

Ключевой момент — граница. Выгодно переносить крупные куски работы: вызов Rust из Python в цикле по миллиону элементов съест выигрыш на маршалинге. Переносите цикл целиком, а не тело цикла. Дальше — сборка колёс под все платформы (maturin умеет это в GitHub Actions), и с этого момента пользователи вашего пакета даже не знают, что внутри Rust.

Node.jsnapi-rs по той же схеме. C и C++bindgen для заголовков C и cxx для безопасного двустороннего моста с C++. Мобильные приложенияUniFFI от Mozilla генерирует биндинги для Kotlin и Swift, что позволяет держать одну реализацию бизнес-логики на две платформы. Go — технически возможно через cgo, практически болезненно: разные модели рантайма и стеков, накладные расходы на переход и потеря главного преимущества Go — простой сборки; чаще выгоднее вынести Rust в отдельный сервис.

Крупные публичные истории, на которые можно ссылаться в разговоре с руководством: Android снизил долю уязвимостей памяти с 76% до 24% по мере роста доли Rust в новом коде; ядро Linux приняло Rust как второй язык начиная с версии 6.1; Microsoft переписывает части Windows; Cloudflare, AWS (Firecracker, s2n-quic), Dropbox, Figma, Discord используют Rust в проде годами. Аргумент «это экзотика» в 2026 году не работает — работает аргумент «это дорого в найме», и он честный.

Честная цена

Компиляция

Это главная ежедневная боль, и её нельзя заговорить. Причины системные: мономорфизация дженериков порождает много кода, процедурные макросы (serde, sqlx) выполняются при каждой сборке, LLVM тщательно оптимизирует, компоновка большого бинаря небыстрая. Порядок величин для типичного сервиса на axum, sqlx и tokio (около 300–400 крейтов в графе) на восьми ядрах:

Операция Время Как сократить
cargo check инкрементально 1–5 с это ваш основной цикл; IDE делает то же через rust-analyzer
cargo build инкрементально (debug) 5–30 с mold/lld вместо стандартного компоновщика; debug = 0
cargo build с нуля (debug) 1–3 мин sccache для кеша между ветками и в CI
cargo build --release с нуля 3–10 мин в CI кешируйте ~/.cargo и target, собирайте release только на выкатку
cargo test большого проекта зависит cargo nextest run — параллельный запуск и внятный вывод

Что действительно помогает, по убыванию эффекта: не тянуть лишнее (default-features = false, cargo tree --duplicates, отказ от крейта ради трёх строк); быстрый компоновщик (mold на Linux нередко срезает половину времени инкрементальной сборки); cargo check вместо build в цикле правок; workspace вместо монокрейта, чтобы перекомпилировался только изменённый пакет; cargo build --timings, чтобы увидеть, кто именно съедает минуты, а не гадать. Оптимизировать зависимости, но не свой код, в dev-профиле — полезный трюк для тестов на реальных данных:

[profile.dev]
opt-level = 0      # ваш код: быстро компилируется, медленно работает
debug = 1          # таблицы строк хватает для бэктрейсов, а линковка заметно быстрее

[profile.dev.package."*"]
opt-level = 2      # зависимости оптимизируются один раз и потом просто лежат в кеше

Методику измерения этих улучшений — что мерить, как не обмануться шумом — даёт статья «Честный бенчмаркинг».

Кривая обучения

Реалистичные сроки для опытного инженера: две-три недели до «пишу и оно компилируется», два-три месяца до «не борюсь с заимствованиями», полгода-год до уверенного проектирования API с временами жизни и трейтами. Самый крутой участок — не синтаксис, а смена модели: необходимость заранее решать, кто чем владеет, тогда как в языках с GC этот вопрос просто не задаётся. Возвращайтесь к статьям «Владение и заимствование» и «Времена жизни» не как к теории, а как к справочнику — большинство «непонятных ошибок» на третьем месяце оказываются одним из пяти базовых случаев.

Отдельно про компилятор: умение читать rustc — половина навыка. Сообщение почти всегда содержит note: с причиной и help: со следующим шагом, а rustc --explain E0502 разворачивает код ошибки в статью с примерами. Инженер, который читает ошибку до конца, учится в разы быстрее того, кто ищет её текст в поиске.

Когда Rust избыточен

Прямым текстом, чтобы не пришлось выяснять это на проекте:

  • Прототип и исследование. Требования меняются ежедневно, код живёт неделю. Стоимость строгости не окупается.
  • Скрипты и склейка. Bash, Python и Go написаны для этого; Rust даст вам полминуты компиляции ради двадцати строк.
  • CRUD-сервис средней нагрузки со сроками. Если p99 в 50 мс никому не нужен, а нужен эндпоинт к пятнице, выбирайте то, на чём команда пишет быстро.
  • Команда без опыта и без времени на обучение. Три месяца просадки скорости — реальная цена, её надо планировать, а не обнаруживать.
  • Домены с зависимостью от зрелых SDK. Если ключевая интеграция существует только под Java, спор окончен.
  • Тяжёлый GUI и мобильный UI. Экосистема пока не даёт того, что дают нативные наборы и Flutter.

Симметрично: если у вас сетевой узел с миллионом соединений, прошивка на 64 КБ ОЗУ, песочница для чужого кода, ускоритель для Python, парсер бинарного формата или сервис, где паузы GC уже стоили денег, — Rust не «модный выбор», а профильный инструмент.

Ресурсы: что читать в следующие полгода

Официальное, бесплатное, обязательное

Книги, которые стоят своих денег

  • Jon Gjengset. Rust for Rustaceansnostarch.com/rust-rustaceans. Лучшее «что дальше» после книги: типы, вариантность, unsafe, проектирование API. Его канал Crust of Rust — многочасовые разборы вживую.
  • Mara Bos. Rust Atomics and Locksmarabos.nl/atomics/, доступна бесплатно. Единственная книга, после которой модель памяти и Ordering перестают быть заклинанием.
  • Jim Blandy и др. Programming Rust, 2-е изд. (O’Reilly) — самое подробное описание языка с системным уклоном.
  • Luca Palmieri. Zero To Production In Rustzero2prod.com. Один сквозной продовый сервис: тесты, миграции, телеметрия, деплой. Идеальное дополнение к статье «Прод на Rust».
  • Ken Youens-Clark. Command-Line Rust (O’Reilly) — упражнения через переписывание утилит Unix.

Держать руку на пульсе

Практика важнее чтения. Порядок задач, который работает: (1) утилита CLI на 200 строк с clap и тестами; (2) HTTP-сервис с базой, миграциями и tracing, выкаченный в контейнере; (3) перенос узкого места из вашего рабочего проекта в Rust через PyO3 или napi-rs — с замером до и после; (4) что-то из «странного»: прошивка мигающего светодиода, wasm-плагин, парсер бинарного формата на nom; (5) публикация своего крейта на crates.io с документацией и doc-тестами по правилам из статьи «Тестирование и документация». Пятый пункт закрывает больше пробелов, чем первые четыре: публичный API заставляет думать про времена жизни, обратную совместимость и сообщения об ошибках.

Мини-итог

  • Стандартная библиотека Rust маленькая намеренно; из-за этого выбор зависимостей — часть архитектуры, а не деталь. cargo-deny, cargo-audit и cargo tree --duplicates ставятся в CI в первый день.
  • Веб-бэкенд, CLI, сетевая инфраструктура и инструментарий — зрелые слои: здесь Rust скучен и предсказуем. GUI, фронтенд целиком и обучение ML-моделей — нет.
  • «WASM» — это два разных мира: браузерный wasm32-unknown-unknown с wasm-bindgen и серверный WASI с песочницей и плагинами. Через границу проходят только числа, всё остальное копируется через линейную память.
  • no_std — не урезанный Rust, а другой слой: core всегда, alloc при наличии аллокатора, std при наличии ОС. Библиотеки полезно писать на core с фичами.
  • Самый дешёвый путь внедрения — не переписывание, а точечная замена узкого места через PyO3, napi-rs, cxx или UniFFI, с замером до и после.
  • Цена реальна: минуты сборки, месяцы обучения, узкий рынок найма. Есть большой класс задач, где Rust — неправильный ответ, и умение это сказать — часть профессионализма.
  • Умение читать сообщения rustc (до note: и help:, плюс --explain) ускоряет обучение сильнее любого курса.

Источники

Что дальше

Трек «Rust» закончен. Мы прошли путь от обзора языка и его философии и инструментария через основы, владение, времена жизни, типы и трейты, ошибки, коллекции и итераторы, конкурентность, асинхронность, unsafe и FFI, тестирование и прод — к карте экосистемы. Дальше стоит выбирать не следующую статью, а следующий контекст:

  • Понять машину, на которой всё это исполняется — трек «Системное программирование» на портале: память, процессы, системные вызовы с точки зрения программиста, который пишет близко к железу. Прямое продолжение статей о владении и об unsafe.
  • Разобраться с платформойОперационные системы, особенно «Управление памятью» и «Системные вызовы и IPC»: то, что Rust даёт вам в виде типов, ядро реализует в виде страниц и дескрипторов.
  • Научиться доказывать, что стало быстрееПроизводительность систем и «Честный бенчмаркинг». Переписать на Rust и не измерить — самый дорогой способ ошибиться.
  • Уйти во встраиваемоеВстраиваемые системы: железо, прерывания, питание, протоколы. no_std из этой статьи там станет ежедневной средой.
  • Достроить бэкенд-картинуБазы данных, Распределённые системы и DevOps: язык — меньшая часть продового сервиса.
  • Сравнить моделиGo с его горутинами и сборщиком мусора, Elixir с моделью акторов и отказоустойчивостью: понимание того, что именно вы выбрали в Rust, приходит от сравнения с альтернативой.

Общая карта портала и разумный порядок изучения треков — в «Дорожной карте».

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов