Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Макросы Rust: от препроцессора к метапрограммированию

Что вы узнаете: как работают макросы Rust, когда использовать их вместо функций или обобщений и как они заменяют препроцессор C/C++. К концу главы вы сможете писать собственные макросы macro_rules! и понимать, что скрывается за #[derive(Debug)].

Макросы — одна из первых вещей, с которой вы сталкиваетесь в Rust (println!("hello") в первой же строке), но одна из последних, что объясняют большинство курсов. Эта глава восполняет этот пробел.

Зачем нужны макросы

Функции и обобщения покрывают большую часть повторного использования кода в Rust. Макросы закрывают пробелы, до которых не дотягивается система типов:

ПотребностьФункция/обобщение?Макрос?Почему
Вычислить значение✅ fn max<T: Ord>(a: T, b: T) -> T—Это делает система типов
Принимать переменное число аргументов❌ В Rust нет вариативных функций✅ println!("{} {}", a, b)Макросы принимают любое число токенов
Генерировать однотипные блоки impl❌ Одними обобщениями не получится✅ macro_rules!Макросы генерируют код на этапе компиляции
Выполнять код на этапе компиляции❌ const fn ограничен✅ Процедурные макросыПолноценный код Rust выполняется на этапе компиляции
Условно включать код❌✅ #[cfg(...)]Макросы-атрибуты управляют компиляцией

Если вы пришли из C/C++, думайте о макросах как о единственной правильной замене препроцессора — с той разницей, что они работают с синтаксическим деревом, а не с сырым текстом, поэтому они гигиеничны (нет случайных коллизий имён) и учитывают типы.

Для разработчиков на C: макросы Rust полностью заменяют #define. Текстового препроцессора нет. Полное соответствие препроцессора и Rust см. в ch18.


Декларативные макросы с macro_rules!

Декларативные макросы (также называемые «макросами по примеру») — самая распространённая форма макросов в Rust. Они используют сопоставление с образцом по синтаксису, похожее на match по значениям.

Базовый синтаксис

macro_rules! say_hello {
    () => {
        println!("Hello!");
    };
}

fn main() {
    say_hello!();  // Раскрывается в: println!("Hello!");
}

! после имени — это то, что сообщает (и вам, и компилятору), что это вызов макроса.

Сопоставление с образцом по аргументам

Макросы сопоставляют деревья токенов с помощью спецификаторов фрагментов:

macro_rules! greet {
    // Образец 1: без аргументов
    () => {
        println!("Hello, world!");
    };
    // Образец 2: один аргумент-выражение
    ($name:expr) => {
        println!("Hello, {}!", $name);
    };
}

fn main() {
    greet!();           // "Hello, world!"
    greet!("Rust");     // "Hello, Rust!"
}

Справочник спецификаторов фрагментов

СпецификаторСоответствуетПример
$x:exprЛюбому выражению42, a + b, foo()
$x:tyТипуi32, Vec<String>, &str
$x:identИдентификаторуfoo, my_var
$x:patОбразцуSome(x), _, (a, b)
$x:stmtОператоруlet x = 5;
$x:blockБлоку{ println!("hi"); 42 }
$x:literalЛитералу42, "hello", true
$x:ttОдному дереву токеновЧто угодно — универсальный шаблон
$x:itemЭлементу (fn, struct, impl и т. д.)fn foo() {}

Повторение — ключевая возможность

Макросы C/C++ не умеют циклов. Макросы Rust умеют повторять образцы:

macro_rules! make_vec {
    // Сопоставляем ноль или более выражений, разделённых запятыми
    ( $( $element:expr ),* ) => {
        {
            let mut v = Vec::new();
            $( v.push($element); )*  // Повторяем для каждого сопоставленного элемента
            v
        }
    };
}

fn main() {
    let v = make_vec![1, 2, 3, 4, 5];
    println!("{v:?}");  // [1, 2, 3, 4, 5]
}

Синтаксис $( ... ),* означает «сопоставить ноль или более таких образцов, разделённых запятыми». Конструкция $( ... )* в раскрытии повторяет тело один раз для каждого совпадения.

Именно так реализован vec![] в стандартной библиотеке. Реальный исходный код:

#![allow(unused)]
fn main() {
macro_rules! vec {
    () => { Vec::new() };
    ($elem:expr; $n:expr) => { vec::from_elem($elem, $n) };
    ($($x:expr),+ $(,)?) => { <[_]>::into_vec(Box::new([$($x),+])) };
}
}

