Ir al contenido

Macros procedurales en Rust: escribe tu propio #[derive]

Cada vez que escribes #[derive(Debug)] o #[derive(Serialize)], un pequeño programa se ejecuta dentro del compilador. Mira tu struct y escribe un bloque impl por ti. Ese pequeño programa es una macro procedural (procedural macro), y tú también puedes escribir las tuyas.

Imagínalo como un empleado muy servicial. Le entregas un formulario (tu struct), él lee cada campo y te devuelve todo el papeleo aburrido ya rellenado. En la página de macros conociste macro_rules!, que funciona comparando patrones. Las macros procedurales son distintas: son funciones normales de Rust que reciben código y devuelven código. En esta página vas a construir una de verdad, paso a paso, y verás la salida real.

Hay tres tipos. Por dentro funcionan igual, pero quien las usa las ve de forma distinta:

Tipo Lo que escribe quien la usa Ejemplos famosos
Macro derive #[derive(Serialize)] sobre un struct o un enum Serialize de serde, Error de thiserror, Parser de clap
Macro de atributo #[route(GET, "/")] sobre una función u otro elemento #[tokio::main], #[get("/")] de actix-web
Macro tipo función sql!(SELECT * FROM users) query! de sqlx, html! de yew
  • Una macro derive solo añade código junto a tu tipo. Tu struct se queda exactamente como lo escribiste.
  • Una macro de atributo reemplaza el elemento sobre el que está. #[tokio::main] toma tu async fn main y la convierte en una fn main normal que arranca el runtime de tokio.
  • Una macro tipo función parece una llamada a macro_rules!, pero es una función de Rust la que decide qué significa la entrada. Así es como sqlx::query! puede revisar tu SQL en tiempo de compilación.

Ya usas macros derive de otros crates. Aquí tienes dos haciendo trabajo de verdad. Serialize escribe el código para el JSON (mira JSON con Serde) y Error escribe el código de Display a partir del texto de #[error(...)] (mira anyhow y thiserror):

use serde::Serialize;
use thiserror::Error;
#[derive(Serialize)]
struct Order {
id: u32,
item: String,
quantity: u8,
}
#[derive(Debug, Error)]
enum ShopError {
#[error("el artículo {0} está agotado")]
OutOfStock(String),
}
fn main() {
let order = Order { id: 42, item: "Tetera".to_string(), quantity: 2 };
println!("{}", serde_json::to_string(&order).unwrap());
let err = ShopError::OutOfStock("Tetera".to_string());
println!("{err}");
}

Salida:

{"id":42,"item":"Tetera","quantity":2}
el artículo Tetera está agotado
▶ Pruébalo tú mismo

Nunca escribiste un impl Display ni nada de código JSON. Los derive lo escribieron mientras tu programa se compilaba.

Cómo funcionan: tokens que entran, tokens que salen

Sección titulada «Cómo funcionan: tokens que entran, tokens que salen»

Lo primero que hace el compilador es cortar tu código fuente en tokens: nombres, signos de puntuación, literales, paréntesis y llaves. Una macro procedural es una función que recibe esos tokens como un TokenStream y devuelve un TokenStream nuevo. Después, el compilador compila lo que devolviste, como si lo hubieras escrito tú.

Estas son las tres formas de función. Cada una se marca con un atributo para que el compilador sepa de qué tipo es:

use proc_macro::TokenStream;
// #[derive(HelloMacro)]: recibe el struct o enum y devuelve código para AÑADIR.
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream { /* ... */ }
// #[route(GET, "/")]: recibe los tokens del propio atributo y el elemento, y devuelve un REEMPLAZO.
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { /* ... */ }
// sql!(...): recibe todo lo que va entre los paréntesis y devuelve el reemplazo.
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream { /* ... */ }

Un TokenStream en bruto es solo una lista de tokens, y trabajar con eso es difícil. Por eso casi todas las macros procedurales usan tres crates de ayuda:

  • syn convierte los tokens en un árbol de sintaxis. En lugar de tokens, recibes un DeriveInput con el nombre del tipo, sus genéricos y sus campos.
  • quote convierte código con aspecto de Rust otra vez en tokens. Dentro de quote! { ... } escribes Rust normal, y #name pega el valor de una variable.
  • proc-macro2 es un envoltorio sobre los tipos proc_macro del compilador, y es lo que usan syn y quote. También funciona fuera del compilador, lo que facilita las funciones de ayuda y los tests.

Una idea importante: tu macro se ejecuta en tiempo de compilación. Cargo compila primero el crate de la macro, para tu propio ordenador, y el compilador la ejecuta mientras construye el crate que la usa. Cuando por fin se ejecuta el programa, la macro ya no está. Solo queda el código que escribió.

