Что такое Procedural Macros
Procedural macros — это код, который выполняется во время компиляции и генерирует Rust-код. Они позволяют создавать DSL (Domain-Specific Languages) и автоматизировать повторяющийся код.
▸Типы procedural macros
Derive macros — генерируют impl блоки для структур
Attribute macros — определяют кастомные атрибуты
Function-like macros — выглядят как вызовы функций
Derive Macros
▸Создание derive macro
1// proc-macro crate2use proc_macro::TokenStream;3use quote::quote;4use syn::{parse_macro_input, DeriveInput};56#[proc_macro_derive(HelloMacro)]7pub fn hello_macro_derive(input: TokenStream) -> TokenStream {8 let ast = parse_macro_input!(input as DeriveInput);9 let name = &ast.ident;1011 let gen = quote! {12 impl HelloMacro for #name {13 fn hello_macro() {14 println!("Hello from {}!", stringify!(#name));15 }16 }17 };1819 gen.into()20}2122// Trait для derive23pub trait HelloMacro {24 fn hello_macro();25}
▸Использование
1use my_macros::HelloMacro;23#[derive(HelloMacro)]4struct Pancakes;56fn main() {7 Pancakes::hello_macro(); // Hello from Pancakes!8}
Attribute Macros
▸Создание attribute macro
1use proc_macro::TokenStream;2use quote::quote;3use syn::{parse_macro_input, ItemFn};45#[proc_macro_attribute]6pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {7 let input = parse_macro_input!(item as ItemFn);8 let name = &input.sig.ident;9 let body = &input.block;10 let attrs = attr.to_string();1112 let gen = quote! {13 fn #name() {14 println!("Route: {}", #attrs);15 #body16 }17 };1819 gen.into()20}
▸Использование
1use my_macros::route;23#[route("GET /users")]4fn get_users() {5 println!("Getting users");6}
Function-like Macros
1use proc_macro::TokenStream;2use quote::quote;3use syn::{parse_macro_input, LitStr};45#[proc_macro]6pub fn sql(input: TokenStream) -> TokenStream {7 let query = parse_macro_input!(input as LitStr);8 let query_str = query.value();910 let gen = quote! {11 {12 let query = #query_str;13 println!("Executing SQL: {}", query);14 // Выполнение запроса15 }16 };1718 gen.into()19}
▸Использование
1use my_macros::sql;23fn main() {4 sql!("SELECT * FROM users WHERE id = 1");5}
syn и quote
▸Парсинг с syn
1use syn::{parse_macro_input, DeriveInput, Data, Fields};23#[proc_macro_derive(Builder)]4pub fn builder_derive(input: TokenStream) -> TokenStream {5 let input = parse_macro_input!(input as DeriveInput);6 let name = &input.ident;78 // Получение полей9 let fields = match &input.data {10 Data::Struct(data) => match &data.fields {11 Fields::Named(fields) => &fields.named,12 _ => panic!("Only named fields supported"),13 },14 _ => panic!("Only structs supported"),15 };1617 // Генерация кода18 let builder_name = format_ident!("{}Builder", name);19 let field_names: Vec<_> = fields.iter().map(|f| &f.ident).collect();20 let field_types: Vec<_> = fields.iter().map(|f| &f.ty).collect();2122 let gen = quote! {23 struct #builder_name {24 #(#field_names: Option<#field_types>,)*25 }2627 impl #builder_name {28 fn new() -> Self {29 Self {30 #(#field_names: None,)*31 }32 }3334 #(35 fn #field_names(mut self, value: #field_types) -> Self {36 self.#field_names = Some(value);37 self38 }39 )*40 }41 };4243 gen.into()44}
Практические примеры
▸Serialize/Deserialize
1// serde derive macro2#[derive(Serialize, Deserialize, Debug)]3struct User {4 name: String,5 age: u32,6 email: String,7}89// Автоматическая реализация10// impl Serialize for User { ... }11// impl Deserialize<'de> for User { ... }
▸Builder pattern
1#[derive(Builder)]2struct Config {3 host: String,4 port: u16,5 max_connections: u32,6}78// Генерируется:9// ConfigBuilder с методами host(), port(), max_connections()10// build() метод для создания Config
Тестирование macros
1// tests/macro_tests.rs2use my_macros::HelloMacro;34#[derive(HelloMacro)]5struct TestStruct;67#[test]8fn test_hello_macro() {9 // Проверка что макрос генерирует правильный код10 let output = TestStruct::hello_macro();11 assert!(output.contains("TestStruct"));12}
Ошибки в macros
1use proc_macro::TokenStream;2use syn::parse_macro_input;34#[proc_macro]5pub fn my_macro(input: TokenStream) -> TokenStream {6 let _ = parse_macro_input!(input as syn::Expr)7 .map_err(|e| {8 // Генерация ошибки компиляции9 syn::Error::new(e.span(), "Invalid expression")10 .to_compile_error()11 });1213 // ...14}
Best Practices
▸Минимальные macros
1// Плохо: сложная логика в macro2#[proc_macro_derive(BadMacro)]3pub fn bad_derive(input: TokenStream) -> TokenStream {4 // Сотни строк кода5}67// Хорошо: простой delegation8#[proc_macro_derive(GoodMacro)]9pub fn good_derive(input: TokenStream) -> TokenStream {10 let ast = parse_macro_input!(input as DeriveInput);11 generate_impl(ast).into()12}
▸Документирование
1/// Добавляет метод hello_macro() для структуры2///3/// # Пример4/// ```5/// #[derive(HelloMacro)]6/// struct Foo;7///8/// Foo::hello_macro(); // Prints "Hello from Foo!"9/// ```10#[proc_macro_derive(HelloMacro)]11pub fn hello_macro_derive(input: TokenStream) -> TokenStream {12 // ...13}
Заключение
Procedural macros — мощный инструмент для метапрограммирования в Rust. Они позволяют создавать DSL и автоматизировать повторяющийся код. Понимание syn и quote критически важно для написания macros. На собеседовании спрашивают про разницу types macros, как работают derive macros, и когда использовать macros vs generic code.