Конструкция $(,)? в конце разрешает необязательную завершающую запятую.

Операторы повторения

ОператорЗначениеПример
$( ... )*Ноль или болееvec![], vec![1], vec![1, 2, 3]
$( ... )+Один или болееТребуется хотя бы один элемент
$( ... )?Ноль или одинНеобязательный элемент

Практический пример: конструктор hashmap!

В стандартной библиотеке есть vec![], но нет hashmap!{}. Давайте создадим его:

macro_rules! hashmap {
    ( $( $key:expr => $value:expr ),* $(,)? ) => {
        {
            let mut map = std::collections::HashMap::new();
            $( map.insert($key, $value); )*
            map
        }
    };
}

fn main() {
    let scores = hashmap! {
        "Alice" => 95,
        "Bob" => 87,
        "Carol" => 92,  // Завершающая запятая допустима благодаря $(,)?
    };
    println!("{scores:?}");
}

Практический пример: макрос проверки диагностики

Шаблон, распространённый во встраиваемом и диагностическом коде, — проверить условие и вернуть ошибку:

#![allow(unused)]
fn main() {
use thiserror::Error;

#[derive(Error, Debug)]
enum DiagError {
    #[error("Check failed: {0}")]
    CheckFailed(String),
}

macro_rules! diag_check {
    ($cond:expr, $msg:expr) => {
        if !($cond) {
            return Err(DiagError::CheckFailed($msg.to_string()));
        }
    };
}

fn run_diagnostics(temp: f64, voltage: f64) -> Result<(), DiagError> {
    diag_check!(temp < 85.0, "GPU too hot");
    diag_check!(voltage > 0.8, "Rail voltage too low");
    diag_check!(voltage < 1.5, "Rail voltage too high");
    println!("All checks passed");
    Ok(())
}
}

Сравнение с C/C++:

// Препроцессор C — текстовая подстановка, без проверки типов, без гигиены
#define DIAG_CHECK(cond, msg) \
    do { if (!(cond)) { log_error(msg); return -1; } } while(0)

Версия на Rust возвращает корректный тип Result, не рискует двойным вычислением, а компилятор проверяет, что $cond действительно является выражением типа bool.

Гигиена: почему макросы Rust безопасны

Ошибки макросов C/C++ часто возникают из-за коллизий имён:

// C: опасно — `x` может затенить `x` вызывающего кода
#define SQUARE(x) ((x) * (x))
int x = 5;
int result = SQUARE(x++);  // Неопределённое поведение: x инкрементируется дважды!

Макросы Rust гигиеничны — переменные, созданные внутри макроса, не утекают наружу:

macro_rules! make_x {
    () => {
        let x = 42;  // Этот `x` ограничен раскрытием макроса
    };
}

fn main() {
    let x = 10;
    make_x!();
    println!("{x}");  // Выводит 10, а не 42 — гигиена предотвращает коллизию
}

Компилятор считает x макроса и x вызывающего кода разными переменными, даже несмотря на одинаковые имена. С препроцессором C это невозможно.


Распространённые макросы стандартной библиотеки

Вы используете их с первой главы — вот что они делают на самом деле:

МакросЧто делаетРаскрывается в (упрощённо)
println!("{}", x)Форматирует и выводит в stdout с переводом строкиstd::io::_print(format_args!(...))
eprintln!("{}", x)Выводит в stderr с переводом строкиТо же, но в stderr
format!("{}", x)Форматирует в StringВыделяет память и возвращает String
vec![1, 2, 3]Создаёт Vec с элементамиVec::from([1, 2, 3]) (примерно)
todo!()Отмечает незавершённый кодpanic!("not yet implemented")
unimplemented!()Отмечает намеренно не реализованный кодpanic!("not implemented")
unreachable!()Отмечает код, недостижимость которого компилятор не может доказатьpanic!("unreachable")
assert!(cond)Вызывает panic, если условие ложноif !cond { panic!(...) }
assert_eq!(a, b)Вызывает panic, если значения не равныПри ошибке показывает оба значения
dbg!(expr)Выводит выражение и его значение в stderr, возвращает значениеeprintln!("[file:line] expr = {:#?}", &expr); expr
include_str!("file.txt")Встраивает содержимое файла как &str на этапе компиляцииЧитает файл во время компиляции
include_bytes!("data.bin")Встраивает содержимое файла как &[u8] на этапе компиляцииЧитает файл во время компиляции
cfg!(condition)Условие времени компиляции как booltrue или false в зависимости от цели
env!("VAR")Читает переменную окружения на этапе компиляцииКомпиляция завершается ошибкой, если её нет
concat!("a", "b")Объединяет литералы на этапе компиляции"ab"