Una macro procedural tiene que vivir en su propio crate, con un tipo de crate especial. Ese crate solo puede exportar macros, no funciones ni traits normales. Por eso lo habitual son dos crates uno al lado del otro, como serde y serde_derive: uno para el trait y otro para el derive. Nosotros añadiremos un tercer crate, un pequeño programa llamado shop que los usa. (¿Necesitas repasar los crates y los workspaces? Mira Cargo y crates.)

Terminal window
mkdir procmacro-demo && cd procmacro-demo
cargo new --lib hello_macro
cargo new --lib hello_macro_derive
cargo new shop

Este es el árbol de archivos terminado:

procmacro-demo/
├── Cargo.toml (el workspace)
├── hello_macro/
│ ├── Cargo.toml
│ └── src/lib.rs (los traits y la reexportación de los derive)
├── hello_macro_derive/
│ ├── Cargo.toml (proc-macro = true)
│ └── src/lib.rs (las macros en sí)
└── shop/
├── Cargo.toml
└── src/main.rs (un programa que usa #[derive(...)])

El Cargo.toml de la carpeta principal une los tres crates en un solo workspace:

[workspace]
members = ["hello_macro", "hello_macro_derive", "shop"]
resolver = "2"

El crate de la macro es el único que tiene algo especial: proc-macro = true. Además depende de syn, quote y 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"

Los otros dos crates son normales. hello_macro depende del crate del derive, y shop solo necesita 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" }

Empecemos con algo muy pequeño. Queremos que cualquier tipo con #[derive(HelloMacro)] reciba una función hello_macro() que imprima el nombre del tipo. Primero el trait, en hello_macro/src/lib.rs. La última línea reexporta las macros derive, así quien las use solo necesita este crate. Es exactamente lo que hace 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];
}
// Reexporta las macros derive, así quien las use solo necesita este crate.
pub use hello_macro_derive::{Describe, HelloMacro};

(Describe es para el paso 2.) Ahora la macro en sí, en 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 y Fields: paso 2
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 1. Convierte los tokens en un árbol de sintaxis que podemos examinar.
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
// 2. Construye el código nuevo con quote!.
let generated = quote! {
impl hello_macro::HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
// 3. Devuelve los tokens nuevos al compilador.
generated.into()
}

Tres pasos, y los vas a ver en casi todas las macros derive:

  1. parse_macro_input! convierte los tokens en un DeriveInput. ast.ident es el nombre del tipo, por ejemplo Product. Si el análisis falla, esta macro devuelve por ti un error de compilación como es debido.
  2. quote! construye el bloque impl. #name pega el nombre. stringify!(#name) convierte el nombre en el texto "Product".
  3. .into() convierte el proc_macro2::TokenStream de quote en el proc_macro::TokenStream del compilador.

Fíjate en la ruta completa hello_macro::HelloMacro. El código generado se pega en el archivo de quien usa la macro, así que no puede depender de las líneas use que esa persona tenga.

Cuando alguien escribe #[derive(HelloMacro)] struct Product, el compilador añade código que funciona igual que esta versión escrita a mano. Esta sí se puede ejecutar en el Playground, porque no hay ningún crate de macros de por medio (dejamos el texto en inglés para que coincida con el proyecto de ejemplo):

trait HelloMacro {
fn hello_macro();
}
struct Product;
// Esto es lo que #[derive(HelloMacro)] escribe por ti:
impl HelloMacro for Product {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", "Product");
}
}
fn main() {
Product::hello_macro();
}

Salida:

Hello, Macro! My name is Product!
▶ Pruébalo tú mismo

Escribir esto a mano una vez no es problema. Escribirlo para 40 tipos es cuando un derive empieza a valer la pena.

HelloMacro solo usaba el nombre del tipo. Los derive de verdad, como los de serde, miran los campos. Vamos a construir Describe: devuelve los nombres de los campos de un struct, algo muy práctico para la cabecera de un CSV. syn te da los campos a través de ast.data:

  • Data::Struct, Data::Enum o Data::Union: qué clase de tipo es.
  • Para un struct, Fields::Named (struct Product { id: u32 }), Fields::Unnamed (struct Meters(f64)) o Fields::Unit (struct Marker;).

Añade esto debajo de la primera macro, en 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),*]
}
}
})
}

Las piezas nuevas:

  • El trabajo de verdad ocurre en una función de ayuda normal que devuelve syn::Result. Así puedes usar ? y return Err(...) como en cualquier otro código de manejo de errores. La función de la macro, arriba del todo, solo convierte un error en un error de compilación.
  • let ... else resuelve el caso “no es un struct” enseguida y mantiene plano el camino normal.
  • #(#field_names),* es la repetición de quote. Pega cada cadena del Vec, separadas por comas. Es la misma idea que $(...),* en macro_rules!. Cada String se convierte en un literal de cadena, así que esto queda como &["id", "name", "price_cents"].
  • field.ident.as_ref().unwrap() es seguro aquí: en Fields::Named, todos los campos tienen nombre.

