Generics и трейты решают задачу полиморфизма по типам, но не умеют: принимать переменное число аргументов, генерировать код на этапе компиляции по структуре другого кода (например, автоматически реализовать трейт для структуры, зная список её полей), или работать с синтаксисом, которого ещё не существует в виде значения (SQL-подобный DSL, HTML-шаблоны). Макросы в Rust — это метапрограммирование на уровне AST, выполняемое до типовой проверки.
| Механизм | Когда работает | Что умеет |
|---|---|---|
| Generics / trait bounds | компиляция, после парсинга | полиморфизм по типам с проверкой границ |
macro_rules! | раннее расширение AST (expansion) | сопоставление с образцом по токенам, переменное число аргументов |
| proc-macro | компиляция крейта, до основной сборки | произвольные преобразования TokenStream → TokenStream (полный Rust-код) |
macro_rules! определяет макрос через набор шаблонов (patterns) сопоставления токенов, похожих на ветки match, но работающих на уровне синтаксиса, а не значений.
macro_rules! my_vec {
() => {
Vec::new()
};
($($x:expr),+ $(,)?) => {
{
let mut v = Vec::new();
$( v.push($x); )+
v
}
};
}
fn main() {
let empty: Vec<i32> = my_vec!();
let nums = my_vec![1, 2, 3,]; // trailing comma поддержан благодаря $(,)?
println!("{nums:?}");
}
Основные fragment specifiers, определяющие тип "дырки" в шаблоне: expr (выражение), ident (идентификатор), ty (тип), pat (паттерн), block, stmt, tt (произвольное token tree — самый гибкий), literal, path, vis (видимость).
macro_rules! create_getter {
($field:ident : $ty:ty) => {
pub fn $field(&self) -> &$ty {
&self.$field
}
};
}
struct User { name: String, age: u32 }
impl User {
create_getter!(name: String);
create_getter!(age: u32);
}
Rust-макросы гигиеничны: идентификаторы, введённые внутри макроса (например, временная переменная в теле my_vec!), не конфликтуют с одноимёнными идентификаторами в коде вызывающей стороны — они существуют как бы в отдельном "синтаксическом контексте". Это принципиально отличает Rust от текстовых макросов C/C++ препроцессора.
macro_rules! using_v {
($e:expr) => {
{
let v = 42; // эта `v` невидима снаружи макроса
v + $e
}
};
}
fn main() {
let v = 10;
let result = using_v!(v); // `v` в аргументе — это внешняя v=10, а не внутренняя v=42
assert_eq!(result, 52); // 42 (внутренняя) + 10 (внешняя v, подставленная как $e)
}
Гигиена избавляет от классической проблемы C-макросов — "захвата" переменной по имени, когда макрос случайно ссылается на переменную вызывающего кода или наоборот. В Rust такое возможно только явно, если вы намеренно передаёте идентификатор через $name:ident.
macro_rules! частичная — она защищает локальные переменные и метки, но не распространяется одинаково на все контексты (например, на видимость через self в некоторых случаях). Proc-macro на основе syn/quote дают более явный контроль через Span::call_site() и Span::mixed_site().
Процедурные макросы — это функции, которые получают TokenStream на вход и возвращают новый TokenStream, выполняясь как отдельный компилируемый плагин компилятора. Они должны быть объявлены в отдельном крейте с proc-macro = true в Cargo.toml. Существует три вида.
#[derive(Debug, Clone, MyTrait)]
struct Point { x: i32, y: i32 }
#[route(GET, "/users")]
fn list_users() -> impl Responder { /* ... */ }
#[tokio::main]
async fn main() { /* ... */ }
let query = sql!(SELECT id, name FROM users WHERE id = ?);
| Вид | Вход | Типичное применение |
|---|---|---|
| derive | структура/enum целиком | Serialize/Deserialize, Debug, builder-паттерн |
| attribute | элемент + сам атрибут | роутинг веб-фреймворков, async runtime entrypoint |
| function-like | произвольные токены в скобках | DSL: SQL, HTML, regex, форматирование |
Писать proc-macro напрямую с TokenStream неудобно — токены нужно парсить в структуры и генерировать код обратно. Экосистема стандартизировалась вокруг двух крейтов: syn парсит токены в типизированное синтаксическое дерево Rust (структуры, поля, типы), а quote генерирует токены из шаблона, похожего на обычный Rust-код с интерполяцией через #var.
// в крейте с proc-macro = true
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
#[proc_macro_derive(Describe)]
pub fn derive_describe(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let field_names: Vec<String> = match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Named(named) => named.named.iter()
.map(|f| f.ident.as_ref().unwrap().to_string())
.collect(),
_ => vec![],
},
_ => vec![],
};
let expanded = quote! {
impl $name {
pub fn describe() -> &'static [&'static str] {
&[#(#field_names),*]
}
}
};
expanded.into()
}
Конструкция #(#field_names),* в quote! — это повторение (repetition), аналог $(...)* в macro_rules!, разворачивающее вектор строк в список токенов, разделённых запятыми.
Основная сложность работы с макросами — непрозрачность: ошибки типов после расширения указывают на сгенерированный код, а не на исходный вызов макроса, что усложняет диагностику.
$ cargo expand # печатает код после раскрытия всех макросов
$ cargo expand my_module::my_fn # раскрытие конкретного элемента
Внутри macro_rules! помогает атрибут для трассировки, а для proc-macro — вывод через eprintln!("{}", expanded) во время компиляции (в build-скрипте proc-macro крейта) или использование syn::Error::to_compile_error() для превращения внутренней ошибки парсинга в понятное сообщение компилятора, указывающее на нужный Span.
use syn::spanned::Spanned;
fn check_named_fields(fields: &syn::Fields) -> syn::Result<()> {
match fields {
syn::Fields::Named(_) => Ok(()),
_ => Err(syn::Error::new(
fields.span(),
"Describe поддерживает только именованные поля",
)),
}
}
syn/quote/proc-macro2) заметно увеличивает время cargo build, особенно clean-сборки.
serde), билдеры, конверсии, ORM-мэппинг полей.#[instrument] из tracing).sqlx::query!), HTML-шаблоны (html! в Yew).// Пример из реальной практики: sqlx проверяет SQL на этапе компиляции,
// обращаясь к схеме БД через переменную окружения DATABASE_URL
let row = sqlx::query!("SELECT id, email FROM users WHERE id = $1", user_id)
.fetch_one(&pool)
.await?;
macro_rules! — он проще, не требует отдельного крейта и достаточен для 90% случаев дублирования кода. Переходите на proc-macro только когда нужен полноценный доступ к синтаксическому дереву входных данных (derive) или произвольный DSL-синтаксис.