Метапрограммирование в Rust: макросы и мощь кода на новом уровне

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: зачем Rust умеет колдовать с кодом?

Вы когда-нибудь писали на Rust и чувствовали, что повторяете одни и те же конструкции для десятка структур? Или хотели бы автоматически генерировать реализацию трейта, не копируя код вручную? Добро пожаловать в мир метапрограммирования в Rust — техник, которые позволяют писать код, который пишет код.

Rust не имеет полноценной рефлексии во время выполнения (как Java или Python), но он предлагает нечто более мощное и безопасное — макросы. Они работают на этапе компиляции, анализируя и преобразуя абстрактное синтаксическое дерево (AST). Это даёт невероятные возможности для оптимизации и сокращения boilerplate-кода без потери производительности.

В этой статье мы разберём продвинутые техники метапрограммирования: от классических macro_rules! до полноценных процедурных макросов. Вы узнаете, как создавать свои derive-атрибуты и писать атрибутные макросы, которые меняют семантику кода. Поехали!

Декларативные макросы: macro_rules! — это не просто "замена текста"

Многие думают, что macro_rules! — это примитивный текстовый препроцессор. На самом деле, это мощная система сопоставления с образцом, работающая на уровне токенов. Она позволяет создавать DSL (предметно-ориентированные языки) прямо внутри Rust.

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

Представьте, что вам нужно создать "строитель" (builder) для структуры. Вместо того чтобы писать десятки методов, можно написать один макрос:

macro_rules! builder {
    ($struct_name:ident, { $($field:ident: $field_type:ty),* $(,)? }) => {
        struct $struct_name {
            $($field: Option<$field_type>),*
        }

        impl $struct_name {
            $(
                pub fn $field(mut self, value: $field_type) -> Self {
                    self.$field = Some(value);
                    self
                }
            )*

            pub fn build(self) -> Result<Self, String> {
                $(
                    if self.$field.is_none() {
                        return Err(format!("Field {} is required", stringify!($field)));
                    }
                )*
                Ok(self)
            }
        }
    };
}

builder!(Config, {
    host: String,
    port: u16,
    timeout: u64,
});

fn main() {
    let config = Config {
        host: None,
        port: None,
        timeout: None,
    }
    .host("localhost".to_string())
    .port(8080)
    .build()
    .unwrap();
    println!("Config ready: {}:{}", config.host.unwrap(), config.port.unwrap());
}

Этот макрос не просто генерирует поля, но и добавляет валидацию обязательных полей. Обратите внимание на stringify!($field) — он преобразует идентификатор в строку на этапе компиляции. Это типичный приём продвинутого метапрограммирования в Rust.

Репетиторы и повторения: $(...)* в действии

Синтаксис повторения позволяет обрабатывать списки аргументов любой длины. Комбинируя ident, ty, expr и другие "фрагменты", вы можете создавать сложные шаблоны. Например, макрос для создания enum с автоматической реализацией Display:

macro_rules! display_enum {
    ($enum_name:ident, { $($variant:ident => $desc:expr),* $(,)? }) => {
        enum $enum_name {
            $($variant),*
        }

        impl std::fmt::Display for $enum_name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                match self {
                    $(Self::$variant => write!(f, "{}", $desc)),*
                }
            }
        }
    };
}

display_enum!(Status, {
    Active => "status_active",
    Inactive => "status_inactive",
    Pending => "status_pending",
});

Такие макросы здорово экономят время при работе с большими перечислениями. Это одна из самых полезных продвинутых техник для повседневной разработки.

Процедурные макросы: полный контроль над AST

Если macro_rules! — это швейцарский нож, то процедурные макросы — это промышленный робот. Они пишутся на Rust как отдельные крейты и получают на вход поток токенов, а на выходе должны вернуть новый поток. Существует три типа процедурных макросов:

  • Функциональные макросы — вызываются как my_macro!(...)
  • Атрибутные макросы — задаются как #[my_attribute]
  • Derive-макросы — автоматически реализуют трейты через #[derive(MyTrait)]

Создаём свой derive-макрос: #[derive(Hello)]

Сначала создайте новый крейт с процедурными макросами. В Cargo.toml добавьте:

[lib]
proc-macro = true

[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = "1.0"

Теперь сам макрос:

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Hello)]
pub fn hello_derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;

    let expanded = quote! {
        impl Hello for #name {
            fn hello(&self) -> String {
                format!("Hello from {}!", stringify!(#name))
            }
        }
    };

    TokenStream::from(expanded)
}

Теперь любой тип может получить метод hello() одной строчкой:

#[derive(Hello)]
struct MyStruct;

fn main() {
    let s = MyStruct;
    println!("{}", s.hello()); // Выведет: "Hello from MyStruct!"
}

Это очень мощная техника метапрограммирования в Rust. Она используется в популярных крейтах: serde, clap, diesel — везде, где нужно автоматически генерировать реализации трейтов.

Атрибутные макросы: пишем свой #[route]

Атрибутные макросы позволяют модифицировать функции, структуры или модули. Например, можно создать простой аналог роутинга из веб-фреймворков:

#[proc_macro_attribute]
pub fn route(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input_fn = parse_macro_input!(item as syn::ItemFn);
    let fn_name = &input_fn.sig.ident;
    let fn_block = &input_fn.block;

    let expanded = quote! {
        pub fn #fn_name() {
            println!("Executing route: {}", stringify!(                

Блоги

Рекомендаций по книгам