wandres.dev
MACROS PROCEDURALES · derive y attribute

Un derive macro paso a paso: generar un getter por campo

Construimos de principio a fin un #[derive(Getters)] que, dado un struct, genera un método de acceso por cada campo. Parseamos el DeriveInput, extraemos los campos nombrados con syn, iteramos con quote! para emitir un getter por cada uno y cerramos con split_for_impl para respetar los genéricos. Código real, compilable, comentado línea a línea.

⏱ 20 min

La teoría se asienta cuando construyes una macro completa. Vamos a escribir #[derive(Getters)]: un derive que, colocado sobre un struct de campos privados, genera automáticamente un método de acceso pub fn campo(&self) -> &Tipo por cada uno de ellos. Es el ejemplo canónico porque toca todas las piezas —parsear el DeriveInput, ramificar según la forma del dato, iterar sobre los campos, interpolar identificadores y tipos, respetar los genéricos— y sin embargo cabe en una pantalla. Al terminar tendrás una macro real y compilable, y habrás visto el patrón que se repite en derive_builder, en los getters de innumerables crates y en buena parte de los derives que usas a diario sin pensar en lo que generan.

🎯 Al terminar esta lección sabrás
  • Parsear un DeriveInput y extraer sus campos nombrados navegando Data y Fields.
  • Iterar los campos con .map y quote! para sintetizar un método por cada uno.
  • Interpolar el identificador y el tipo de cada campo, y desplegar la lista con #( ... )*.
  • Cerrar la generación respetando los genéricos del tipo con generics.split_for_impl().

El objetivo y la estructura del crate

Queremos que este uso funcione, sin que el autor del struct escriba un solo getter a mano:

#[derive(Getters)]
struct Usuario {
    nombre: String,
    edad: u32,
}

let u = Usuario { nombre: "Ada".into(), edad: 36 };
assert_eq!(u.nombre(), "Ada");   // metodo generado por la macro
assert_eq!(*u.edad(), 36);       // metodo generado por la macro

La macro vive en un crate aparte de tipo proc-macro, con syn y quote como dependencias:

[lib]
proc-macro = true

[dependencies]
syn = "2"
quote = "1"
proc-macro2 = "1"

Paso a paso: parsear, extraer, iterar, emitir

Este es el cuerpo completo de la macro. Léelo entero una vez y luego lo desmenuzamos.

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};

#[proc_macro_derive(Getters)]
pub fn derivar_getters(input: TokenStream) -> TokenStream {
    // 1. PARSEAR: los tokens del struct anotado se elevan a un AST tipado.
    let ast = parse_macro_input!(input as DeriveInput);
    let nombre = &ast.ident;

    // 2. EXTRAER: solo soportamos structs con campos nombrados.
    let campos = match &ast.data {
        Data::Struct(datos) => match &datos.fields {
            Fields::Named(nombrados) => &nombrados.named,
            _ => panic!("Getters exige un struct con campos nombrados"),
        },
        _ => panic!("Getters solo se aplica a structs"),
    };

    // 3. ITERAR: un fragmento de codigo (un getter) por cada campo.
    let getters = campos.iter().map(|campo| {
        let ident = campo.ident.as_ref().unwrap(); // el nombre del campo
        let tipo = &campo.ty;                       // el tipo del campo
        quote! {
            pub fn #ident(&self) -> &#tipo {
                &self.#ident
            }
        }
    });

    // 4. RESPETAR GENERICOS: descomponer <T>, <T: Bound> y where.
    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();

    // 5. EMITIR: envolver todos los getters en un solo impl.
    let expandido = quote! {
        impl #impl_generics #nombre #ty_generics #where_clause {
            #( #getters )*
        }
    };

    expandido.into()
}

Parsear. parse_macro_input!(input as DeriveInput) convierte los tokens en un DeriveInput, la estructura que syn reserva para lo que decora un derive. Si el usuario aplicara el derive a algo que no parsea como item derivable, syn emitiría el error por nosotros. De ahí sacamos ast.ident, el nombre del tipo.

Extraer. Un DeriveInput puede envolver un struct, un enum o una union: por eso ast.data es un enum Data de tres variantes. Descendemos hasta Data::Struct, y dentro, hasta Fields::Named, que es la lista de campos con nombre. Si nos dan un enum o un struct de tupla, fallamos con un mensaje claro —más adelante veremos cómo hacerlo con elegancia—.

