# Декларативные макросы macro_rules! в Rust: от шаблонов vec! до собственных DSL

> Изучаем декларативные макросы macro_rules! в Rust: синтаксическое дерево (AST), гигиена макросов, спецификаторы типов, повторения и создание удобных DSL.

- **URL:** https://eagle2000.space/rust/rust-declarative-macros/
- **Date:** 2026-08-08

- **Tags:** rust, dev, macros, macro_rules, metaprogramming, dsl

---
## Введение

Метапрограммирование — это способность программы писать или генерировать другой код во время компиляции. Если в языках C/C++ макросы осуществляют простую препроцессорную замену текста (что часто приводит к неуловимым багам), то макросы в Rust работают на уровне абстрактного синтаксического дерева (**AST — Abstract Syntax Tree**).

В Rust есть две категории макросов: декларативные (`macro_rules!`) и процедурные.

В этой статье мы подробно изучим **декларативные макросы `macro_rules!`**. Вы узнаете, как устроен паттерн-матчинг по фрагментам кода, что такое гигиена макросов (Macro Hygiene), как конструировать удобные макросы инициализации коллекций (`my_vec!`, `map!`), замерять время выполнения кода (`time_it!`) и писать собственные предметно-ориентированные языки (DSL).

---

## 1. Гигиена макросов (Macro Hygiene)

Самый частый страх разработчиков при работе с макросами — это случайная подмена имён переменных (Variable Shadowing). 

Посмотрим на пример из языка C:
```c
#define SWAP(a, b) int tmp = a; a = b; b = tmp;

int main() {
    int tmp = 5, y = 10;
    SWAP(tmp, y); // Катастрофа! Внутренняя переменная tmp перекроет аргумент tmp!
}
```

В Rust такие ошибки **невозможны благодаря Гигиене макросов (Hygiene)**. 

Каждому идентификатору, создаваемому внутри макроса, компилятор присваивает уникальную метку контекста синтаксического дерева. Переменная `let temp = ...`, объявленная в макросе, никогда не столкнется с переменной `temp` в коде вызова.

---

## 2. Анатомия макроса: Сопоставления и спецификаторы

Макрос `macro_rules!` очень похож на обычную конструкцию `match`, но вместо сопоставления значений сопоставляются **шаблоны синтаксических токенов**:

```rust
macro_rules! my_macro {
    ( $pattern:designator ) => {
        // Код, который будет сгенерирован
    };
}
```

### Основные спецификаторы фрагментов (`Designators`)

| Спецификатор | Что принимает | Пример |
| :--- | :--- | :--- |
| **`expr`** | Выражение (Expression), вычисляемое в значение | `2 + 2`, `x.len()` |
| **`ident`** | Идентификатор (имя переменной, функции или структуры) | `my_var`, `User` |
| **`ty`** | Тип данных | `i32`, `Vec<String>` |
| **`stmt`** | Инструкция (Statement) | `let x = 5;` |
| **`pat`** | Шаблон сопоставления | `Some(x)`, `(a, b)` |
| **`block`** | Блок кода в фигурных скобках | `{ let a = 1; a + 2 }` |
| **`tt`** | Один произвольный токен дерева | Любая лексема |

Попробуйте запустить примеры определения базового синтаксиса:


### 💻 Основы синтаксиса и спецификаторы


#### Шаг 1

```rust
// Шаг 1: Инициализатор вектора my_vec!
macro_rules! my_vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

fn main() {
    let numbers = my_vec![10, 20, 30, 40];
    println!("Вектор из макроса: {:?}", numbers);
}
```

**Базовый макрос инициализации**
- Спецификатор `$x:expr` сопоставляется с любым выражением (expression).
- Конструкция `$( ... ),*` разворачивает аргументы через запятую.


#### Шаг 2

```rust
// Шаг 2: Использование различных спецификаторов ($i:ident, $t:ty, $e:expr)
macro_rules! make_struct {
    ( $name:ident, $field:ident: $type:ty ) => {
        struct $name {
            $field: $type,
        }
    };
}

make_struct!(User, age: u32);

fn main() {
    let u = User { age: 25 };
    println!("Создана структура с возрастом: {}", u.age);
}
```

**Спецификаторы типов токенов**
- `$name:ident` захватывает имя структуры или переменной (идентификатор).
- `$type:ty` захватывает тип данных Rust (`u32`, `String` и т.д.).


---

## 3. Практические утилиты: map!, time_it! и retry!

Для обработки списков переменной длины в макросах используются шаблоны повторений:

* `$( ... ),*` — повторение 0 или более раз через запятую.
* `$( ... );+` — повторение 1 или более раз через точку с запятой.
* `$( ... )?` — опциональный элемент (0 или 1 раз).

С помощью спецификаторов `:block` и `:expr` мы можем конструировать удобные служебные утилиты: макросы логирования, автоматического замеров времени вычислений или повторных попыток вызова нестабильных операций (`retry!`).

Изучите работу этих утилит в интерактивном Playground:


### 💻 Практика: map!, time_it! и retry!


#### Шаг 1

```rust
use std::collections::HashMap;

// Шаг 1: Макрос хеш-таблицы map!
macro_rules! map {
    ( $( $key:expr => $val:expr ),* $(,)? ) => {
        {
            let mut temp_map = HashMap::new();
            $(
                temp_map.insert($key, $val);
            )*
            temp_map
        }
    };
}

fn main() {
    let config = map! {
        "host" => "localhost",
        "port" => "8080",
        "protocol" => "https",
    };

    println!("Конфигурация: {:?}", config);
}
```

