Proc macros en Rust: implementar derive macros personalizadas con syn y quote

Las macros procedurales (proc macros) de Rust son funciones que reciben código Rust como entrada y devuelven código Rust como salida, todo en tiempo de compilación. A diferencia de las macros declarativas (macro_rules!), que trabajan con patrones de tokens, las proc macros tienen acceso a la estructura sintáctica completa del código.

Hay tres tipos:

  • Derive macros: #[derive(MiMacro)] generan código a partir de una struct o enum.
  • Attribute macros: #[mi_atributo] transforman el item al que se aplican.
  • Function-like macros: mi_macro!() similares a las declarativas pero más potentes.

Crear el crate de proc macros

Las proc macros deben estar en un crate separado con proc-macro = true en Cargo.toml:

# Cargo.toml del crate de macros
[package]
name = "mi-derive"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"

Los tres crates son indispensables:

  • syn: parsea el código Rust de entrada a un AST (árbol sintáctico).
  • quote: genera código Rust de salida interpolando variables.
  • proc-macro2: tipos de tokens más ergonómicos que los del compilador.

Una derive macro básica

Implementamos #[derive(Describir)] que genera un método describir() para cualquier struct:

// src/lib.rs del crate mi-derive
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Describir)]
pub fn describir_derive(input: TokenStream) -> TokenStream {
    // Parsear la entrada a un AST de syn
    let ast = parse_macro_input!(input as DeriveInput);

    let nombre = &ast.ident;

    // Generar el código de salida con quote!
    let output = quote! {
        impl #nombre {
            pub fn describir(&self) -> String {
                format!("Soy una instancia de {}", stringify!(#nombre))
            }
        }
    };

    output.into()
}

Uso en otro crate que dependede mi-derive:

use mi_derive::Describir;

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

let u = Usuario { nombre: "Ana".into(), edad: 30 };
println!("{}", u.describir());  // "Soy una instancia de Usuario"

Derive macro con acceso a campos

Ahora generamos un método que muestra todos los campos de la struct:

#[proc_macro_derive(MostrarCampos)]
pub fn mostrar_campos_derive(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    let nombre = &ast.ident;

    // Extraer los campos de la struct
    let campos = match &ast.data {
        syn::Data::Struct(data) => &data.fields,
        _ => panic!("MostrarCampos solo funciona en structs"),
    };

    // Generar código para mostrar cada campo
    let mostrar_campos = campos.iter().map(|campo| {
        let nombre_campo = campo.ident.as_ref().unwrap();
        let nombre_str = nombre_campo.to_string();
        quote! {
            resultado.push_str(&format!("  {}: {:?}n", #nombre_str, self.#nombre_campo));
        }
    });

    let output = quote! {
        impl #nombre {
            pub fn mostrar_campos(&self) -> String {
                let mut resultado = format!("{}:n", stringify!(#nombre));
                #(#mostrar_campos)*
                resultado
            }
        }
    };

    output.into()
}
#[derive(MostrarCampos, Debug)]
struct Producto {
    nombre: String,
    precio: f64,
    stock: u32,
}

let p = Producto { nombre: "Camiseta".into(), precio: 19.99, stock: 100 };
println!("{}", p.mostrar_campos());
// Producto:
//   nombre: "Camiseta"
//   precio: 19.99
//   stock: 100

Attribute macro

Las attribute macros transforman el item completo al que se aplican. Aquí un ejemplo que añade logging automático a una función:

#[proc_macro_attribute]
pub fn log_llamada(atributos: TokenStream, item: TokenStream) -> TokenStream {
    let funcion = parse_macro_input!(item as syn::ItemFn);
    let nombre_fn = &funcion.sig.ident;
    let nombre_str = nombre_fn.to_string();
    let cuerpo = &funcion.block;
    let firma = &funcion.sig;
    let visibilidad = &funcion.vis;
    let atributos_fn = &funcion.attrs;

    let output = quote! {
        #(#atributos_fn)*
        #visibilidad #firma {
            println!("[LOG] Llamando a {}", #nombre_str);
            let __resultado = (|| #cuerpo)();
            println!("[LOG] {} completado", #nombre_str);
            __resultado
        }
    };

    output.into()
}
use mi_derive::log_llamada;

#[log_llamada]
fn calcular(x: i32) -> i32 {
    x * 2
}

calcular(5);
// [LOG] Llamando a calcular
// [LOG] calcular completado

Depurar proc macros con cargo expand

Para ver exactamente qué código genera una proc macro, usa cargo-expand:

cargo install cargo-expand
cargo expand

Esto muestra el código Rust después de expandir todas las macros. Es esencial para debuggear proc macros: si el código generado no compila, cargo expand te muestra exactamente qué se generó.

Errores en proc macros: span

Para generar mensajes de error que apuntan a la ubicación correcta en el código del usuario:

use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(SoloStructs)]
pub fn solo_structs(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);

    match &ast.data {
        syn::Data::Struct(_) => {
            // OK, generar código
            quote::quote! {}.into()
        }
        _ => {
            // Usar syn::Error para apuntar al span correcto
            syn::Error::new_spanned(
                &ast.ident,
                "SoloStructs solo puede aplicarse a structs, no a enums ni unions"
            )
            .to_compile_error()
            .into()
        }
    }
}

Con syn::Error::new_spanned, el mensaje de error del compilador apuntará exactamente al enum MiEnum o donde esté el problema, no a la definición de la macro.

Las proc macros son la base de mucho del "magic" de Rust: serde, axum, sqlx, tokio, clap y decenas de crates populares las usan internamente para ofrecer APIs ergonómicas sin coste en tiempo de ejecución.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP