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

13. Макросы: код, который пишет код 🟡

Что вы узнаете:

  • Декларативные макросы (macro_rules!) с сопоставлением с образцом и повторениями
  • Когда макросы подходят лучше обобщений и трейтов
  • Процедурные макросы: derive-, атрибутные и функциональные
  • Написание собственного derive-макроса с syn и quote

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

Макросы сопоставляют образцы с синтаксисом и раскрываются в код на этапе компиляции:

#![allow(unused)]
fn main() {
// Простой макрос, который создаёт HashMap
macro_rules! hashmap {
    // Сопоставление: пары «ключ => значение», разделённые запятыми
    ( $( $key:expr => $value:expr ),* $(,)? ) => {
        {
            let mut map = std::collections::HashMap::new();
            $( map.insert($key, $value); )*
            map
        }
    };
}

let scores = hashmap! {
    "Alice" => 95,
    "Bob" => 87,
    "Carol" => 92,
};
// Раскрывается в:
// let mut map = HashMap::new();
// map.insert("Alice", 95);
// map.insert("Bob", 87);
// map.insert("Carol", 92);
// map
}

Типы фрагментов в макросах:

ФрагментЧто сопоставляетПример
$x:exprЛюбое выражение42, a + b, foo()
$x:tyТипi32, Vec<String>
$x:identИдентификаторmy_var, Config
$x:patОбразецSome(x), _
$x:stmtИнструкциюlet x = 5;
$x:ttОдно дерево токеновЧто угодно (самый гибкий)
$x:literalЛитерал42, "hello", true

Повторения: $( ... ),* означает «ноль или больше, разделённых запятыми»

#![allow(unused)]
fn main() {
// Автоматически генерируем тестовые функции
macro_rules! test_cases {
    ( $( $name:ident: $input:expr => $expected:expr ),* $(,)? ) => {
        $(
            #[test]
            fn $name() {
                assert_eq!(process($input), $expected);
            }
        )*
    };
}

test_cases! {
    test_empty: "" => "",
    test_hello: "hello" => "HELLO",
    test_trim: "  spaces  " => "SPACES",
}
// Генерирует три отдельные функции #[test]
}

Когда (не) стоит использовать макросы

Используйте макросы, когда:

  • Нужно убрать шаблонный код, с которым не справляются трейты и обобщения (вариативные аргументы, генерация тестов без повторов, DRY)
  • Создаёте предметно-ориентированный язык (DSL), например html!, sql!, vec!
  • Нужна условная генерация кода (cfg!, compile_error!)

Не используйте макросы, когда:

  • Подойдёт функция или обобщение (макросы труднее отлаживать, и автодополнение в них не помогает)
  • Внутри макроса нужна проверка типов (макросы работают с токенами, а не с типами)
  • Шаблон используется один-два раза (абстракция не стоит того)
#![allow(unused)]
fn main() {
// ❌ Лишний макрос: функция справляется
macro_rules! double {
    ($x:expr) => { $x * 2 };
}

// ✅ Просто используйте функцию:
fn double(x: i32) -> i32 { x * 2 }

// ✅ Хорошее применение макроса: вариативные аргументы, не может быть функцией:
macro_rules! println {
    ($($arg:tt)*) => { /* строка формата + аргументы */ };
}
}

Обзор процедурных макросов

Процедурные макросы это функции на Rust, которые преобразуют потоки токенов. Они требуют отдельного крейта с proc-macro = true:

#![allow(unused)]
fn main() {
// Три вида процедурных макросов:

// 1. Derive-макросы — #[derive(MyTrait)]
// Генерируют реализации трейтов по определениям структур
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Config {
    name: String,
    port: u16,
}

// 2. Атрибутные макросы — #[my_attribute]
// Преобразуют помеченный элемент
#[route(GET, "/api/users")]
async fn list_users() -> Json<Vec<User>> { /* ... */ }

// 3. Функциональные макросы — my_macro!(...)
// Пользовательский синтаксис
let query = sql!(SELECT * FROM users WHERE id = ?);
}

Derive-макросы на практике

Это самый распространённый вид процедурных макросов. Вот как концептуально работает #[derive(Debug)]:

