метапрограммирование на уровне ASTAST-level metaprogramming

МакросыMacros

macro_rules! и derive-макросы работают с синтаксическим деревом на этапе компиляции, до типовой проверки — в отличие от текстовой подстановки препроцессора C. vec! и println! — это обычные macro_rules!, а не встроенная магия компилятора.macro_rules! and derive macros operate on the syntax tree at compile time, before type-checking — unlike C's textual preprocessor substitution. vec! and println! are ordinary macro_rules! macros, not compiler built-ins.

системный уровень и метапрограммированиеsystems level and metaprogramming частый вопрос: чем derive отличается от обычной реализации трейта вручнуюcommon question: how derive differs from a hand-written trait impl

Что этоWhat it is

У Rust две основные системы макросов, обе работающие на СИНТАКСИЧЕСКОМ ДЕРЕВЕ (в отличие от текстовых, token-substitution макросов препроцессора C) на этапе компиляции, до обычной типовой проверки: декларативные макросы (macro_rules!), определяемые через сопоставление с образцом на входных токен-деревьях, подставляемых в шаблон — например, vec![1, 2, 3] и println!("{}", x) стандартной библиотеки сами по себе являются обычными macro_rules! макросами, а не встроенными в компилятор конструкциями; и процедурные макросы, самая распространённая разновидность которых — derive-макрос (#[derive(Debug, Clone, PartialEq)]) — функция, выполняемая на этапе компиляции, получающая синтаксическое дерево аннотированной структуры/enum'а на входе и генерирующая дополнительный Rust-код (обычно реализацию трейта), который компилируется вместе с исходным определением.Rust has two main macro systems, both operating on the SYNTAX TREE (unlike C's textual, token-substitution preprocessor macros) at compile time, before normal type-checking: declarative macros (macro_rules!), defined via pattern-matching on token trees fed into a template — e.g. the standard library's own vec![1, 2, 3] and println!("{}", x) are themselves ordinary macro_rules! macros, not compiler built-ins; and procedural macros, of which the most common variety by far is a derive macro (#[derive(Debug, Clone, PartialEq)]) — a function that runs at compile time, receives the annotated struct/enum's syntax tree as input, and generates additional Rust code (typically a trait implementation) that gets compiled alongside the original definition.

Зачем ввелиWhy it was introduced

01

Синтаксически особый, переменный по числу аргументов кодSyntactically special, variable-arity code

macro_rules! решает задачу написания кода, действительно повторяющегося на СИНТАКСИЧЕСКОМ уровне, но невыразимого обычной обобщённой функцией, потому что нужно принимать переменное число аргументов разнородных типов со специальным синтаксисом (как скобочно-запятой синтаксис vec![1, 2, 3]) — обычная функция фундаментально не может иметь переменной длины, синтаксически особый список параметров так, как это может макрос, поскольку макрос работает до/вне обычной типизированной машинерии вызова функций.macro_rules! solves the problem of writing code that's genuinely repetitive at the syntactic level but can't be expressed as an ordinary generic function because it needs to accept a variable number of arguments of heterogeneous types with special syntax (like vec!'s bracket-and-comma syntax) — a generic function fundamentally cannot have a variable-length, syntactically-special parameter list the way a macro can, since a macro operates before/outside the normal type-checked function-call machinery.

02

Механическая, но объёмная boilerplate-задача трейтовThe mechanical but voluminous trait-boilerplate problem

Derive-макросы решают другую, чрезвычайно распространённую задачу: реализация простых, механических, чисто структурных реализаций трейтов (Debug печатает каждое поле, Clone клонирует каждое поле, PartialEq сравнивает каждое поле) вручную для каждой структуры/enum'а в кодовой базе была бы огромным, подверженным ошибкам boilerplate'ом.Derive macros solve a different, extremely common problem: implementing simple, mechanical, purely-structural trait implementations (Debug printing every field, Clone cloning every field, PartialEq comparing every field) by hand for every struct/enum in a codebase would be enormous, error-prone boilerplate.

03

Концептуальная параллель с = default в C++A conceptual parallel to = default in C++

#[derive(...)] генерирует такой код автоматически и корректно — концептуально похоже на (и значительно более общо, чем) компилятором генерируемые специальные функции-члены в C++ (конструктор по умолчанию, операторы сравнения через = default), только применимо к куда более широкому набору трейтов.#[derive(...)] generates this mechanical code automatically and correctly — conceptually similar to (and considerably more general than) C++'s compiler-generated default constructors/comparison operators via = default, just applicable to a much broader set of traits.

Как это работает под капотомHow it works under the hood

Упрощённое определение macro_rules! сопоставляет собственный синтаксис вызова макроса с образцом:A simplified macro_rules! definition matches the macro's own call syntax against a pattern:

macro_rules! max {
    ($a:expr, $b:expr) => {
        if $a > $b { $a } else { $b }
    };
}

let m = max!(3, 7);  // раскрывается в if 3 > 7 { 3 } else { 7 }// expands to if 3 > 7 { 3 } else { 7 }

#[derive(Debug, Clone, PartialEq)] на простой структуре концептуально раскрывается в написанные вручную реализации — impl Debug for Point, делающую ровно ту же пофлдовую работу:#[derive(Debug, Clone, PartialEq)] on a simple struct conceptually expands to hand-written implementations — an impl Debug for Point doing the equivalent field-by-field work:

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

// derive(Debug) концептуально генерирует что-то вроде:// derive(Debug) conceptually generates something like:
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, существуют и полноценные процедурные макросы — attribute-подобные (#[my_macro]) и function-подобные (my_macro!(...), определяемые как настоящие Rust-функции, манипулирующие потоками токенов, а не паттернами macro_rules!) — более продвинутая тема, требующая отдельного крейта; здесь она лишь упоминается для полноты, основной фокус этой страницы — macro_rules! и derive.Beyond derive, fully custom procedural macros also exist — attribute-like (#[my_macro]) and function-like (my_macro!(...), defined as actual Rust functions manipulating token streams, not macro_rules! patterns) — a more advanced topic requiring a separate crate; it's only name-dropped here for completeness, this page's main focus is macro_rules! and derive.

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

vec! и println! — обычные macro_rules!, а не магия компилятораvec! and println! — ordinary macro_rules!, not compiler magic

Оба — реальные macro_rules! макросы стандартной библиотеки; вариадический синтаксис vec! с квадратными скобками концептуально определяется через паттерн повторения.Both are real standard-library macro_rules! macros; vec!'s variadic bracket syntax is conceptually defined via a repetition pattern.

macro_rules! my_vec {
    ($($x:expr),*) => {
        {
            let mut v = Vec::new();
            $(v.push($x);)*
            v
        }
    };
}
// my_vec![1, 2, 3] раскрывается в последовательность push()// my_vec![1, 2, 3] expands into a sequence of push() calls
02

#[derive(Debug)] против ручной реализации Debug#[derive(Debug)] vs. a hand-written Debug impl

derive(Debug) для быстрой диагностической печати во время разработки (println!("{:?}", my_struct)) экономит именно тот boilerplate, который пришлось бы писать вручную для каждого поля.derive(Debug) for quick diagnostic printing during development (println!("{:?}", my_struct)) saves exactly the boilerplate that would otherwise need to be hand-written for every field.

#[derive(Debug)]
struct Request { url: String, timeout_ms: u32 }

let r = Request { url: String::from("http://x"), timeout_ms: 500 };
println!("{:?}", r);  // Request { url: "http://x", timeout_ms: 500 } — без единой строчки вручную// Request { url: "http://x", timeout_ms: 500 } — with zero lines written by hand
03

#[derive(PartialEq, Eq, Hash)] для ключа HashMap#[derive(PartialEq, Eq, Hash)] for a HashMap key

Позволяет использовать пользовательскую структуру как ключ HashMap без единой строчки ручной реализации трейтов — опирается на trait bound машинерию, описанную в generics.html.Enables a custom struct to be used as a HashMap key with zero manual trait-implementation code — relying on the trait-bound machinery described in generics.html.

#[derive(PartialEq, Eq, Hash)]
struct UserId(u64);

let mut map: HashMap<UserId, String> = HashMap::new();
map.insert(UserId(1), String::from("alice"));  // требует Eq + Hash, оба получены через derive// requires Eq + Hash, both obtained via derive
04

Пользовательский macro_rules! с повторением аргументовA custom macro_rules! with argument repetition

Макрос hashmap!{ "a" => 1, "b" => 2 }, имитирующий синтаксис map-литерала, которого нет в самом языке, — показывает паттерн повторения $($key:expr => $value:expr),* и его раскрытие.A hashmap!{ "a" => 1, "b" => 2 } macro mimicking map-literal syntax the language itself doesn't have built in — showing the repetition pattern $($key:expr => $value:expr),* and its expansion.

macro_rules! hashmap {
    ($($key:expr => $value:expr),* $(,)?) => {
        {
            let mut m = HashMap::new();
            $(m.insert($key, $value);)*
            m
        }
    };
}
let m = hashmap!{ "a" => 1, "b" => 2 };

ГраблиGotchas

macro_rules! — сопоставление с образцом, но своя маленькая подъязыковая сложностьmacro_rules! is pattern matching, but its own small sub-language complexity

macro_rules! сопоставляются через сопоставление с образцом на токен-деревьях, а не через текстовую подстановку общего назначения — это делает их значительно безопаснее макросов препроцессора C (аргументы захватываются целиком как синтаксические фрагменты вроде $x:expr, а не вставляются как сырой текст, поэтому нет классических багов вроде #define SQUARE(x) x*x, ломающегося на SQUARE(a+b)), но написание действительно сложных macro_rules! (с множеством форм вызова, рекурсивным раскрытием) может стать собственным неуклюжим подъязыком со своими, часто менее ясными, чем у обычного Rust, сообщениями об ошибках.macro_rules! macros are matched via pattern matching on token trees, not a general-purpose text substitution — this makes them considerably safer than C preprocessor macros (arguments are captured as whole syntax fragments like $x:expr, not spliced in as raw text, so there's no classic bug like #define SQUARE(x) x*x breaking on SQUARE(a+b)) but also means writing genuinely complex macro_rules! macros can become its own small, awkward sub-language, with error messages that are often less clear than ordinary Rust compile errors.

Derive не может выразить произвольную логику по полюDerive can't express arbitrary per-field custom logic

Derive-макросы генерируют код только по СТРУКТУРНОЙ форме типа (имена полей, типы, варианты enum'а) — они не могут выразить произвольную кастомную логику по полю, как это может ручная реализация трейта (например, #[derive(Debug)] всегда печатает все поля в фиксированном механическом формате; чтобы скрыть одно поле из Debug-вывода, нужно писать impl Debug вручную или использовать атрибут уровня поля, если конкретный derive-макрос его поддерживает, как #[serde(skip)] — но это уже тема ecosystem/, не самого языка).Derive macros can only generate code based on the structural shape of the type they're attached to (field names, types, and enum variants) — they cannot express arbitrary custom logic per-field the way a hand-written trait implementation could (e.g. #[derive(Debug)] will always print every field in a fixed, mechanical format; to hide one field from Debug output you need to hand-write impl Debug yourself, or reach for a field-level attribute if the specific derive macro supports one, like serde's #[serde(skip)] — but that's an ecosystem/ topic, not the language itself).

Ошибки типов в раскрытом коде трудно отследить до места вызова макросаType errors in expanded code are hard to trace back to the macro call

Раскрытие макроса происходит полностью до типовой проверки, а значит макрос может сгенерировать синтаксически корректный, но приводящий к запутанному сообщению об ошибке типов код, указывающий на РАСКРЫТЫЙ код (который пользователь макроса никогда напрямую не писал), а не ясно на место вызова макроса — отладка таких ошибок типов, особенно для сторонних процедурных макросов, чьих внутренностей читатель не видел, — заметно более сложная категория Rust-ошибок, чем обычно.Macro expansion happens fully before type-checking, meaning a macro can generate code that's syntactically well-formed but produces a confusing type-error message that points at the expanded code (which the macro's user never wrote directly) rather than clearly at the macro invocation site — debugging macro-related type errors is a genuinely harder-than-average category of Rust error message to interpret, especially for third-party procedural macros the reader hasn't seen the internals of.