Процедурные макросы в Rust: свой #[derive]
Каждый раз, когда ты пишешь #[derive(Debug)] или #[derive(Serialize)], внутри компилятора запускается маленькая программа. Она смотрит на твою структуру и пишет за тебя блок impl. Эта маленькая программа — процедурный макрос (procedural macro), и ты можешь написать свой.
Представь услужливого клерка. Ты отдаёшь ему анкету (свою структуру), он читает каждое поле и возвращает скучные бумаги уже заполненными. На странице Макросы ты познакомился с macro_rules!, который работает через сопоставление с образцом. Процедурные макросы устроены иначе: это обычные функции на Rust, которые получают код и возвращают код. На этой странице ты шаг за шагом построишь настоящий макрос и увидишь настоящий вывод.
Три вида процедурных макросов
Заголовок раздела «Три вида процедурных макросов»Видов три. Внутри они устроены одинаково, но для того, кто ими пользуется, выглядят по-разному:
| Вид | Что пишет пользователь | Известные примеры |
|---|---|---|
| Derive-макрос | #[derive(Serialize)] над структурой или перечислением |
Serialize из serde, Error из thiserror, Parser из clap |
| Атрибутный макрос | #[route(GET, "/")] над функцией или другим элементом |
#[tokio::main], #[get("/")] из actix-web |
| Функциональный макрос | sql!(SELECT * FROM users) |
query! из sqlx, html! из yew |
- Derive-макрос только добавляет код рядом с твоим типом. Сама структура остаётся ровно такой, какой ты её написал.
- Атрибутный макрос заменяет элемент, над которым стоит.
#[tokio::main]берёт твоюasync fn mainи превращает её в обычнуюfn main, которая запускает рантайм tokio. - Функциональный макрос (function-like) выглядит как вызов
macro_rules!, но что значат входные данные, решает функция на Rust. Именно такsqlx::query!может проверять твой SQL во время компиляции.
Derive-макросами из других крейтов ты уже пользуешься. Вот два из них за настоящей работой. Serialize пишет код для JSON (см. JSON и Serde), а Error пишет код Display из текста в #[error(...)] (см. anyhow и thiserror):
use serde::Serialize;use thiserror::Error;
#[derive(Serialize)]struct Order { id: u32, item: String, quantity: u8,}
#[derive(Debug, Error)]enum ShopError { #[error("товар «{0}» закончился")] OutOfStock(String),}
fn main() { let order = Order { id: 42, item: "Чайник".to_string(), quantity: 2 }; println!("{}", serde_json::to_string(&order).unwrap());
let err = ShopError::OutOfStock("Чайник".to_string()); println!("{err}");}Вывод:
{"id":42,"item":"Чайник","quantity":2}товар «Чайник» закончилсяТы не писал ни реализацию Display, ни код для JSON. Всё это написали derive-макросы, пока программа компилировалась.
Как они работают: токены на входе, токены на выходе
Заголовок раздела «Как они работают: токены на входе, токены на выходе»Сначала компилятор режет исходный код на токены (tokens): имена, знаки препинания, литералы, скобки. Процедурный макрос — это функция, которая получает эти токены как TokenStream и возвращает новый TokenStream. Затем компилятор компилирует всё, что ты вернул, как будто ты сам это напечатал.
Вот три формы таких функций. Каждую помечают атрибутом, чтобы компилятор знал, какого она вида:
use proc_macro::TokenStream;
// #[derive(HelloMacro)]: получает структуру или перечисление, возвращает код, который нужно ДОБАВИТЬ.#[proc_macro_derive(HelloMacro)]pub fn hello_macro_derive(input: TokenStream) -> TokenStream { /* ... */ }
// #[route(GET, "/")]: получает токены самого атрибута и элемент, возвращает ЗАМЕНУ.#[proc_macro_attribute]pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { /* ... */ }
// sql!(...): получает всё внутри скобок, возвращает замену.#[proc_macro]pub fn sql(input: TokenStream) -> TokenStream { /* ... */ }Голый TokenStream — это просто список токенов, и работать с ним трудно. Поэтому почти каждый процедурный макрос использует три вспомогательных крейта:
- syn разбирает токены в синтаксическое дерево. Вместо токенов ты получаешь
DeriveInputс именем типа, его обобщёнными параметрами и полями. - quote превращает код, похожий на Rust, обратно в токены. Внутри
quote! { ... }ты пишешь обычный Rust, а#nameвставляет переменную. - proc-macro2 — обёртка над типами
proc_macroкомпилятора, которой пользуются syn и quote. Она работает и вне компилятора, поэтому со вспомогательными функциями и тестами проще.
Одна важная мысль: твой макрос выполняется во время компиляции. Cargo сначала компилирует крейт с макросом для твоего компьютера, а компилятор запускает его, пока собирает крейт, который этот макрос использует. Когда программа наконец запускается, макроса давно нет. Остаётся только код, который он написал.
Настраиваем проект
Заголовок раздела «Настраиваем проект»Процедурный макрос должен жить в отдельном крейте особого типа. Такой крейт может экспортировать только макросы — ни обычных функций, ни трейтов. Поэтому обычно делают два крейта рядом, как serde и serde_derive: один для трейта, другой для derive. Мы добавим третий крейт — маленькую программу shop, которая ими пользуется. (Нужно освежить в памяти крейты и рабочие пространства? См. Cargo и крейты.)
Строки, которые печатают программы этого проекта, остались на английском: весь вывод ниже — настоящий результат сборки и запуска именно этого кода.
mkdir procmacro-demo && cd procmacro-democargo new --lib hello_macrocargo new --lib hello_macro_derivecargo new shopВот готовое дерево файлов:
procmacro-demo/├── Cargo.toml (рабочее пространство)├── hello_macro/│ ├── Cargo.toml│ └── src/lib.rs (трейты и реэкспорт derive-макросов)├── hello_macro_derive/│ ├── Cargo.toml (proc-macro = true)│ └── src/lib.rs (сами макросы)└── shop/ ├── Cargo.toml └── src/main.rs (программа, которая использует #[derive(...)])Cargo.toml верхнего уровня связывает три крейта в одно рабочее пространство (workspace):
[workspace]members = ["hello_macro", "hello_macro_derive", "shop"]resolver = "2"Только у крейта с макросами есть кое-что особенное: proc-macro = true. Ещё он зависит от syn, quote и proc-macro2:
[package]name = "hello_macro_derive"version = "0.1.0"edition = "2024"
[lib]proc-macro = true
[dependencies]proc-macro2 = "1"quote = "1"syn = "2"Два других крейта обычные. hello_macro зависит от крейта с derive, а shop нужен только hello_macro:
[dependencies]hello_macro_derive = { version = "0.1.0", path = "../hello_macro_derive" }[dependencies]hello_macro = { version = "0.1.0", path = "../hello_macro" }Шаг 1: #[derive(HelloMacro)]
Заголовок раздела «Шаг 1: #[derive(HelloMacro)]»Начнём с крошечного. Мы хотим, чтобы любой тип с #[derive(HelloMacro)] получал функцию hello_macro(), которая печатает имя типа. Сначала трейт, в hello_macro/src/lib.rs. Последняя строка реэкспортирует derive-макросы, чтобы пользователям был нужен только этот один крейт. Ровно так делает serde.
pub trait HelloMacro { fn hello_macro();}
pub trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str];}
// Реэкспортируем derive-макросы, чтобы пользователям хватало одного этого крейта.pub use hello_macro_derive::{Describe, HelloMacro};(Describe понадобится на шаге 2.) Теперь сам макрос, в hello_macro_derive/src/lib.rs:
use proc_macro::TokenStream;use quote::quote;use syn::{parse_macro_input, Data, DeriveInput, Fields}; // Data и Fields: шаг 2
#[proc_macro_derive(HelloMacro)]pub fn hello_macro_derive(input: TokenStream) -> TokenStream { // 1. Разбираем токены в синтаксическое дерево, которое можно изучать. let ast = parse_macro_input!(input as DeriveInput); let name = &ast.ident;
// 2. Собираем новый код с помощью quote!. let generated = quote! { impl hello_macro::HelloMacro for #name { fn hello_macro() { println!("Hello, Macro! My name is {}!", stringify!(#name)); } } };
// 3. Отдаём новые токены обратно компилятору. generated.into()}Три шага, и ты увидишь их почти в каждом derive-макросе:
parse_macro_input!превращает токены вDeriveInput.ast.ident— имя типа, напримерProduct. Если разбор не удался, этот макрос сам вернёт нормальную ошибку компиляции.quote!собирает блокimpl.#nameвставляет имя.stringify!(#name)превращает имя в текст"Product"..into()преобразуетproc_macro2::TokenStreamиз quote вproc_macro::TokenStreamкомпилятора.
Обрати внимание на полный путь hello_macro::HelloMacro. Сгенерированный код вставляется в файл пользователя, поэтому он не может полагаться на строки use пользователя.
Когда кто-то пишет #[derive(HelloMacro)] struct Product, компилятор добавляет код, который работает так же, как эта версия, написанная вручную. Она запускается в Playground, потому что здесь нет крейта с макросом:
trait HelloMacro { fn hello_macro();}
struct Product;
// Вот что #[derive(HelloMacro)] пишет за тебя:impl HelloMacro for Product { fn hello_macro() { println!("Hello, Macro! My name is {}!", "Product"); }}
fn main() { Product::hello_macro();}Вывод:
Hello, Macro! My name is Product!Один раз написать это вручную — нормально. Писать это для 40 типов — вот тут derive и начинает окупаться.
Шаг 2: #[derive(Describe)] читает поля
Заголовок раздела «Шаг 2: #[derive(Describe)] читает поля»HelloMacro использовал только имя типа. Настоящие derive-макросы, как у serde, смотрят на поля. Давай построим Describe: он перечисляет имена полей структуры, что удобно, например, для заголовка CSV. syn отдаёт поля через ast.data:
Data::Struct,Data::EnumилиData::Union— какой это тип.- Для структуры:
Fields::Named(struct Product { id: u32 }),Fields::Unnamed(struct Meters(f64)) илиFields::Unit(struct Marker;).
Добавь это под первым макросом в hello_macro_derive/src/lib.rs:
#[proc_macro_derive(Describe)]pub fn describe_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); expand_describe(&ast) .unwrap_or_else(|err| err.to_compile_error()) .into()}
fn expand_describe(ast: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> { let name = &ast.ident;
let Data::Struct(data) = &ast.data else { return Err(syn::Error::new_spanned( name, "Describe can only be derived for structs", )); };
let field_names: Vec<String> = match &data.fields { Fields::Named(fields) => fields .named .iter() .map(|field| field.ident.as_ref().unwrap().to_string()) .collect(), Fields::Unit => Vec::new(), Fields::Unnamed(_) => { return Err(syn::Error::new_spanned( name, "Describe needs named fields, like `struct Point { x: i32 }`", )); } };
Ok(quote! { impl hello_macro::Describe for #name { fn type_name() -> &'static str { stringify!(#name) }
fn field_names() -> &'static [&'static str] { &[#(#field_names),*] } } })}Что здесь нового:
- Основная работа делается в обычной вспомогательной функции, которая возвращает
syn::Result. Это позволяет использовать?иreturn Err(...), как в любом другом коде с обработкой ошибок. Функция-макрос наверху просто превращает ошибку в ошибку компиляции. let ... elseсразу обрабатывает случай «это не структура» и оставляет основной путь плоским.#(#field_names),*— это повторение в quote. Он вставляет каждую строку изVecчерез запятую. Идея та же, что у$(...),*вmacro_rules!. КаждаяStringстановится строковым литералом, так что получается&["id", "name", "price_cents"].field.ident.as_ref().unwrap()здесь безопасен: вFields::Namedу каждого поля есть имя.
Теперь используем оба derive-макроса в shop/src/main.rs. Заметь, что use hello_macro::{Describe, HelloMacro} подключает и трейты, и derive-макросы. У них одинаковые имена, но они живут в разных пространствах имён — ровно так работает serde::Serialize.
use hello_macro::{Describe, HelloMacro};
#[derive(HelloMacro, Describe)]struct Product { id: u32, name: String, price_cents: u64,}
#[derive(HelloMacro, Describe)]struct Customer { email: String, city: String,}
fn main() { Product::hello_macro(); Customer::hello_macro();
println!("{} has fields {:?}", Product::type_name(), Product::field_names());
// CSV-файл: заголовок берётся из derive, строки — из настоящих значений. println!("{}", Customer::field_names().join(",")); println!("{},{}", ana.email, ana.city);
let teapot = Product { id: 7, name: "Teapot".to_string(), price_cents: 1999 }; println!("{} #{}: {} costs {} cents", Product::type_name(), teapot.id, teapot.name, teapot.price_cents);}Запусти из папки рабочего пространства:
cargo run -p shopВывод (при сборке с нуля):
Compiling proc-macro2 v1.0.107 Compiling unicode-ident v1.0.27 Compiling quote v1.0.47 Compiling syn v2.0.119 Compiling hello_macro_derive v0.1.0 (/home/you/procmacro-demo/hello_macro_derive) Compiling hello_macro v0.1.0 (/home/you/procmacro-demo/hello_macro) Compiling shop v0.1.0 (/home/you/procmacro-demo/shop) Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.45s Running `target/debug/shop`Hello, Macro! My name is Product!Hello, Macro! My name is Customer!Product has fields ["id", "name", "price_cents"]email,city[email protected],LisbonProduct #7: Teapot costs 1999 centsПосмотри на порядок: Cargo собирает syn и hello_macro_derive раньше shop, потому что компилятору нужно запускать твой макрос, пока он компилирует shop. Если добавить поле в Customer, заголовок CSV сам обновится при следующей сборке.
Понятные сообщения об ошибках
Заголовок раздела «Понятные сообщения об ошибках»Что будет, если кто-то поставит #[derive(Describe)] над перечислением? Поскольку expand_describe возвращает syn::Error, пользователь увидит обычную на вид ошибку компилятора. (Сломанные примеры с этой страницы мы положили в shop/src/bin/broken.rs, поэтому в выводе и появляется этот путь.)
use hello_macro::Describe;
#[derive(Describe)]enum OrderStatus { Pending, Shipped,}error: Describe can only be derived for structs --> shop/src/bin/broken.rs:4:6 |4 | enum OrderStatus { | ^^^^^^^^^^^Это наше собственное сообщение из макроса: «Describe можно вывести только для структур». Хорошим эту ошибку делают две вещи:
syn::Error::new_spanned(name, ...)запоминает, где имя находится в файле пользователя (его span). Поэтому^^^^^указывает наOrderStatus, а не на какую-то строку внутри твоего макроса.to_compile_error()превращает ошибку в вызовcompile_error!("..."). Компилятор сообщает о нём, как о любой другой ошибке. Тот же макросcompile_error!можно использовать и в обычном коде.
Кортежная структура получает второе сообщение («Describe нужны именованные поля»), и оно тоже указывает на правильное место:
error: Describe needs named fields, like `struct Point { x: i32 }` --> shop/src/bin/broken.rs:4:8 |4 | struct Meters(f64); | ^^^^^^Пиши сообщения для того, кто пользуется макросом. Скажи, что не так и что сработает вместо этого. (Мы оставили свои сообщения на английском, как и большинство крейтов, но в своём макросе ты можешь писать их на любом языке.)
Смотрим сгенерированный код с cargo expand
Заголовок раздела «Смотрим сгенерированный код с cargo expand»Макросы могут казаться магией, а магию трудно отлаживать. cargo-expand показывает код после того, как отработали все макросы. Установи его один раз (о других полезных инструментах — на странице Инструменты и установка):
cargo install cargo-expandcargo expand -p shopЧасть того, что он печатает для нашего проекта:
impl hello_macro::Describe for Product { fn type_name() -> &'static str { "Product" } fn field_names() -> &'static [&'static str] { &["id", "name", "price_cents"] }}impl hello_macro::Describe for Customer { fn type_name() -> &'static str { "Customer" } fn field_names() -> &'static [&'static str] { &["email", "city"] }}Вот твой блок impl с подставленными именами полей, а stringify!(#name) уже превратился в "Product". Когда derive делает не то, что ты ждёшь, первым делом запусти cargo expand. Он разворачивает и println!, и другие макросы, так что ищи свои блоки impl.
Тестируем макрос
Заголовок раздела «Тестируем макрос»Крейт с макросом не может легко тестировать сам себя, потому что умеет экспортировать только макросы. Простое решение — писать обычные тесты в крейте, который макрос использует. Положи это в hello_macro/tests/describe.rs и запусти cargo test -p hello_macro:
use hello_macro::Describe;
#[allow(dead_code)]#[derive(Describe)]struct Product { id: u32, name: String,}
#[test]fn lists_field_names_in_order() { assert_eq!(Product::type_name(), "Product"); assert_eq!(Product::field_names(), ["id", "name"]);}running 1 testtest lists_field_names_in_order ... okА как проверить, что для перечисления получается хорошая ошибка? Обычный тест этого не может: тестовый файл просто не скомпилируется. Для этого есть крейт trybuild. Ты кладёшь маленькие файлы, которые «должны не скомпилироваться», в отдельную папку, а trybuild компилирует каждый и сравнивает ошибку компилятора с сохранённым файлом .stderr:
#[test]fn ui() { let t = trybuild::TestCases::new(); t.compile_fail("tests/ui/*.rs");}Большинство популярных крейтов с макросами, включая serde и thiserror, тестируют свои сообщения об ошибках именно так.
Когда стоит писать свой макрос?
Заголовок раздела «Когда стоит писать свой макрос?»Процедурные макросы — мощная штука, но у них есть цена. Будь честен с собой, прежде чем браться:
- Время компиляции. syn — большой крейт. Первая сборка каждого проекта, который использует твой макрос, должна его скомпилировать. (Для derive-макросов хватает фич syn по умолчанию.
features = ["full"]нужны только для разбора вещей вроде тел функций, и с ними сборка медленнее.) - Сложность. Теперь у тебя лишний крейт, код, который пишет код, и ошибки, в которых труднее разобраться.
- Инструменты. Редакторы и rust-analyzer лучше понимают обычный код, чем код из макроса.
Хороший порядок, в котором стоит пробовать варианты:
- Функция, трейт с методом по умолчанию или обобщения. Большинство проблем с «повторяющимся кодом» решаются здесь.
- Макрос
macro_rules!(см. Макросы). Ему не нужен отдельный крейт. - Процедурный макрос — когда код зависит от структуры типа (его имени, полей или вариантов), он нужен многим типам, и пользоваться им будут другие люди. Поэтому serde, thiserror и clap и используют derive.
Для сравнения вот Describe, сделанный через macro_rules!. Он работает и не требует отдельного крейта, но структуру приходится писать внутри вызова макроса. Это быстро становится неудобным, например если тебе нужны ещё doc-комментарии, поля pub или другие derive.
trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str];}
macro_rules! describe_struct { (struct $name:ident { $($field:ident : $ty:ty),* $(,)? }) => { struct $name { $($field: $ty),* }
impl Describe for $name { fn type_name() -> &'static str { stringify!($name) } fn field_names() -> &'static [&'static str] { &[$(stringify!($field)),*] } } };}
describe_struct!(struct Customer { email: String, city: String });
fn main() { println!("{} has fields {:?}", Customer::type_name(), Customer::field_names()); println!("{},{}", ana.email, ana.city);}Вывод:
Customer has fields ["email", "city"][email protected],LisbonДля одного-двух типов в своей программе этот вариант (или просто impl, написанный вручную) часто лучше. А derive удобнее для тех, кто им пользуется: их структура остаётся обычной, а добавить нужно всего одну строку.
Частые ошибки
Заголовок раздела «Частые ошибки»Забыть proc-macro = true. Без секции [lib] в Cargo.toml крейта с макросом он остаётся обычной библиотекой, и компилятор отказывается работать:
error: the `#[proc_macro_derive]` attribute is only usable with crates of the `proc-macro` crate type --> hello_macro_derive/src/lib.rs:5:1 |5 | #[proc_macro_derive(HelloMacro)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0432]: unresolved import `proc_macro`Компилятор говорит, что атрибут #[proc_macro_derive] можно использовать только в крейтах типа proc-macro, и не находит proc_macro.
Использовать panic! или unwrap() для ошибок пользователя. Это «работает», но ошибка получается расплывчатой и указывает на весь #[derive], а не на проблему:
error: proc-macro derive panicked --> shop/src/bin/broken.rs:3:10 |3 | #[derive(Describe)] | ^^^^^^^^ | = help: message: Describe can only be derived for structsВместо этого возвращай syn::Error с new_spanned, как на шаге 2.
Короткие пути в сгенерированном коде. Если написать impl Describe for #name вместо impl hello_macro::Describe for #name, это сработает, только если пользователь случайно импортировал Describe. А тот, кто пишет #[derive(hello_macro::Describe)], получит вот что:
error[E0405]: cannot find trait `Describe` in this scope --> shop/src/bin/broken.rs:1:10 |1 | #[derive(hello_macro::Describe)] | ^^^^^^^^^^^^^^^^^^^^^ not found in this scope | = note: this error originates in the derive macro `hello_macro::Describe`Компилятор не находит трейт Describe в этой области видимости, а заметка подсказывает, что ошибка пришла из derive-макроса. Всегда используй полные пути в quote!: hello_macro::Describe, ::std::fmt::Display, ::core::option::Option.
Практика
Заголовок раздела «Практика»Эти упражнения меняют настоящий проект, поэтому кнопки «Попробуй сам» здесь нет. Собери проект с этой страницы на своём компьютере и запускай cargo run -p shop.
1. Считаем поля
Заголовок раздела «1. Считаем поля»Добавь третий метод, fn field_count() -> usize, в трейт Describe и в derive. Product::field_count() должен возвращать 3. Подсказка: вектор field_names у тебя уже есть. Сохрани field_names.len() в переменную до quote! и вставь её через #.
Показать решение
В hello_macro/src/lib.rs добавь метод в трейт:
pub trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str]; fn field_count() -> usize;}В expand_describe посчитай поля и добавь метод в сгенерированный impl:
let count = field_names.len();
Ok(quote! { impl hello_macro::Describe for #name { fn type_name() -> &'static str { stringify!(#name) }
fn field_names() -> &'static [&'static str] { &[#(#field_names),*] }
fn field_count() -> usize { #count } } })Тогда println!("{} has {} fields", Product::type_name(), Product::field_count()); в main печатает:
Product has 3 fieldsПодсчёт происходит в твоём макросе, во время компиляции. cargo expand показывает, что в результате получается просто fn field_count() -> usize { 3usize } — константа, и во время работы программы делать уже нечего.
2. Поддержка обобщённых структур
Заголовок раздела «2. Поддержка обобщённых структур»Попробуй #[derive(Describe)] на обобщённой структуре вроде struct Page<T> { items: Vec<T>, number: u32 }. Она падает с error[E0107]: missing generics for struct Page, потому что сгенерированный код говорит impl hello_macro::Describe for Page и забывает <T>. Исправь derive так, чтобы обобщённые структуры работали. Подсказка: ast.generics.split_for_impl() даёт три кусочка, которые нужно вставить в строку impl. Как выглядит impl<T>, напомнит страница Обобщения.
Показать решение
В expand_describe раздели обобщённые параметры и используй все три кусочка в строке impl:
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
Ok(quote! { impl #impl_generics hello_macro::Describe for #name #ty_generics #where_clause { fn type_name() -> &'static str { stringify!(#name) }
fn field_names() -> &'static [&'static str] { &[#(#field_names),*] } } })Теперь в shop работает вот это:
#[derive(Describe)]struct Page<T> { items: Vec<T>, number: u32,}
fn main() { let page = Page { items: vec!["a", "b"], number: 1 }; println!("{} {} {:?}", page.items.len(), page.number, Page::<String>::field_names());}2 1 ["items", "number"]impl_generics — это <T> (со всеми ограничениями), ty_generics — <T>, а where_clause — часть where ..., если она есть. Так делает каждый серьёзный derive, так что запомни split_for_impl.
- Процедурный макрос — это функция на Rust, которая выполняется во время компиляции:
TokenStreamна входе,TokenStreamна выходе. - Видов три: derive (
#[derive(Serialize)]), атрибутный (#[tokio::main]) и функциональный (sql!(...)). - Они живут в отдельном крейте с
proc-macro = true. Основной крейт обычно реэкспортирует их рядом с трейтами. - syn разбирает входные данные (
DeriveInput,Data,Fields), а quote пишет результат (#name,#(...),*). - Сообщай об ошибках пользователя через
syn::Error::new_spanned(...).to_compile_error(), проверяй результат черезcargo expandи тестируй ошибки с trybuild. - Сначала попробуй функцию, трейт или
macro_rules!. Процедурный макрос оправдан, когда многим типам нужен код, зависящий от их структуры.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Когда выполняется код процедурного макроса?
2. К какому виду процедурных макросов относится #[tokio::main]?
3. Что делают syn и quote?
4. Твой derive должен отвергать перечисления. Как лучше всего это сделать?
Результат: 0 / 4
Что почитать
Заголовок раздела «Что почитать»- Процедурные макросы для генерации кода из атрибутов — книга «Язык программирования Rust» (перевод на русский): официальный разбор derive-макроса
HelloMacro, а также атрибутных и функциональных макросов. - Procedural Macros — The Rust Reference (на английском): точные правила для трёх видов, типы крейтов, span и гигиена.
- Документация syn (на английском):
DeriveInput,Data,Fields,Errorи функции разбора, которые используются на этой странице. - Документация quote (на английском): как работают подстановка
#varи повторение#(...),*внутриquote!. - proc-macro-workshop (на английском): бесплатные практические проекты Дэвида Толнея (derive для builder, derive для debug и другие) с тестами, которые ведут тебя шаг за шагом.