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

Процедурные макросы в Rust: derive, attribute-like и function-like

1041 слово·5 минут· loading · loading · · ·Rust-middle Черновик
О Rust - Эта статья часть цикла.
Статей прочитано 0/58
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
35 Процедурные макросы в Rust: derive, attribute-like и function-like (текущая)
Не прочитана

Введение
#

В отличие от декларативных макросов (macro_rules!), которые работают по принципу паттерн-матчинга токенов, процедурные макросы (Procedural Macros) в Rust являются полноценными функциями на языке Rust.

Процедурный макрос принимает на вход поток токенов proc_macro::TokenStream, выполняет произвольную логику трансформации кода на этапе компиляции и возвращает новый TokenStream, который компилятор встраивает в итоговую сборку.

Именно благодаря процедурным макросам функционируют популярнейшие библиотеки экосистемы: serde (#[derive(Serialize, Deserialize)]), tokio (#[tokio::main]), actix-web (#[get("/api")]) и sqlx (sql!("SELECT...")).

В этой статье мы подробно разберем три типа процедурных макросов, устройство proc-macro крейтов и роль связки библиотек syn и quote.


1. Три категории процедурных макросов
#

Стандарт языка Rust выделяет три разновидности процедурных макросов:

1. Пользовательский Derive (Custom Derive)
#

Вызывается через атрибут #[derive(MyTrait)] над структурами или перечислениями.

  • Назначение: Автоматически генерирует реализацию типажа (impl MyTrait for Struct).
  • Особенность: Не может изменять существующий код структуры, он лишь добавляет новый код рядом.

2. Атрибутные макросы (Attribute-like)
#

Создают пользовательские атрибуты вида #[route(GET, "/users")] или #[tokio::main].

  • Назначение: Применяются к функциям, структурам или модулям.
  • Особенность: Принимают в качестве аргументов как свойства атрибута, так и декорируемый элемент, способны полностью переписать или заменить его.

3. Функциональные макросы (Function-like)
#

Вызываются как обычные макросы my_macro!(...).

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

Попробуйте запустить интерактивную эмуляцию работы разных типов макросов:

Концепция Custom Derive и Атрибутов
Шаг 1/2
 1// Эмуляция поведения Custom Derive #[derive(Describe)]
 2trait Describe {
 3    fn describe(&self) -> String;
 4}
 5
 6struct UserProfile {
 7    username: String,
 8    age: u32,
 9    is_active: bool,
10}
11
12// Процедурный макрос derive генерирует следующую реализацию типажа во время компиляции:
13impl Describe for UserProfile {
14    fn describe(&self) -> String {
15        format!(
16            "Структура UserProfile [username: {}, age: {}, is_active: {}]",
17            self.username, self.age, self.is_active
18        )
19    }
20}
21
22fn main() {
23    let user = UserProfile {
24        username: "Alex".to_string(),
25        age: 28,
26        is_active: true,
27    };
28
29    println!("{}", user.describe());
30}
31

1. Идея Custom Derive макросов

  • Атрибут #[derive(MyTrait)] анализирует AST структуры или перечисления на этапе компиляции.
  • Он генерирует новую реализацию типажа (impl MyTrait for Struct), НЕ изменяя исходный код структуры.

2. Анатомия Proc-Macro Крейта
#

В силу того, что процедурный макрос исполняется в процессе работы самого компилятора rustc, он подлежит абсолютно строгому архитектурному ограничению:

Важно

Процедурные макросы должны быть вынесены в отдельный Cargo-крейт, в файле Cargo.toml которого явно прописан флаг proc-macro = true:

[lib]
proc-macro = true

Компилятор сначала собирает этот отдельный крейт в динамическую библиотеку (.so / .dylib / .dll), затем загружает её в память своего процесса и передает ей токены из вашего основного проекта.


3. Инструментарий: syn, quote и TokenStream
#

Ручной парсинг сырых токенов proc_macro::TokenStream — задача крайне сложная. Поэтому 99% процедурных макросов в мире Rust пишутся с использованием двух фундаментальных библиотек:

  1. syn — библиотека-парсер. Она превращает сырые токены Rust в типизированное абстрактное синтаксическое дерево (AST).

    use syn::{parse_macro_input, DeriveInput};
    
    #[proc_macro_derive(MyTrait)]
    pub fn my_derive(input: TokenStream) -> TokenStream {
        let ast = parse_macro_input!(input as DeriveInput);
        // Теперь ast содержит struct_name, имена полей и их типы!
    }
  2. quote — библиотека квази-цитирования. Она позволяет генерировать новый код Rust прямо в фигурных скобках с подстановкой переменных #ast_node:

    use quote::quote;
    
    let expanded = quote! {
        impl MyTrait for #name {
            fn hello() { println!("Привет от структуры!"); }
        }
    };
    expanded.into()

Изучите рабочий процесс разбора AST и генерации кода в интерактивных слайдах:

Связка syn + quote и генерация кода
Шаг 1/2
 1// Эмуляция разбора AST через syn и генерации кода через quote
 2struct FieldInfo {
 3    name: &'static str,
 4    ty: &'static str,
 5}
 6
 7struct ParsedStruct {
 8    struct_name: &'static str,
 9    fields: Vec<FieldInfo>,
10}
11
12fn simulate_syn_parse() -> ParsedStruct {
13    // Библиотека syn превращает сырой TokenStream в типизированную AST-структуру DeriveInput
14    ParsedStruct {
15        struct_name: "Config",
16        fields: vec![
17            FieldInfo { name: "db_url", ty: "String" },
18            FieldInfo { name: "max_connections", ty: "u32" },
19        ],
20    }
21}
22
23fn simulate_quote_generate(ast: &ParsedStruct) -> String {
24    // Библиотека quote! { ... } генерирует конечный код на основе разобранного AST
25    let mut code = format!("impl Default for {} {{\n  fn default() -> Self {{\n    Self {{\n", ast.struct_name);
26    for field in &ast.fields {
27        code.push_str(&format!("      {}: Default::default(),\n", field.name));
28    }
29    code.push_str("    }\n  }\n}");
30    code
31}
32
33fn main() {
34    let ast = simulate_syn_parse();
35    let generated_code = simulate_quote_generate(&ast);
36
37    println!("--- Сгенерированный код через syn & quote ---");
38    println!("{generated_code}");
39}
40

1. Связка syn и quote

  • syn::parse::<DeriveInput>(tokens) парсит исходные лексемы кода в выверенное AST-дерево.
  • quote! { ... } генерирует готовые токены Rust для вставки в итоговую бинарную сборку.

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

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

Статья прочитана
Пожалуйста, оцените насколько статья была вам полезна и понятна
Цикл статей
О Rust - Эта статья часть цикла.
Статей прочитано 0/58
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
35 Процедурные макросы в Rust: derive, attribute-like и function-like (текущая)
Не прочитана

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