Iterar. Aquí está el corazón. campos.iter().map(...) recorre cada campo y, para cada uno, invoca quote! para fabricar un fragmento de código: la definición de un método cuyo nombre es el del campo (#ident), que devuelve una referencia a su tipo (&#tipo) y cuyo cuerpo presta el campo (&self.#ident). El resultado es un iterador de TokenStream, uno por campo.

Emitir. El quote! final envuelve todo en un impl y despliega la colección de getters con la repetición #( #getters )*, que expande el iterador entero, un método tras otro. Convertimos con .into() y devolvemos.

⚠️
El panic! en una macro es un error de compilación, pero es tosco

Fallar con panic! funciona —recuerda que la macro corre en el compilador, así que su panic! se convierte en un error de build—, pero el mensaje sale sin señalar el punto exacto del código del usuario. La forma profesional es construir un syn::Error con un span y convertirlo con .to_compile_error(), de modo que el error aparezca subrayando el struct culpable. Para una primera versión, panic! enseña la idea; para una macro publicada, devuelve errores con span.

Por qué split_for_impl no es opcional

Es tentador escribir impl #nombre { ... } a secas, y funcionará mientras el struct no tenga genéricos. En cuanto alguien escriba struct Caja<T> { valor: T }, esa versión ingenua generaría impl Caja { ... } —sin el <T>— y no compilaría. generics.split_for_impl() resuelve esto devolviendo las tres piezas que un impl genérico necesita, cada una en su sitio: impl_generics es el <T: Bound> que va tras impl, ty_generics es el <T> que va tras el nombre del tipo, y where_clause es la cláusula where si la hubiera. Colocadas en la plantilla, el impl generado es correcto para cualquier struct, tenga los genéricos que tenga.

#[derive(Getters)]
struct Caja<T> {
    valor: T,
}

// La macro genera, gracias a split_for_impl:
// impl<T> Caja<T> {
//     pub fn valor(&self) -> &T { &self.valor }
// }
flowchart TD
A[derive Getters sobre el struct] --> B[parse_macro_input a DeriveInput]
B --> C[Descender Data Struct y Fields Named]
C --> D[map sobre cada campo]
D --> E[quote genera un getter por campo]
E --> F[split_for_impl aporta los genericos]
F --> G[quote envuelve todo en un impl]
G --> H[El struct gana sus metodos de acceso]
style B fill:#89b4fa,color:#11111b
style E fill:#cba6f7,color:#11111b
style F fill:#f9e2af,color:#11111b
style H fill:#a6e3a1,color:#11111b
Un derive es una función de la forma de un tipo a su comportamiento

Da un paso atrás y mira lo que acabas de construir con la distancia adecuada, porque encierra la esencia de por qué los derives son tan centrales en Rust. Nuestra macro nunca supo, ni le importó, qué campos concretos tendría el struct: no está escrita para Usuario ni para Caja, sino para la forma de un struct cualquiera. Recibe una estructura —cuántos campos, cómo se llaman, de qué tipo son— y de esa forma deduce un comportamiento, un método por campo, generado mecánicamente. Es, en el sentido más literal, una función cuyo dominio son las estructuras de datos y cuyo codominio son las implementaciones: le das un tipo y te devuelve código adaptado a él. Ahí reside la potencia que macro_rules! no alcanza. Una macro declarativa empareja patrones sintácticos, pero no puede interrogar la semántica: no puede recorrer los campos, leer sus tipos, contar cuántos hay, ramificar según su forma. Nuestra macro sí, porque syn le dio un AST donde el struct es un dato inspeccionable, y quote le dio la capacidad de sintetizar una respuesta proporcional a lo que encontró. Por eso el mismo esqueleto —parsear el DeriveInput, descender a los campos, iterar con map, emitir con quote, cerrar con split_for_impl— reaparece idéntico en un derive que genera un builder, en uno que serializa a JSON, en uno que deriva PartialEq: cambia lo que lees de cada campo y lo que emites por cada campo, pero la maquinaria de “recorrer la forma y generar comportamiento” es una sola. Cuando escribes #[derive(Serialize)] sobre un struct de veinte campos y aparecen, gratis, veinte líneas de serialización correctas, estás cobrando exactamente este dividendo: una función que, en tiempo de compilación, leyó la forma de tu tipo y escribió por ti el código que le corresponde. Interioriza el derive como esa función —de la estructura al comportamiento— y habrás entendido no una macro, sino la clase entera.

📝
Lo esencial del derive paso a paso

Un derive sigue cinco movimientos: parsear con parse_macro_input!(input as DeriveInput); extraer los campos descendiendo Data::Struct y Fields::Named; iterar con .map generando un fragmento por campo con quote!, interpolando #ident y #tipo; respetar los genéricos con generics.split_for_impl(), que da las tres piezas del impl; y emitir envolviendo todo en un impl con la repetición #( ... )* y convirtiendo con .into(). Sin split_for_impl la macro falla en cuanto el struct tiene un parámetro genérico. Para errores de calidad, prefiere syn::Error con span a panic!. Ese esqueleto es común a todos los derives del ecosistema.

⚔️ Extiende el derive Getters
  1. Compila la macro tal cual y verifica los assert_eq! del ejemplo Usuario; luego añade un tercer campo y comprueba que aparece su getter sin tocar la macro.
  2. Aplica #[derive(Getters)] a struct Caja<T> { valor: T } y confirma que split_for_impl genera un impl<T> Caja<T> correcto; después quítalo y observa el error para entender qué aporta.
  3. Sustituye el panic! por un syn::Error::new_spanned(...).to_compile_error() para que aplicar el derive a un enum señale el punto exacto en el mensaje.
  4. Modifica la macro para que, además del getter &Tipo, genere un setter pub fn set_campo(&mut self, v: Tipo); duplica la interpolación dentro del mismo map.
  5. Haz que la macro salte los campos marcados con un atributo #[oculto], filtrando con campo.attrs antes del map; razona por qué esto ya empieza a parecerse a un mini-framework.