dbg! — макрос для отладки, которым вы будете пользоваться каждый день

fn factorial(n: u32) -> u32 {
    if dbg!(n <= 1) {     // Выводит: [src/main.rs:2] n <= 1 = false
        dbg!(1)           // Выводит: [src/main.rs:3] 1 = 1
    } else {
        dbg!(n * factorial(n - 1))  // Выводит промежуточные значения
    }
}

fn main() {
    dbg!(factorial(4));   // Выводит все рекурсивные вызовы с файлом и строкой
}

dbg! возвращает значение, которое оборачивает, поэтому его можно вставить куда угодно, не меняя поведение программы. Он выводит в stderr (а не в stdout), поэтому не мешает выводу программы. Перед коммитом удаляйте все вызовы dbg!.

Синтаксис строк формата

Поскольку println!, format!, eprintln! и write! используют один и тот же механизм форматирования, вот краткий справочник:

#![allow(unused)]
fn main() {
let name = "sensor";
let value = 3.14159;
let count = 42;

println!("{name}");                    // Переменная по имени (Rust 1.58+)
println!("{}", name);                  // Позиционно
println!("{value:.2}");                // 2 знака после запятой: "3.14"
println!("{count:>10}");               // Выравнивание по правому краю, ширина 10: "        42"
println!("{count:0>10}");              // Дополнение нулями: "0000000042"
println!("{count:#06x}");              // Шестнадцатеричный с префиксом: "0x002a"
println!("{count:#010b}");             // Двоичный с префиксом: "0b00101010"
println!("{value:?}");                 // Формат Debug
println!("{value:#?}");                // Формат Debug с красивым выводом
}

Для разработчиков на C: думайте об этом как о типобезопасном printf — компилятор проверяет, что {:.2} применяется к числу с плавающей точкой, а не к строке. Ошибок несоответствия %s/%d не бывает.

Для разработчиков на C++: это заменяет std::cout << std::fixed << std::setprecision(2) << value одной читаемой строкой формата.


Макросы derive

Вы видели #[derive(...)] почти у каждой структуры в этой книге:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}
}

#[derive(Debug)] — это макрос derive, особый вид процедурного макроса, который автоматически генерирует реализации трейтов. Вот что он создаёт (упрощённо):

#![allow(unused)]
fn main() {
// То, что #[derive(Debug)] генерирует для Point:
impl std::fmt::Debug for Point {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Point")
            .field("x", &self.x)
            .field("y", &self.y)
            .finish()
    }
}
}

Без #[derive(Debug)] вам пришлось бы писать такой блок impl вручную для каждой структуры.

Распространённые производные трейты

DeriveЧто генерируетКогда использовать
DebugФорматирование {:?}Почти всегда — позволяет выводить для отладки
CloneМетод .clone()Когда нужно дублировать значения
CopyНеявное копирование при присваиванииНебольшие типы только на стеке (целые, [f64; 3])
PartialEq / EqОператоры == и !=Когда нужно сравнение на равенство
PartialOrd / OrdОператоры <, >, <=, >=Когда нужен порядок
HashХеширование для ключей HashMap/HashSetТипы, используемые как ключи словаря
DefaultКонструктор Type::default()Типы с разумными нулевыми/пустыми значениями
serde::Serialize / DeserializeСериализация в JSON/TOML и т. д.Типы данных, пересекающие границы API

Дерево решений по derive

Стоит ли выводить этот трейт?
  │
  ├── Содержит ли мой тип только типы, которые реализуют трейт?
  │     ├── Да → #[derive] сработает
  │     └── Нет → Напишите реализацию вручную (или пропустите)
  │
  └── Будут ли пользователи моего типа разумно ожидать такого поведения?
        ├── Да → Выводите (Debug, Clone, PartialEq почти всегда разумны)
        └── Нет → Не выводите (например, не выводите Copy для типа с файловым дескриптором)

Сравнение с C++: #[derive(Clone)] похож на автоматическую генерацию корректного конструктора копирования. #[derive(PartialEq)] похож на автоматически сгенерированный operator==, который сравнивает каждое поле — то, что оператор «космического корабля» = default из C++20 наконец предоставляет.


Макросы-атрибуты

Макросы-атрибуты преобразуют элемент, к которому они прикреплены. Некоторые вы уже использовали:

#![allow(unused)]
fn main() {
#[test]                    // Помечает функцию как тест
fn test_addition() {
    assert_eq!(2 + 2, 4);
}

#[cfg(target_os = "linux")] // Условно включает эту функцию
fn linux_only() { /* ... */ }

#[derive(Debug)]            // Генерирует реализацию Debug
struct MyType { /* ... */ }

#[allow(dead_code)]         // Подавляет предупреждение компилятора
fn unused_helper() { /* ... */ }

#[must_use]                 // Предупреждает, если возвращаемое значение отброшено
fn compute_checksum(data: &[u8]) -> u32 { /* ... */ }
}

Распространённые встроенные атрибуты:

АтрибутНазначение
#[test]Пометить как тестовую функцию
#[cfg(...)]Условная компиляция
#[derive(...)]Автоматическая генерация реализаций трейтов
#[allow(...)] / #[deny(...)] / #[warn(...)]Управление уровнями линтера
#[must_use]Предупреждать о неиспользованных возвращаемых значениях
#[inline] / #[inline(always)]Подсказка компилятору встроить функцию
#[repr(C)]Использовать раскладку памяти, совместимую с C (для FFI)
#[no_mangle]Не искажать имя символа (для FFI)
#[deprecated]Пометить как устаревший с необязательным сообщением

