Стандартная библиотека Rust на самом деле состоит из трёх слоёв: core (не требует ни аллокатора, ни ОС — примитивные типы, Option, Result, трейты вроде Iterator), alloc (коллекции вроде Vec и Box, но требует реализации глобального аллокатора) и собственно std, которая добавляет поверх файлы, сеть, потоки и опирается на конкретную ОС. Атрибут #![no_std] убирает неявный extern crate std и оставляет только core — актуально для микроконтроллеров без ОС, ядер, загрузчиков и WASM-модулей без рантайма.
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use cortex_m_rt::entry;
use embedded_hal::digital::OutputPin;
// В no_std нет std::process::abort — панику нужно обработать вручную,
// иначе линковка не пройдёт: компилятору нужен ровно один panic_handler
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
// entry — атрибут cortex-m-rt, заменяет обычный fn main(),
// т.к. нет рантайма ОС, который бы его вызывал стандартным способом
#[entry]
fn main() -> ! {
let mut led = get_led_pin(); // HAL-абстракция конкретной платы
loop {
led.set_high().ok();
delay_ms(500);
led.set_low().ok();
delay_ms(500);
}
}
Экосистема embedded строится вокруг нескольких слоёв абстракции: embedded-hal — набор трейтов (OutputPin, SpiBus, I2c и т.д.), не привязанных к конкретному чипу, поверх которых драйверы периферии пишутся один раз и работают на любой плате, реализующей эти трейты; cortex-m / cortex-m-rt — низкоуровневый доступ к регистрам ARM Cortex-M и точка входа программы; probe-rs — современный инструмент для прошивки и отладки через SWD/JTAG, пришедший на смену связке OpenOCD + GDB для многих сценариев.
| Возможность | std | alloc (no_std + аллокатор) | core (чистый no_std) |
|---|---|---|---|
| Примитивы, Option/Result, трейты | Да | Да | Да |
| Vec, Box, String, BTreeMap | Да | Да (нужен #[global_allocator]) | Нет |
| HashMap (нужен random state из ОС) | Да | Нет напрямую | Нет |
| Потоки, Mutex со syscalls | Да | Нет | Нет |
| Файлы, сеть, env | Да | Нет | Нет |
| panic = "unwind" | Да (по умолчанию) | Обычно panic = "abort" | Нужен свой #[panic_handler] |
no_std-совместимыми по умолчанию (сериализаторы, крипто, парсеры), даже если целевая аудитория — обычные серверные приложения: это расширяет применимость на embedded и WASM бесплатно, если код не тянет неявных зависимостей от std.
Мы уже видели базовый синтаксис const N: usize в дженериках (тема 8). На expert-уровне важно понимать границы: сегодня в стабильном Rust параметром const generic может быть только цельная константа (число, bool, простой enum), но константные выражения в generic-позиции — то есть арифметика над const-параметрами прямо в сигнатуре типа — до сих пор требуют nightly-фичи generic_const_exprs.
// Стабильно: N используется "как есть"
struct Buffer<const N: usize> {
data: [u8; N],
len: usize,
}
impl<const N: usize> Buffer<N> {
const CAPACITY: usize = N;
fn new() -> Self {
Buffer { data: [0; N], len: 0 }
}
fn push(&mut self, byte: u8) -> Result<(), &'static str> {
if self.len == Self::CAPACITY {
return Err("buffer full");
}
self.data[self.len] = byte;
self.len += 1;
Ok(())
}
}
// Нестабильно на сегодня: N * 2 как размер поля требует #![feature(generic_const_exprs)]
// struct DoubleBuffer<const N: usize> {
// data: [u8; N * 2], // ошибка на stable: "generic parameters may not be used in const operations"
// }
Обходной путь на стабильном Rust — не вычислять размер внутри сигнатуры, а передавать уже готовую константу отдельным параметром, либо использовать trait-based диспетчеризацию через ассоциированные константы, либо генерировать код макросом на этапе компиляции. Матрицы, буферы протоколов, криптографические блоки фиксированной длины — все они на практике останавливаются именно на этой границе.
GATs (стабилизированы в Rust 1.65) позволяют ассоциированному типу трейта иметь собственные generic-параметры, включая lifetime — то, что раньше можно было указать только у самого трейта, а не у его associated type. Без GATs эта информация была "заморожена" на уровне impl, что не позволяло, например, ассоциированному типу заимствовать данные с временем жизни, зависящим от конкретного вызова метода.
Классическая мотивация — LendingIterator: итератор, чей Item заимствует из самого итератора (а не из внешних данных), что невозможно выразить обычным std::iter::Iterator, потому что его Item не параметризован временем жизни.
trait LendingIterator {
// Item сам параметризован lifetime 'a — это и есть GAT
type Item<'a>
where
Self: 'a;
fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}
struct WindowsMut<'buf> {
slice: &'buf mut [u8],
pos: usize,
window: usize,
}
impl<'buf> LendingIterator for WindowsMut<'buf> {
// Каждый вызов next() возвращает срез, заимствующий из self,
// с lifetime, привязанным к конкретному вызову — не к 'buf целиком
type Item<'a> = &'a mut [u8] where Self: 'a;
fn next<'a>(&'a mut self) -> Option<&'a mut [u8]> {
if self.pos + self.window > self.slice.len() {
return None;
}
let start = self.pos;
self.pos += 1;
Some(&mut self.slice[start..start + self.window])
}
}
Помимо lending-итераторов, GATs полезны для абстракций над storage-слоями (associated тип "handle", заимствующий из конкретного backend'а), async-трейтов до появления полноценной поддержки async fn (Future как GAT, зависящий от &self), и generic-обёрток над сериализацией с zero-copy заимствованием.
LendingIterator не входит в std именно потому, что он несовместим с большинством комбинаторов обычного Iterator (map, filter и т.д. требуют, чтобы элементы жили независимо от итератора) — GATs решают одну проблему, но открывают другую.
Specialization — возможность иметь несколько impl одного трейта для пересекающихся множеств типов, где компилятор выбирает "более специфичную" реализацию, если она применима, и падает обратно на общую (generic) — по аналогии с перегрузкой функций в других языках, но в терминах trait impl.
// Желаемый (nightly-only, #![feature(min_specialization)]) сценарий:
// generic impl для любого T: Clone, и более быстрый — специально для Copy-типов
#![feature(min_specialization)]
trait FastClone {
fn fast_clone(&self) -> Self;
}
// Базовая реализация — подходит для всех Clone-типов
impl<T: Clone> FastClone for T {
default fn fast_clone(&self) -> Self {
self.clone()
}
}
// Специализация — для Copy-типов используем memcpy напрямую, минуя Clone::clone
impl<T: Copy> FastClone for T {
fn fast_clone(&self) -> Self {
*self
}
}
Полная специализация остаётся nightly-only годами (сначала #![feature(specialization)], затем более узкий и безопасный min_specialization, используемый внутри самого rustc и std) из-за проблем с soundness: специализация плохо сочетается с lifetime-параметрами (специфичная реализация может незаконно "заглянуть" в более короткое время жизни, чем позволяет базовая) и с coherence-правилами оркестрации трейтов между крейтами. Найденные баги позволяли получить UB в safe-коде — поэтому стабилизация заморожена до тех пор, пока не будет формально доказанной модели.
На стабильном Rust вместо специализации применяют:
match вместо неявного выбора impl компилятором.&&T vs &T) для получения specialization-подобного поведения в macro_rules! на стабильном канале; используется, например, в некоторых логирующих и тестовых крейтах, но остаётся хрупким и не документированным официально.Rust распространяется по трём каналам: stable (выходит раз в ~6 недель, обратно совместим по эдишн-политике), beta (кандидат в следующий stable, для раннего тестирования регрессий) и nightly (собирается ежедневно, содержит все экспериментальные фичи под флагами). Доступ к нестабильной фиче открывается атрибутом уровня крейта:
// Работает только при компиляции nightly-тулчейном
#![feature(portable_simd)]
use std::simd::f32x4;
fn add_vectors(a: f32x4, b: f32x4) -> f32x4 {
a + b // SIMD-сложение без ручных platform intrinsics
}
Каждая нестабильная фича проходит через RFC-процесс (Request for Comments) для крупных языковых изменений, либо более лёгкий tracking issue в repo rust-lang/rust для API-дополнений std. Библиотеки требуют nightly, когда им нужны возможности, которые в принципе невозможно эмулировать на stable: generic_const_exprs для сложной const-арифметики, portable_simd для переносимых SIMD-типов, specialization для оптимизаций диспетчеризации, allocator_api для кастомных аллокаторов в коллекциях std.
Несколько направлений развития стабильно остаются в фокусе команды языка и сообщества:
async fn внутри трейта уже доступен на stable, но полная эквивалентность обычным трейтам (object safety для dyn Trait с async-методами, тонкая настройка `Send`-бондов на возвращаемый Future) продолжает дорабатываться — это прямое следствие тех же проблем, что решают GATs, потому что возвращаемый Future — это, по сути, associated type с лишним lifetime.RELEASES.md в репозитории rust-lang/rust и rustc release notes при обновлении тулчейна в проекте.
Большая часть материала этой темы — специализированные инструменты, а не то, что должно появляться в обычном бизнес-коде по умолчанию.
[T; N] — там, где реально нужна compile-time проверка размерностей (крипто, линейная алгебра, буферы протоколов), а не как способ "быть модным".Iterator/associated type действительно не выражает то, что нужно (заимствование, зависящее от вызова); в 90% случаев обычных трейтов с lifetime-параметром у самого трейта достаточно.Чтобы оставаться в курсе развития языка без постоянного чтения RFC целиком, полезны: еженедельная рассылка/сайт This Week in Rust (сводка изменений, обсуждений, новых крейтов), rustc release notes при каждом обновлении тулчейна, блог самой команды языка (blog.rust-lang.org) для крупных анонсов, и сам поиск по tracking issues в rust-lang/rust, если конкретная фича интересна практически.
Курс прошёл путь от владения и заимствований до const generics, GATs и границ стабильного языка — от основ, которые пишутся каждый день, до экспериментальных возможностей, которые определяют, каким Rust станет через несколько лет. Ключевой навык senior+ инженера — не знание каждой фичи наизусть, а умение быстро оценить: решает ли конкретная продвинутая возможность реальную проблему в задаче, или её сложность не окупается.
Дальнейшее развитие имеет смысл фокусировать на выбранной нише, а не на попытке знать "весь Rust одинаково глубоко":
wasm32-unknown-unknown, оптимизация размера бинарника, интеграция с JS/браузером, компонентная модель WASI.rust-lang/rust или std: там же, где рождаются const generics и GATs, которые вы только что изучили.