Ahora usa los dos derive en shop/src/main.rs. Fíjate en que use hello_macro::{Describe, HelloMacro} trae los traits y las macros derive. Comparten nombre, pero viven en espacios de nombres distintos, que es exactamente como funciona 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());
// Un archivo CSV: la cabecera sale del derive y las filas, de valores reales.
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);
}

Ejecútalo desde la carpeta del workspace:

Terminal window
cargo run -p shop

Salida (en una compilación desde cero):

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

Las líneas de Cargo (Compiling, Finished, Running) son lo que imprime Cargo, siempre en inglés. Fíjate en el orden: Cargo construye syn y hello_macro_derive antes que shop, porque el compilador necesita ejecutar tu macro mientras compila shop. Si añades un campo a Customer, la cabecera del CSV se actualiza sola la próxima vez que compiles.

¿Qué pasa si alguien pone #[derive(Describe)] sobre un enum? Como expand_describe devuelve un syn::Error, quien usa la macro recibe un error del compilador de aspecto normal. (Pusimos los ejemplos rotos de esta página en shop/src/bin/broken.rs; por eso aparece esa ruta.)

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 {
| ^^^^^^^^^^^

El mensaje es el texto que escribimos en la macro (“Describe solo se puede derivar para structs”). Dos cosas hacen que este error sea bueno:

  • syn::Error::new_spanned(name, ...) recuerda dónde está el nombre en el archivo del usuario (su span, la posición en el código). Por eso los ^^^^^ señalan OrderStatus y no alguna línea dentro de tu macro.
  • to_compile_error() convierte el error en una llamada compile_error!("..."). Después, el compilador lo informa como cualquier otro error. Puedes usar esa misma macro compile_error! en código normal también.

Un tuple struct recibe el otro mensaje (“Describe necesita campos con nombre”), que también señala el lugar correcto:

error: Describe needs named fields, like `struct Point { x: i32 }`
--> shop/src/bin/broken.rs:4:8
|
4 | struct Meters(f64);
| ^^^^^^

Escribe tus mensajes pensando en la persona que usa la macro. Di qué está mal y qué funcionaría en su lugar.

Las macros pueden parecer magia, y la magia es difícil de depurar. cargo-expand te muestra el código después de que se hayan ejecutado todas las macros. Instálalo una vez (en Herramientas de Rust tienes más herramientas útiles):

Terminal window
cargo install cargo-expand
cargo expand -p shop

Parte de lo que imprime para nuestro proyecto:

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"]
}
}

Ahí está tu bloque impl, con los nombres de los campos ya puestos y stringify!(#name) convertido en "Product". Cuando un derive no hace lo que esperas, ejecuta cargo expand primero. También expande println! y otras macros, así que busca tus propios bloques impl.

Un crate de macros no puede probarse a sí mismo con facilidad, porque solo puede exportar macros. La solución sencilla es escribir tests normales en un crate que use la macro. Pon esto en hello_macro/tests/describe.rs y ejecuta 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

Pero ¿cómo compruebas que el caso del enum da un buen error? Un test normal no puede, porque el archivo del test no compilaría. Para eso existe el crate trybuild. Pones pequeños archivos que “deben fallar” en una carpeta, y trybuild compila cada uno y compara el error del compilador con un archivo .stderr guardado:

#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/*.rs");
}

La mayoría de los crates de macros populares, incluidos serde y thiserror, prueban sus mensajes de error así.

Las macros procedurales son potentes, pero tienen un coste. Sé honesto contigo antes de escribir una:

  • Tiempo de compilación. syn es un crate grande. La primera compilación de cada proyecto que use tu macro tiene que compilarlo. (Las features por defecto de syn bastan para los derive. Solo necesitas features = ["full"] para analizar cosas como el cuerpo de una función, y eso lo hace más lento.)
  • Complejidad. Ahora tienes un crate más, código que escribe código y errores más difíciles de seguir.
  • Herramientas. Los editores y rust-analyzer entienden mejor el código normal que el código que sale de una macro.

Un buen orden para probar opciones:

  1. Una función, un trait con un método por defecto o genéricos. La mayoría de los problemas de “código repetitivo” se resuelven aquí.
  2. Una macro macro_rules! (mira Macros en Rust). No necesita ningún crate extra.
  3. Una macro procedural, cuando el código depende de la estructura de un tipo (su nombre, sus campos o sus variantes), muchos tipos lo necesitan y otras personas la van a usar. Por eso serde, thiserror y clap usan derive.

