← Назад к списку тем

18. Макросы

macro_rules!, гигиена макросов, derive-макросы, атрибутные и function-like proc-macro, syn/quote.

Зачем в Rust макросы, если есть generics

Generics и трейты решают задачу полиморфизма по типам, но не умеют: принимать переменное число аргументов, генерировать код на этапе компиляции по структуре другого кода (например, автоматически реализовать трейт для структуры, зная список её полей), или работать с синтаксисом, которого ещё не существует в виде значения (SQL-подобный DSL, HTML-шаблоны). Макросы в Rust — это метапрограммирование на уровне AST, выполняемое до типовой проверки.

МеханизмКогда работаетЧто умеет
Generics / trait boundsкомпиляция, после парсингаполиморфизм по типам с проверкой границ
macro_rules!раннее расширение AST (expansion)сопоставление с образцом по токенам, переменное число аргументов
proc-macroкомпиляция крейта, до основной сборкипроизвольные преобразования TokenStream → TokenStream (полный Rust-код)
🔑 Ключевая идея: макросы работают с исходным кодом как с данными (токенами / синтаксическим деревом), а не со значениями во время выполнения — расширение макроса происходит полностью на этапе компиляции, рантайм-цена равна нулю.

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

macro_rules! определяет макрос через набор шаблонов (patterns) сопоставления токенов, похожих на ветки match, но работающих на уровне синтаксиса, а не значений.

macro_rules! my_vec {
    () => {
        Vec::new()
    };
    ($($x:expr),+ $(,)?) => {
        {
            let mut v = Vec::new();
            $( v.push($x); )+
            v
        }
    };
}

fn main() {
    let empty: Vec<i32> = my_vec!();
    let nums = my_vec![1, 2, 3,]; // trailing comma поддержан благодаря $(,)?
    println!("{nums:?}");
}

Основные fragment specifiers, определяющие тип "дырки" в шаблоне: expr (выражение), ident (идентификатор), ty (тип), pat (паттерн), block, stmt, tt (произвольное token tree — самый гибкий), literal, path, vis (видимость).

macro_rules! create_getter {
    ($field:ident : $ty:ty) => {
        pub fn $field(&self) -> &$ty {
            &self.$field
        }
    };
}

struct User { name: String, age: u32 }

impl User {
    create_getter!(name: String);
    create_getter!(age: u32);
}

Гигиена макросов (macro hygiene)

Rust-макросы гигиеничны: идентификаторы, введённые внутри макроса (например, временная переменная в теле my_vec!), не конфликтуют с одноимёнными идентификаторами в коде вызывающей стороны — они существуют как бы в отдельном "синтаксическом контексте". Это принципиально отличает Rust от текстовых макросов C/C++ препроцессора.

macro_rules! using_v {
    ($e:expr) => {
        {
            let v = 42; // эта `v` невидима снаружи макроса
            v + $e
        }
    };
}

fn main() {
    let v = 10;
    let result = using_v!(v); // `v` в аргументе — это внешняя v=10, а не внутренняя v=42
    assert_eq!(result, 52); // 42 (внутренняя) + 10 (внешняя v, подставленная как $e)
}

Гигиена избавляет от классической проблемы C-макросов — "захвата" переменной по имени, когда макрос случайно ссылается на переменную вызывающего кода или наоборот. В Rust такое возможно только явно, если вы намеренно передаёте идентификатор через $name:ident.

⚠️ Ограничение гигиены: гигиена в macro_rules! частичная — она защищает локальные переменные и метки, но не распространяется одинаково на все контексты (например, на видимость через self в некоторых случаях). Proc-macro на основе syn/quote дают более явный контроль через Span::call_site() и Span::mixed_site().

Три вида proc-macro

Процедурные макросы — это функции, которые получают TokenStream на вход и возвращают новый TokenStream, выполняясь как отдельный компилируемый плагин компилятора. Они должны быть объявлены в отдельном крейте с proc-macro = true в Cargo.toml. Существует три вида.

1. Derive-макросы

#[derive(Debug, Clone, MyTrait)]
struct Point { x: i32, y: i32 }

2. Атрибутные макросы

