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.
