В отличие от декларативных макросов (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.
В силу того, что процедурный макрос исполняется в процессе работы самого компилятора rustc, он подлежит абсолютно строгому архитектурному ограничению:
Важно
Процедурные макросы должны быть вынесены в отдельный Cargo-крейт, в файле Cargo.toml которого явно прописан флаг proc-macro = true:
[lib]proc-macro=true
Компилятор сначала собирает этот отдельный крейт в динамическую библиотеку (.so / .dylib / .dll), затем загружает её в память своего процесса и передает ей токены из вашего основного проекта.
Ручной парсинг сырых токенов proc_macro::TokenStream — задача крайне сложная. Поэтому 99% процедурных макросов в мире Rust пишутся с использованием двух фундаментальных библиотек:
syn — библиотека-парсер. Она превращает сырые токены Rust в типизированное абстрактное синтаксическое дерево (AST).
usesyn::{parse_macro_input,DeriveInput};#[proc_macro_derive(MyTrait)]pubfnmy_derive(input: TokenStream)-> TokenStream{letast=parse_macro_input!(inputasDeriveInput);// Теперь ast содержит struct_name, имена полей и их типы!
}
quote — библиотека квази-цитирования. Она позволяет генерировать новый код Rust прямо в фигурных скобках с подстановкой переменных #ast_node:
usequote::quote;letexpanded=quote!{implMyTraitfor#name{fnhello(){println!("Привет от структуры!");}}};expanded.into()
Изучите рабочий процесс разбора AST и генерации кода в интерактивных слайдах:
Связка syn + quote и генерация кода
Шаг 1/2
1// Эмуляция разбора AST через syn и генерации кода через quote
2structFieldInfo{ 3name: &'staticstr, 4ty: &'staticstr, 5} 6 7structParsedStruct{ 8struct_name: &'staticstr, 9fields: Vec<FieldInfo>,10}1112fnsimulate_syn_parse()-> ParsedStruct{13// Библиотека syn превращает сырой TokenStream в типизированную AST-структуру DeriveInput
14ParsedStruct{15struct_name: "Config",16fields: vec![17FieldInfo{name: "db_url",ty: "String"},18FieldInfo{name: "max_connections",ty: "u32"},19],20}21}2223fnsimulate_quote_generate(ast: &ParsedStruct)-> String{24// Библиотека quote! { ... } генерирует конечный код на основе разобранного AST
25letmutcode=format!("impl Default for {}{{\n fn default() -> Self {{\n Self {{\n",ast.struct_name);26forfieldin&ast.fields{27code.push_str(&format!(" {}: Default::default(),\n",field.name));28}29code.push_str(" }\n }\n}");30code31}3233fnmain(){34letast=simulate_syn_parse();35letgenerated_code=simulate_quote_generate(&ast);3637println!("--- Сгенерированный код через syn & quote ---");38println!("{generated_code}");39}40
1. Связка syn и quote
syn::parse::<DeriveInput>(tokens) парсит исходные лексемы кода в выверенное AST-дерево.
quote! { ... } генерирует готовые токены Rust для вставки в итоговую бинарную сборку.
Результат выполнения:
1// Эмуляция работы Function-like макроса sql_query!("SELECT * FROM users WHERE id = $1")
2structSqlQuery{ 3raw_query: &'staticstr, 4params_count: usize, 5} 6 7implSqlQuery{ 8fnnew(query: &'staticstr)-> Self{ 9letcount=query.matches('$').count();10SqlQuery{raw_query: query,params_count: count}11}12}1314fnmain(){15// В SQL-библиотеках (например, sqlx!) процедурный макрос sql!("SELECT...")
16// проверяет валидность синтаксиса базы данных на этапе КОМПИЛЯЦИИ:
17letq=SqlQuery::new("SELECT name, email FROM users WHERE id = $1 AND active = $2");1819println!("Проверенный запрос: '{}'",q.raw_query);20println!("Ожидается параметров на вход: {}",q.params_count);21}22
2. Идея Function-like макросов
Вызываются как my_macro!(...) и принимают произвольный поток токенов в скобках.
Способны выполнять статическую валидацию запросов к БД, регулярных выражений или конфигураций прямо при сборке.