Перейти к содержимому

Процедурные макросы в 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 и крейты.)

Строки, которые печатают программы этого проекта, остались на английском: весь вывод ниже — настоящий результат сборки и запуска именно этого кода.

Terminal window
mkdir procmacro-demo && cd procmacro-demo
cargo new --lib hello_macro
cargo new --lib hello_macro_derive
cargo 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:

hello_macro_derive/Cargo.toml
[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:

hello_macro/Cargo.toml
[dependencies]
hello_macro_derive = { version = "0.1.0", path = "../hello_macro_derive" }
shop/Cargo.toml
[dependencies]
hello_macro = { version = "0.1.0", path = "../hello_macro" }

Начнём с крошечного. Мы хотим, чтобы любой тип с #[derive(HelloMacro)] получал функцию hello_macro(), которая печатает имя типа. Сначала трейт, в hello_macro/src/lib.rs. Последняя строка реэкспортирует derive-макросы, чтобы пользователям был нужен только этот один крейт. Ровно так делает serde.

hello_macro/src/lib.rs
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:

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-макросе:

  1. parse_macro_input! превращает токены в DeriveInput. ast.ident — имя типа, например Product. Если разбор не удался, этот макрос сам вернёт нормальную ошибку компиляции.
  2. quote! собирает блок impl. #name вставляет имя. stringify!(#name) превращает имя в текст "Product".
  3. .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 и начинает окупаться.

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.

shop/src/main.rs
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, строки — из настоящих значений.
let ana = Customer { email: "[email protected]".to_string(), city: "Lisbon".to_string() };
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);
}

Запусти из папки рабочего пространства:

Terminal window
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
Product #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 показывает код после того, как отработали все макросы. Установи его один раз (о других полезных инструментах — на странице Инструменты и установка):

Terminal window
cargo install cargo-expand
cargo 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 test
test 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 лучше понимают обычный код, чем код из макроса.

Хороший порядок, в котором стоит пробовать варианты:

  1. Функция, трейт с методом по умолчанию или обобщения. Большинство проблем с «повторяющимся кодом» решаются здесь.
  2. Макрос macro_rules! (см. Макросы). Ему не нужен отдельный крейт.
  3. Процедурный макрос — когда код зависит от структуры типа (его имени, полей или вариантов), он нужен многим типам, и пользоваться им будут другие люди. Поэтому 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() {
let ana = Customer { email: "[email protected]".to_string(), city: "Lisbon".to_string() };
println!("{} has fields {:?}", Customer::type_name(), Customer::field_names());
println!("{},{}", ana.email, ana.city);
}

Вывод:

Customer has fields ["email", "city"]
▶ Попробуй сам

Для одного-двух типов в своей программе этот вариант (или просто 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.

Добавь третий метод, 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 } — константа, и во время работы программы делать уже нечего.

Попробуй #[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