**Макрос для HashMap с висячей запятой**
- Синтаксис `$key:expr => $val:expr` задает свой разделитель `=>`.
- `$(,)?` поддерживает опциональную запятую в конце списка элементов.


#### Шаг 2

```rust
use std::time::Instant;

// Шаг 2: Замер времени выполнения блока кода
macro_rules! time_it {
    ( $name:expr, $code:block ) => {
        {
            let start = Instant::now();
            let result = $code;
            let elapsed = start.elapsed();
            println!("[ТАЙМЕР] Блок '{}' выполнен за {:?}", $name, elapsed);
            result
        }
    };
}

fn main() {
    let sum = time_it!("Подсчет суммы", {
        (1..=50_000).sum::<u64>()
    });

    println!("Результат: {sum}");
}
```

**Измерение времени с помощью $code:block**
- Спецификатор `$code:block` принимает произвольный блок кода `{ ... }`.
- Макрос возвращает результат выполнения блока не нарушая логики.


#### Шаг 3

```rust
// Шаг 3: Макрос повторных попыток вызова retry!
macro_rules! retry {
    ( $attempts:expr, $code:expr ) => {
        {
            let mut result = None;
            for attempt in 1..=$attempts {
                let res = $code;
                if res.is_ok() {
                    result = Some(res);
                    break;
                }
                println!("Попытка {attempt} завершилась ошибкой, повторяем...");
            }
            result.unwrap_or_else(|| Err("Превышено количество попыток"))
        }
    };
}

fn main() {
    let mut attempts_counter = 0;
    
    let res: Result<&str, &str> = retry!(3, {
        attempts_counter += 1;
        if attempts_counter == 2 {
            Ok("Успешное соединение!")
        } else {
            Err("Ошибка сети")
        }
    });

    println!("Итоговое состояние: {:?}", res);
}
```

**Макрос автоматических повторов (retry!)**
- Позволяет оборачивать ненадежные вычисления в цикл с заданным количеством попыток.


---

## 4. Построение собственных DSL (Domain-Specific Languages)

Макросы в Rust идеально подходят для создания лаконичных предметно-ориентированных языков. 

Благодаря поддержке произвольных разделителей (например, двоеточия, стрелок или фигурных скобок), макрос может принимать шаблоны, похожие на HTML, JSON или конфигурационные файлы, и превращать их в оптимизированный Rust-код во время компиляции.

Запустите примеры генерации HTML и JSON разметки:


### 💻 Построение DSL: html! и json_lite!


#### Шаг 1

```rust
// Шаг 1: Макрос генерации HTML-разметки
macro_rules! html {
    ( h1 { $text:expr } ) => {
        format!("<h1>{}</h1>", $text)
    };
    ( p { $text:expr } ) => {
        format!("<p>{}</p>", $text)
    };
}

fn main() {
    let header = html!(h1 { "Заголовок статьи" });
    let paragraph = html!(p { "Текст первого параграфа." });

    println!("{header}");
    println!("{paragraph}");
}
```

**Предметно-ориентированный язык (DSL)**
- Макросы могут сопоставлять фиксированные ключевые слова (`h1`, `p`) с генерацией пользовательского кода.


#### Шаг 2

```rust
// Шаг 2: Макрос генерации мини-JSON строк json_lite!
macro_rules! json_lite {
    ( $( $key:literal : $val:expr ),* $(,)? ) => {
        {
            let mut pairs = Vec::new();
            $(
                pairs.push(format!("\"{}\": \"{}\"", $key, $val));
            )*
            format!("{{ {} }}", pairs.join(", "))
        }
    };
}

fn main() {
    let user_json = json_lite! {
        "name": "Алексей",
        "role": "Разработчик",
        "city": "Москва",
    };

    println!("Сгенерированный JSON: {user_json}");
}
```

**Генератор структуры JSON**
- Спецификатор `$key:literal` сопоставляется со строковыми или числовыми литералами.
- Макрос объединяет элементы через джойнер `, `.


---

## Проверь свои знания!

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


### 📝 Декларативные макросы в Rust


**1. В чем фундаментальное отличие макросов Rust (`macro_rules!`) от текстовых макросов C (`#define`)?**

- [ ] Макросы Rust оперируют токенами AST и соблюдают гигиену (Hygiene), предотвращая подмену переменных
- [ ] Макросы Rust исполняются во время выполнения программы
- [ ] Макросы C компилируются в отдельный бинарный файл

**2. Какой спецификатор фрагмента (`designator`) используется для сопоставления с выражением Rust (например, `x + 5`)?**

- [ ] $x:expr
- [ ] $x:ident
- [ ] $x:stmt

**3. Что означает шаблон повторения `$( ... ),*` в декларативных макросах?**

- [ ] Повторение фрагмента внутри скобок 0 или более раз, разделенного запятыми
- [ ] Строго однократная вставка выражений
- [ ] Указание на умножение элементов вектора

**4. Что обеспечивает Гигиена макросов (Macro Hygiene) в Rust?**

- [ ] Изоляцию имен переменных: локальные переменные внутри макроса не перекрывают внешние переменные с тем же именем
- [ ] Автоматическую очистку мусора во время работы программы
- [ ] Проверку форматирования исходного кода