#[route(GET, "/users")]
fn list_users() -> impl Responder { /* ... */ }

#[tokio::main]
async fn main() { /* ... */ }

3. Function-like proc-macro

let query = sql!(SELECT id, name FROM users WHERE id = ?);
ВидВходТипичное применение
deriveструктура/enum целикомSerialize/Deserialize, Debug, builder-паттерн
attributeэлемент + сам атрибутроутинг веб-фреймворков, async runtime entrypoint
function-likeпроизвольные токены в скобкахDSL: SQL, HTML, regex, форматирование

syn и quote — инструментарий proc-macro

Писать proc-macro напрямую с TokenStream неудобно — токены нужно парсить в структуры и генерировать код обратно. Экосистема стандартизировалась вокруг двух крейтов: syn парсит токены в типизированное синтаксическое дерево Rust (структуры, поля, типы), а quote генерирует токены из шаблона, похожего на обычный Rust-код с интерполяцией через #var.

// в крейте с proc-macro = true
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields};

#[proc_macro_derive(Describe)]
pub fn derive_describe(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;

    let field_names: Vec<String> = match &input.data {
        Data::Struct(s) => match &s.fields {
            Fields::Named(named) => named.named.iter()
                .map(|f| f.ident.as_ref().unwrap().to_string())
                .collect(),
            _ => vec![],
        },
        _ => vec![],
    };

    let expanded = quote! {
        impl $name {
            pub fn describe() -> &'static [&'static str] {
                &[#(#field_names),*]
            }
        }
    };

    expanded.into()
}

Конструкция #(#field_names),* в quote! — это повторение (repetition), аналог $(...)* в macro_rules!, разворачивающее вектор строк в список токенов, разделённых запятыми.

Отладка макросов

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

$ cargo expand                  # печатает код после раскрытия всех макросов
$ cargo expand my_module::my_fn # раскрытие конкретного элемента

Внутри macro_rules! помогает атрибут для трассировки, а для proc-macro — вывод через eprintln!("{}", expanded) во время компиляции (в build-скрипте proc-macro крейта) или использование syn::Error::to_compile_error() для превращения внутренней ошибки парсинга в понятное сообщение компилятора, указывающее на нужный Span.

use syn::spanned::Spanned;

fn check_named_fields(fields: &syn::Fields) -> syn::Result<()> {
    match fields {
        syn::Fields::Named(_) => Ok(()),
        _ => Err(syn::Error::new(
            fields.span(),
            "Describe поддерживает только именованные поля",
        )),
    }
}
⚠️ Время компиляции: каждый proc-macro крейт компилируется отдельно и выполняется на этапе сборки зависимых крейтов — злоупотребление тяжёлыми derive-макросами (особенно с зависимостями от syn/quote/proc-macro2) заметно увеличивает время cargo build, особенно clean-сборки.

Когда использовать какой вид

  • macro_rules! — для повторяющегося boilerplate внутри одного крейта: обёртки над однотипными вызовами, генерация тестов по шаблону, DSL на уровне выражений. Быстро писать, не требует отдельного крейта.
  • derive proc-macro — когда нужно сгенерировать реализацию трейта на основе структуры данных: сериализация (serde), билдеры, конверсии, ORM-мэппинг полей.
  • attribute proc-macro — для изменения сигнатуры или окружения функции/модуля: async runtime entrypoints, веб-роутинг, трассировка (#[instrument] из tracing).
  • function-like proc-macro — для DSL, не укладывающегося в существующий синтаксис Rust: SQL-запросы с проверкой на этапе компиляции (sqlx::query!), HTML-шаблоны (html! в Yew).
// Пример из реальной практики: sqlx проверяет SQL на этапе компиляции,
// обращаясь к схеме БД через переменную окружения DATABASE_URL
let row = sqlx::query!("SELECT id, email FROM users WHERE id = $1", user_id)
    .fetch_one(&pool)
    .await?;
✅ Правило выбора: начинайте с macro_rules! — он проще, не требует отдельного крейта и достаточен для 90% случаев дублирования кода. Переходите на proc-macro только когда нужен полноценный доступ к синтаксическому дереву входных данных (derive) или произвольный DSL-синтаксис.