#![allow(unused)]
fn main() {
// Вход (ваша структура):
#[derive(Debug)]
struct Point {
    x: f64,
    y: f64,
}

// Derive-макрос генерирует:
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-макросы:

DeriveКрейтЧто генерирует
DebugstdРеализацию fmt::Debug (отладочный вывод)
Clone, CopystdДублирование значений
PartialEq, EqstdСравнение на равенство
HashstdХеширование для ключей HashMap
Serialize, DeserializeserdeКодирование в JSON, YAML и др.
Errorthiserrorstd::error::Error и Display
ParserclapРазбор аргументов командной строки
Builderderive_builderПаттерн builder

Практический совет: используйте derive-макросы без колебаний: они убирают подверженный ошибкам шаблонный код. Написание собственных процедурных макросов относится к продвинутым темам, поэтому сначала используйте готовые (serde, thiserror, clap).

Гигиена макросов и $crate

Гигиена означает, что идентификаторы, созданные внутри макроса, не конфликтуют с идентификаторами в области видимости вызывающего кода. macro_rules! в Rust частично гигиеничен:

macro_rules! make_var {
    () => {
        let x = 42; // Этот 'x' находится в области видимости МАКРОСА
    };
}

fn main() {
    let x = 10;
    make_var!();   // Создаёт другой 'x' (гигиенично)
    println!("{x}"); // Выведет 10, а не 42: x из макроса не просачивается наружу
}

$crate: при написании макросов в библиотеке используйте $crate, чтобы ссылаться на собственный крейт. Он корректно разрешается независимо от того, как пользователи импортируют ваш крейт:

#![allow(unused)]
fn main() {
// В крейте my_diagnostics:

pub fn log_result(msg: &str) {
    println!("[diag] {msg}");
}

#[macro_export]
macro_rules! diag_log {
    ($($arg:tt)*) => {
        // ✅ $crate всегда ссылается на my_diagnostics, даже если пользователь
        // переименовал крейт в своём Cargo.toml
        $crate::log_result(&format!($($arg)*))
    };
}

// ❌ Без $crate:
// my_diagnostics::log_result(...)  ← сломается, если пользователь напишет:
//   [dependencies]
//   diag = { package = "my_diagnostics", version = "1" }
}

Правило: всегда используйте $crate:: в макросах с #[macro_export]. Никогда не используйте имя своего крейта напрямую.

Рекурсивные макросы и tt munching

Рекурсивные макросы обрабатывают вход по одному токену за раз. Этот приём называется tt munching (token-tree munching, «поедание» деревьев токенов):

// Считаем, сколько выражений передано в макрос
macro_rules! count {
    // Базовый случай: токенов не осталось
    () => { 0usize };
    // Рекурсивный случай: забираем одно выражение и считаем остальные
    ($head:expr $(, $tail:expr)* $(,)?) => {
        1usize + count!($($tail),*)
    };
}

fn main() {
    let n = count!("a", "b", "c", "d");
    assert_eq!(n, 4);

    // Работает и на этапе компиляции:
    const N: usize = count!(1, 2, 3);
    assert_eq!(N, 3);
}
#![allow(unused)]
fn main() {
// Строим гетерогенный кортеж из списка выражений:
macro_rules! tuple_from {
    // Базовый случай: один элемент
    ($single:expr $(,)?) => { ($single,) };
    // Рекурсивный случай: первый элемент и остальные
    ($head:expr, $($tail:expr),+ $(,)?) => {
        ($head, tuple_from!($($tail),+))
    };
}

let t = tuple_from!(1, "hello", 3.14, true);
// Раскрывается в: (1, ("hello", (3.14, (true,))))
}

Тонкости фрагментов:

ФрагментПодвох
$x:exprЖадный разбор: 1 + 2 это ОДНО выражение, а не три токена
$x:tyЖадный разбор: Vec<String> это один тип; за ним нельзя ставить + или <
$x:ttСопоставляет ровно ОДНО дерево токенов: самый гибкий, но меньше всего проверяется
$x:identТолько простые идентификаторы, а не пути вроде std::io
$x:patВ Rust 2021 сопоставляет шаблоны A | B; для одиночных образцов используйте $x:pat_param

Когда использовать tt: когда нужно передать токены другому макросу, не ограничивая их парсером. Шаблон $($args:tt)* означает «принять всё» (его используют println!, format!, vec!).

Пишем derive-макрос с syn и quote

Derive-макросы живут в отдельном крейте (proc-macro = true) и преобразуют поток токенов с помощью syn (разбор Rust) и quote (генерация Rust):

# my_derive/Cargo.toml
[lib]
proc-macro = true

[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
#![allow(unused)]
fn main() {
// my_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

/// Derive-макрос, который генерирует метод `describe()`,
/// возвращающий имя структуры и имена её полей.
#[proc_macro_derive(Describe)]
pub fn derive_describe(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    let name_str = name.to_string();

    // Извлекаем имена полей (только для структур с именованными полями)
    let fields = match &input.data {
        syn::Data::Struct(data) => {
            data.fields.iter()
                .filter_map(|f| f.ident.as_ref())
                .map(|id| id.to_string())
                .collect::<Vec<_>>()
        }
        _ => vec![],
    };

    let field_list = fields.join(", ");

    let expanded = quote! {
        impl #name {
            pub fn describe() -> String {
                format!("{} {{ {} }}", #name_str, #field_list)
            }
        }
    };

    TokenStream::from(expanded)
}
}
// В приложении:
use my_derive::Describe;

#[derive(Describe)]
struct SensorReading {
    sensor_id: u16,
    value: f64,
    timestamp: u64,
}

fn main() {
    println!("{}", SensorReading::describe());
    // "SensorReading { sensor_id, value, timestamp }"
}

Рабочий процесс: TokenStream (сырые токены) → syn::parse (AST) → анализ и преобразование → quote! (генерация токенов) → TokenStream (обратно компилятору).

КрейтРольКлючевые типы
proc-macroИнтерфейс с компиляторомTokenStream
synРазбор исходного кода Rust в ASTDeriveInput, ItemFn, Type
quoteГенерация токенов Rust из шаблоновquote!{}, интерполяция #variable
proc-macro2Мост между syn/quote и proc-macroTokenStream, Span

Практический совет: прежде чем писать собственный макрос, изучите исходники простого derive-макроса, например thiserror или derive_more. Команда cargo expand (через cargo-expand) показывает, во что раскрывается любой макрос: это бесценно для отладки.

Ключевые выводы: макросы

  • macro_rules! для простой генерации кода; процедурные макросы (syn и quote) для сложных derive-макросов
  • По возможности предпочитайте обобщения и трейты макросам: макросы труднее отлаживать и поддерживать
  • $crate обеспечивает гигиену; tt munching даёт рекурсивное сопоставление с образцом

См. также: гл. 2 — Трейты о том, когда трейты и обобщения лучше макросов. гл. 14 — Тестирование о тестировании кода, сгенерированного макросами.

flowchart LR
    A["Исходный код"] --> B["macro_rules!<br>сопоставление с образцом"]
    A --> C["#[derive(MyMacro)]<br>процедурный макрос"]

    B --> D["Раскрытие токенов"]
    C --> E["syn: разбор AST"]
    E --> F["Преобразование"]
    F --> G["quote!: генерация токенов"]
    G --> D

    D --> H["Скомпилированный код"]

    style A fill:#e8f4f8,stroke:#2980b9,color:#000
    style B fill:#d4efdf,stroke:#27ae60,color:#000
    style C fill:#fdebd0,stroke:#e67e22,color:#000
    style D fill:#fef9e7,stroke:#f1c40f,color:#000
    style E fill:#fdebd0,stroke:#e67e22,color:#000
    style F fill:#fdebd0,stroke:#e67e22,color:#000
    style G fill:#fdebd0,stroke:#e67e22,color:#000
    style H fill:#d4efdf,stroke:#27ae60,color:#000

Упражнение: декларативный макрос map! ★ (~15 минут)

Напишите макрос map!, который создаёт HashMap из пар ключ-значение:

let m = map! {
    "host" => "localhost",
    "port" => "8080",
};
assert_eq!(m.get("host"), Some(&"localhost"));

Требования: поддержите завершающую запятую и пустой вызов map!{}.

🔑 Решение
macro_rules! map {
    () => { std::collections::HashMap::new() };
    ( $( $key:expr => $val:expr ),+ $(,)? ) => {{
        let mut m = std::collections::HashMap::new();
        $( m.insert($key, $val); )+
        m
    }};
}

fn main() {
    let config = map! {
        "host" => "localhost",
        "port" => "8080",
        "timeout" => "30",
    };
    assert_eq!(config.len(), 3);
    assert_eq!(config["host"], "localhost");

    let empty: std::collections::HashMap<String, String> = map!();
    assert!(empty.is_empty());

    let scores = map! { 1 => 100, 2 => 200 };
    assert_eq!(scores[&1], 100);
}