Метапрограммирование — это способность программы писать или генерировать другой код во время компиляции. Если в языках C/C++ макросы осуществляют простую препроцессорную замену текста (что часто приводит к неуловимым багам), то макросы в Rust работают на уровне абстрактного синтаксического дерева (AST — Abstract Syntax Tree).
В Rust есть две категории макросов: декларативные (macro_rules!) и процедурные.
В этой статье мы подробно изучим декларативные макросы macro_rules!. Вы узнаете, как устроен паттерн-матчинг по фрагментам кода, что такое гигиена макросов (Macro Hygiene), как конструировать удобные макросы инициализации коллекций (my_vec!, map!), замерять время выполнения кода (time_it!) и писать собственные предметно-ориентированные языки (DSL).
Самый частый страх разработчиков при работе с макросами — это случайная подмена имён переменных (Variable Shadowing).
Посмотрим на пример из языка C:
#define SWAP(a, b) int tmp = a; a = b; b = tmp;
intmain(){inttmp=5,y=10;SWAP(tmp,y);// Катастрофа! Внутренняя переменная tmp перекроет аргумент tmp!
}
В Rust такие ошибки невозможны благодаря Гигиене макросов (Hygiene).
Каждому идентификатору, создаваемому внутри макроса, компилятор присваивает уникальную метку контекста синтаксического дерева. Переменная let temp = ..., объявленная в макросе, никогда не столкнется с переменной temp в коде вызова.
2. Анатомия макроса: Сопоставления и спецификаторы
#
Макрос macro_rules! очень похож на обычную конструкцию match, но вместо сопоставления значений сопоставляются шаблоны синтаксических токенов:
macro_rules!my_macro{($pattern:designator)=>{// Код, который будет сгенерирован
};}
Спецификатор $x:expr сопоставляется с любым выражением (expression).
Конструкция $( ... ),* разворачивает аргументы через запятую.
Результат выполнения:
1// Шаг 2: Использование различных спецификаторов ($i:ident, $t:ty, $e:expr)
2macro_rules!make_struct{ 3($name:ident,$field:ident: $type:ty)=>{ 4struct$name{ 5$field: $type, 6} 7}; 8} 910make_struct!(User,age: u32);1112fnmain(){13letu=User{age: 25};14println!("Создана структура с возрастом: {}",u.age);15}16
Спецификаторы типов токенов
$name:ident захватывает имя структуры или переменной (идентификатор).
$type:ty захватывает тип данных Rust (u32, String и т.д.).
Результат выполнения:
3. Практические утилиты: map!, time_it! и retry!
#
Для обработки списков переменной длины в макросах используются шаблоны повторений:
$( ... ),* — повторение 0 или более раз через запятую.
$( ... );+ — повторение 1 или более раз через точку с запятой.
$( ... )? — опциональный элемент (0 или 1 раз).
С помощью спецификаторов :block и :expr мы можем конструировать удобные служебные утилиты: макросы логирования, автоматического замеров времени вычислений или повторных попыток вызова нестабильных операций (retry!).
Изучите работу этих утилит в интерактивном Playground:
Позволяет оборачивать ненадежные вычисления в цикл с заданным количеством попыток.
Результат выполнения:
4. Построение собственных DSL (Domain-Specific Languages)
#
Макросы в Rust идеально подходят для создания лаконичных предметно-ориентированных языков.
Благодаря поддержке произвольных разделителей (например, двоеточия, стрелок или фигурных скобок), макрос может принимать шаблоны, похожие на HTML, JSON или конфигурационные файлы, и превращать их в оптимизированный Rust-код во время компиляции.