Перейти к основному содержимому
  1. Rust/

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

1171 слово·6 минут· loading · loading · · ·Rust-middle Черновик
О Rust - Эта статья часть цикла.
Статей прочитано 0/58
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
34 Декларативные макросы macro_rules! в Rust: от шаблонов vec! до собственных 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:

#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, но вместо сопоставления значений сопоставляются шаблоны синтаксических токенов:

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/2
 1// Шаг 1: Инициализатор вектора my_vec!
 2macro_rules! my_vec {
 3    ( $( $x:expr ),* ) => {
 4        {
 5            let mut temp_vec = Vec::new();
 6            $(
 7                temp_vec.push($x);
 8            )*
 9            temp_vec
10        }
11    };
12}
13
14fn main() {
15    let numbers = my_vec![10, 20, 30, 40];
16    println!("Вектор из макроса: {:?}", numbers);
17}
18

Базовый макрос инициализации

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

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

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

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

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

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

Практика: map!, time_it! и retry!
Шаг 1/3
 1use std::collections::HashMap;
 2
 3// Шаг 1: Макрос хеш-таблицы map!
 4macro_rules! map {
 5    ( $( $key:expr => $val:expr ),* $(,)? ) => {
 6        {
 7            let mut temp_map = HashMap::new();
 8            $(
 9                temp_map.insert($key, $val);
10            )*
11            temp_map
12        }
13    };
14}
15
16fn main() {
17    let config = map! {
18        "host" => "localhost",
19        "port" => "8080",
20        "protocol" => "https",
21    };
22
23    println!("Конфигурация: {:?}", config);
24}
25

Макрос для HashMap с висячей запятой

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

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

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

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

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

Построение DSL: html! и json_lite!
Шаг 1/2
 1// Шаг 1: Макрос генерации HTML-разметки
 2macro_rules! html {
 3    ( h1 { $text:expr } ) => {
 4        format!("<h1>{}</h1>", $text)
 5    };
 6    ( p { $text:expr } ) => {
 7        format!("<p>{}</p>", $text)
 8    };
 9}
10
11fn main() {
12    let header = html!(h1 { "Заголовок статьи" });
13    let paragraph = html!(p { "Текст первого параграфа." });
14
15    println!("{header}");
16    println!("{paragraph}");
17}
18

Предметно-ориентированный язык (DSL)

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

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

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

Статья прочитана
Пожалуйста, оцените насколько статья была вам полезна и понятна
Цикл статей
О Rust - Эта статья часть цикла.
Статей прочитано 0/58
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
34 Декларативные макросы macro_rules! в Rust: от шаблонов vec! до собственных DSL (текущая)
Не прочитана

Связанные статьи