Para comparar, aquí tienes Describe hecho con macro_rules!. Funciona y no necesita un crate extra, pero el struct tiene que escribirse dentro de la llamada a la macro. Eso se vuelve incómodo muy rápido, por ejemplo si además quieres comentarios de documentación, campos pub u otros 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);
}

Salida:

Customer has fields ["email", "city"]
▶ Pruébalo tú mismo

Para uno o dos tipos dentro de tu propio programa, esto (o simplemente escribir el impl a mano) suele ser la mejor opción. Un derive es más cómodo para quien lo usa: conserva su struct normal y solo añade una línea.

Olvidar proc-macro = true. Sin la sección [lib] en el Cargo.toml del crate de la macro, el crate es una biblioteca normal y el compilador se niega:

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`

El compilador te dice que #[proc_macro_derive] solo se puede usar en crates del tipo proc-macro, y por eso tampoco encuentra el crate proc_macro.

Usar panic! o unwrap() para los errores del usuario. “Funciona”, pero el error es poco claro y señala todo el #[derive], no el problema:

error: proc-macro derive panicked
--> shop/src/bin/broken.rs:3:10
|
3 | #[derive(Describe)]
| ^^^^^^^^
|
= help: message: Describe can only be derived for structs

Aquí el compilador solo dice que la macro derive hizo panic y muestra el mensaje como una ayuda. Mejor devuelve un syn::Error con new_spanned, como en el paso 2.

Rutas cortas en el código generado. Si escribes impl Describe for #name en lugar de impl hello_macro::Describe for #name, solo funciona cuando quien usa la macro ha importado Describe por casualidad. Quien escriba #[derive(hello_macro::Describe)] recibe esto:

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`

Es decir: no encuentra el trait Describe en ese ámbito (scope). Usa siempre rutas completas en quote!: hello_macro::Describe, ::std::fmt::Display, ::core::option::Option.

Estos ejercicios modifican el proyecto real, así que no tienen botón de “Pruébalo tú mismo”. Construye el proyecto de esta página en tu propio ordenador y ejecuta cargo run -p shop.

Añade un tercer método, fn field_count() -> usize, al trait Describe y al derive. Product::field_count() debería devolver 3. Pista: ya tienes el vector field_names. Guarda field_names.len() en una variable antes de quote! y pégala con #.

Ver solución

En hello_macro/src/lib.rs, añade el método al trait:

pub trait Describe {
fn type_name() -> &'static str;
fn field_names() -> &'static [&'static str];
fn field_count() -> usize;
}

En expand_describe, cuenta los campos y añade el método al impl generado:

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
}
}
})

Luego, println!("{} has {} fields", Product::type_name(), Product::field_count()); en main imprime:

Product has 3 fields

El conteo ocurre en tu macro, en tiempo de compilación. cargo expand muestra que el resultado es simplemente fn field_count() -> usize { 3usize }, una constante sin ningún trabajo pendiente en tiempo de ejecución.

Prueba #[derive(Describe)] sobre un struct genérico como struct Page<T> { items: Vec<T>, number: u32 }. Falla con error[E0107]: missing generics for struct Page (faltan los genéricos), porque el código generado dice impl hello_macro::Describe for Page y se olvida del <T>. Arregla el derive para que funcione con structs genéricos. Pista: ast.generics.split_for_impl() te da tres piezas para pegar en la línea del impl. En Genéricos puedes repasar cómo se ve impl<T>.

Ver solución

En expand_describe, separa los genéricos y usa las tres piezas en la línea del 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),*]
}
}
})

Ahora esto funciona en 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 es <T> (con sus bounds, si los tiene), ty_generics es <T> y where_clause es la parte where ..., si existe. Todo derive serio hace esto, así que recuerda split_for_impl.

  • Una macro procedural es una función de Rust que se ejecuta en tiempo de compilación: entra un TokenStream y sale otro TokenStream.
  • Hay tres tipos: derive (#[derive(Serialize)]), de atributo (#[tokio::main]) y tipo función (sql!(...)).
  • Viven en su propio crate con proc-macro = true. Normalmente un crate principal las reexporta junto a los traits.
  • syn analiza la entrada (DeriveInput, Data, Fields) y quote escribe la salida (#name, #(...),*).
  • Informa de los errores del usuario con syn::Error::new_spanned(...).to_compile_error(), revisa el resultado con cargo expand y prueba los errores con trybuild.
  • Prueba primero una función, un trait o macro_rules!. Una macro procedural vale la pena cuando muchos tipos necesitan código basado en su estructura.

Comprueba lo aprendido

1. ¿Cuándo se ejecuta el código de una macro procedural?

2. ¿Qué tipo de macro procedural es #[tokio::main]?

3. ¿Qué hacen syn y quote?

4. Tu derive debe rechazar los enums. ¿Cuál es la mejor forma?

Puntuación: 0 / 4