Для разработчиков на C/C++: атрибуты заменяют смесь директив препроцессора (#pragma, __attribute__((...))) и расширений, специфичных для компилятора. Они входят в грамматику языка, а не являются приклеенными расширениями.


Процедурные макросы (концептуальный обзор)

Процедурные макросы («proc macros») — это макросы, написанные как отдельные программы на Rust, которые выполняются на этапе компиляции и генерируют код. Они мощнее, чем macro_rules!, но и сложнее.

Существует три вида:

ВидСинтаксисПримерЧто делает
Функциональныйmy_macro!(...)sql!(SELECT * FROM users)Разбирает пользовательский синтаксис, генерирует код Rust
Derive#[derive(MyTrait)]#[derive(Serialize)]Генерирует реализацию трейта по определению структуры
Атрибут#[my_attr]#[tokio::main], #[instrument]Преобразует помеченный элемент

Вы уже используете процедурные макросы

  • #[derive(Error)] из thiserror — генерирует реализации Display и From для перечислений ошибок
  • #[derive(Serialize, Deserialize)] из serde — генерирует код сериализации
  • #[tokio::main] — превращает async fn main() в настройку среды выполнения + block_on
  • #[test] — регистрируется тестовым харнессом (встроенный процедурный макрос)

Когда стоит писать собственный процедурный макрос

Скорее всего, в ходе этого курса писать процедурные макросы не понадобится. Они полезны, когда:

  • Нужно анализировать поля структур или варианты перечислений на этапе компиляции (макросы derive)
  • Вы создаёте предметно-ориентированный язык (функциональные макросы)
  • Нужно преобразовывать сигнатуры функций (макросы-атрибуты)

Для большинства кода достаточно macro_rules! или обычных функций.

Сравнение с C++: процедурные макросы выполняют ту роль, которую в C++ играют генераторы кода, метапрограммирование на шаблонах и внешние инструменты вроде protoc. Разница в том, что процедурные макросы — часть конвейера сборки cargo: никаких внешних шагов сборки, никаких пользовательских команд CMake.


Когда что использовать: макросы, функции или обобщения

Нужно генерировать код?
  │
  ├── Нет → Используйте функцию или обобщённую функцию
  │         (проще, лучшие сообщения об ошибках, поддержка IDE)
  │
  └── Да ─┬── Переменное число аргументов?
            │     └── Да → macro_rules! (например, println!, vec!)
            │
            ├── Однотипные блоки impl для многих типов?
            │     └── Да → macro_rules! с повторением
            │
            ├── Нужно анализировать поля структуры?
            │     └── Да → Макрос derive (процедурный макрос)
            │
            ├── Нужен собственный синтаксис (DSL)?
            │     └── Да → Функциональный процедурный макрос
            │
            └── Нужно преобразовать функцию/структуру?
                  └── Да → Процедурный макрос-атрибут

Общее правило: если можно обойтись функцией или обобщением, не используйте макрос. У макросов хуже сообщения об ошибках, нет автодополнения IDE внутри тела макроса, и их труднее отлаживать.


Упражнения

🟢 Упражнение 1: макрос min!

Напишите макрос min!, который:

  • min!(a, b) возвращает меньшее из двух значений
  • min!(a, b, c) возвращает наименьшее из трёх значений
  • Работает с любым типом, реализующим PartialOrd

Подсказка: в вашем macro_rules! понадобятся два варианта ветвления.

Решение (нажмите, чтобы развернуть)
macro_rules! min {
    ($a:expr, $b:expr) => {
        if $a < $b { $a } else { $b }
    };
    ($a:expr, $b:expr, $c:expr) => {
        min!(min!($a, $b), $c)
    };
}

fn main() {
    println!("{}", min!(3, 7));        // 3
    println!("{}", min!(9, 2, 5));     // 2
    println!("{}", min!(1.5, 0.3));    // 0.3
}

Примечание: для продакшн-кода лучше использовать std::cmp::min или a.min(b). Это упражнение демонстрирует устройство макросов с несколькими ветвями.

🟡 Упражнение 2: hashmap! с нуля

Не глядя на пример выше, напишите макрос hashmap!, который:

  • Создаёт HashMap из пар ключ => значение
  • Поддерживает завершающие запятые
  • Работает с любым хешируемым типом ключа

Проверьте на:

#![allow(unused)]
fn main() {
let m = hashmap! {
    "name" => "Alice",
    "role" => "Engineer",
};
assert_eq!(m["name"], "Alice");
assert_eq!(m.len(), 2);
}
Решение (нажмите, чтобы развернуть)
use std::collections::HashMap;

macro_rules! hashmap {
    ( $( $key:expr => $val:expr ),* $(,)? ) => {{
        let mut map = HashMap::new();
        $( map.insert($key, $val); )*
        map
    }};
}

fn main() {
    let m = hashmap! {
        "name" => "Alice",
        "role" => "Engineer",
    };
    assert_eq!(m["name"], "Alice");
    assert_eq!(m.len(), 2);
    println!("Tests passed!");
}

🟡 Упражнение 3: assert_approx_eq! для сравнения чисел с плавающей точкой

Напишите макрос assert_approx_eq!(a, b, epsilon), который вызывает panic, если |a - b| > epsilon. Он полезен для тестирования вычислений с плавающей точкой, где точное равенство не выполняется.

Проверьте на:

#![allow(unused)]
fn main() {
assert_approx_eq!(0.1 + 0.2, 0.3, 1e-10);        // Должно пройти
assert_approx_eq!(3.14159, std::f64::consts::PI, 1e-4); // Должно пройти
// assert_approx_eq!(1.0, 2.0, 0.5);              // Должно вызвать panic
}
Решение (нажмите, чтобы развернуть)
macro_rules! assert_approx_eq {
    ($a:expr, $b:expr, $eps:expr) => {
        let (a, b, eps) = ($a as f64, $b as f64, $eps as f64);
        let diff = (a - b).abs();
        if diff > eps {
            panic!(
                "assertion failed: |{} - {}| = {} > {} (epsilon)",
                a, b, diff, eps
            );
        }
    };
}

fn main() {
    assert_approx_eq!(0.1 + 0.2, 0.3, 1e-10);
    assert_approx_eq!(3.14159, std::f64::consts::PI, 1e-4);
    println!("All float comparisons passed!");
}

🔴 Упражнение 4: impl_display_for_enum!

Напишите макрос, который генерирует реализацию Display для простых перечислений в стиле C. Дано:

#![allow(unused)]
fn main() {
impl_display_for_enum! {
    enum Color {
        Red => "red",
        Green => "green",
        Blue => "blue",
    }
}
}

Он должен сгенерировать и определение enum Color { Red, Green, Blue }, И impl Display for Color, который сопоставляет каждый вариант с его строкой.

Подсказка: понадобятся и повторение $( ... ),*, и несколько спецификаторов фрагментов.

Решение (нажмите, чтобы развернуть)
use std::fmt;

macro_rules! impl_display_for_enum {
    (enum $name:ident { $( $variant:ident => $display:expr ),* $(,)? }) => {
        #[derive(Debug, Clone, Copy, PartialEq)]
        enum $name {
            $( $variant ),*
        }

        impl fmt::Display for $name {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                match self {
                    $( $name::$variant => write!(f, "{}", $display), )*
                }
            }
        }
    };
}

impl_display_for_enum! {
    enum Color {
        Red => "red",
        Green => "green",
        Blue => "blue",
    }
}

fn main() {
    let c = Color::Green;
    println!("Color: {c}");          // "Color: green"
    println!("Debug: {c:?}");        // "Debug: Green"
    assert_eq!(format!("{}", Color::Red), "red");
    println!("All tests passed!");
}