Введение: зачем 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!(