Macros

Макросы

Макросы — это как “шаблоны кода”, которые генерируют код за тебя. Они работают до того, как программа начнет компилироваться. Ты пишешь правило: “если встретишь такую конструкцию, разверни её в такой код”.

В Rust есть два основных типа макросов:

  • Объявительные макросы — самые простые, создаются через macro_rules!
  • Процедурные макросы — более мощные, пишутся как полноценный код на Rust.
// Создаем макрос, который выводит "Привет!" 2 раза
macro_rules! hello2 {
    () => {
        println!("Привет!");
        println!("Привет!");
    };
}

fn main() {
    // Используем макрос — он развернется в 2 строки кода
    hello2!();
}

Макросы с параметрами

В макросах macro_rules! параметры обозначаются знаком $ и имеют строго определённые типы фрагментов (fragment specifiers). Они говорят компилятору, что именно можно подставить на это место. Макросы в Rust, в отличие от функций, могут принимать разное число параметров.

Таблица сравнения

ТипЧто принимаетПримерКогда использовать
exprВыражение5 + 3xfoo()Чаще всего
blockБлок в {}{ let x = 5; x }Несколько операторов
identИмяmy_varMyTypeСоздание имён
tyТипi32Vec<String>Параметризация типами
pathПутьstd::collections::HashMapДоступ к модулям
ttДерево токеновЛюбой фрагмент кодаСложная обработка
literalЛитерал42"text"trueКонстанты
visВидимостьpubpub(crate)Генерация API
itemЭлементfn foo() {}struct SГенерация целых элементов
metaАтрибутderive(Debug)Генерация атрибутов
lifetimeВремя жизни'a'staticРабота со ссылками

Важное ограничение

Нельзя использовать expr когда нужно что-то, что обязательно должно заканчиваться точкой с запятой. Для этого есть stmt (но он нестабилен) или block.

expr

Макрос с параметрами expr (выражение, expression): любое валидное выражение Rust, которое возвращает значение:

// Макрос выводит текст несколько раз
macro_rules! повтори {
    // $раз:expr и $текст:expr - параметры
    ($раз:expr, $текст:expr) => {
        for _ in 0..$раз {
            println!("{}", $текст);
        }
    };
}

fn main() {
    повтори!(3, "Учу Rust!");  // Выведет "Учу Rust!" 3 раза
}

Макрос с вариативными параметрами:

// Макрос, принимающий любое количество чисел
macro_rules! сумма {
    // Базовый случай: одно число
    ($a:expr) => { $a };
    // Рекурсивный случай: число + остальные
    ($a:expr, $($rest:expr),*) => {
        $a + сумма!($($rest),*)
    };
}

fn main() {
    let s = сумма!(1, 2, 3, 4, 5);
    println!("Сумма: {}", s);  // 15
}

block

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

macro_rules! выполнить_блок {
    ($блок:block) => {
        println!("Выполняем блок:");
        let результат = $блок;
        println!("Результат блока: {}", результат);
    };
}

fn main() {
    выполнить_блок!({
        let a = 5;
        let b = 3;
        a * b
    });
    
    // ОШИБКА! Это не блок, а просто выражение
    // выполнить_блок!(5 + 3);
}

ident

Любой идентификатор языка: имя переменной, функции, типа, модуля.

macro_rules! объяви_переменную {
    ($имя:ident, $тип:ty, $значение:expr) => {
        let $имя: $тип = $значение;
    };
}

macro_rules! распечатай {
    ($имя:ident) => {
        println!("{} = {}", stringify!($имя), $имя);
    };
}

fn main() {
    объяви_переменную!(x, i32, 42);
    объяви_переменную!(сообщение, String, "Привет".to_string());
    
    let возраст = 30;
    распечатай!(возраст);  // выведет: возраст = 30
    
    // ОШИБКА! Это не идентификатор
    // распечатай!(5 + 3);
}

ty

Любое обозначение типа в Rust. Нужно, когда макрос должен работать с разными типами данных.

macro_rules! создай_структуру {
    ($имя:ident, $тип_поля:ty) => {
        struct $имя {
            значение: $тип_поля,
        }
        
        impl $имя {
            fn new(значение: $тип_поля) -> Self {
                Self { значение }
            }
        }
    };
}

создай_структуру!(Число, i32);
создай_структуру!(Текст, String);
создай_структуру!(Ссылка, &'static str);

fn main() {
    let a = Число::new(10);
    let b = Текст::new("пример".to_string());
    let c = Ссылка::new("привет");
}

tt

Один токен или группу токенов в скобках. Самый гибкий тип, но и самый сложный. Нужен для сложной обработки синтаксиса, парсинга, создания DSL.

macro_rules! распечатай_tt {
    ($токен:tt) => {
        println!("Токен: {}", stringify!($токен));
    };
}

macro_rules! повтори_tt {
    ($($т:tt)*) => {
        $($т)*
        $($т)*
    };
}

fn main() {
    распечатай_tt!(hello);           // идентификатор
    распечатай_tt!(5);               // число
    распечатай_tt!(+);               // оператор
    распечатай_tt!((1 + 2));         // группа в скобках (целое дерево)
    
    let x = повтори_tt!(1 + 2 * 3);  // станет: let x = 1 + 2 * 3 1 + 2 * 3;
}

path

Полный путь к элементу (с модулями и двоеточиями). Отличие от ident: ident — только одно имя без ::. path — цепочка имён с ::.

macro_rules! используй_функцию {
    ($путь:path, $аргумент:expr) => {
        let результат = $путь($аргумент);
        println!("Результат: {}", результат);
    };
}

fn моя_функция(x: i32) -> i32 {
    x * 2
}

mod utils {
    pub fn вспомогательная(x: i32) -> i32 {
        x + 10
    }
}

fn main() {
    используй_функцию!(моя_функция, 5);           // простое имя
    используй_функцию!(utils::вспомогательная, 5); // полный путь
    используй_функция!(std::cmp::max, 5, 10);      // но это уже два аргумента 
                                                    // (path не подходит для нескольких)
}

item

Целое определение элемента в Rust (функцию, структуру, impl, модуль и т.д.). Для генерации целых элементов на уровне модуля.

macro_rules! создай_тест {
    ($имя:ident, $тело:item) => {
        #[test]
        fn $имя() {
            $тело
        }
    };
}

создай_тест!(тест_сложения, {
    assert_eq!(2 + 2, 4);
});

// Можно даже целую функцию вставить
создай_тест!(тест_с_функцией, {
    fn вспомогательная() -> i32 { 42 }
    assert_eq!(вспомогательная(), 42);
});

meta

Содержимое атрибутов (для макросов, которые генерируют атрибуты).

macro_rules! добавить_атрибут {
    ($атрибут:meta, $код:item) => {
        #[$атрибут]
        $код
    };
}

добавить_атрибут!(derive(Debug), {
    struct Point { x: i32, y: i32 }
});

// Разворачивается в:
// #[derive(Debug)]
// struct Point { x: i32, y: i32 }