Las macros en Rust son código que genera código en tiempo de compilación. Hay dos familias: las macros declarativas (macro_rules!) que hacen pattern matching sobre tokens, y las macros procedurales que reciben un AST y devuelven otro. Este artículo cubre macro_rules!: la forma más accesible de crear abstracciones que el compilador expande antes de compilar.
Sintaxis básica
macro_rules! saludo {
// patrón => expansión
() => {
println!("Hola, mundo!");
};
($nombre:expr) => {
println!("Hola, {}", $nombre);
};
}
fn main() {
saludo!(); // Hola, mundo!
saludo!("Rust"); // Hola, Rust
saludo!("Programador"); // Hola, Programador
}
Tipos de fragmento
Cada $x:tipo captura un tipo específico de token:
macro_rules! demo_tipos {
($e:expr) => { println!("expr: {}", $e) };
($i:ident) => { println!("ident: {}", stringify!($i)) };
($t:ty) => { println!("tipo: {}", stringify!($t)) };
($b:block) => { let _r = $b; };
($l:literal) => { println!("literal: {}", $l) };
}
macro_rules! assert_eq_custom {
($left:expr, $right:expr) => {
if $left != $right {
panic!(
"assert_eq fallido:n izquierda: {:?}n derecha: {:?}",
$left, $right
);
}
};
($left:expr, $right:expr, $msg:literal) => {
if $left != $right {
panic!("{}: {:?} != {:?}", $msg, $left, $right);
}
};
}
fn main() {
assert_eq_custom!(2 + 2, 4);
assert_eq_custom!(1 + 1, 2, "suma incorrecta");
}
Repetición con $(...)*
// Crear un Vec con sintaxis de literal
macro_rules! mivec {
($($x:expr),*) => {
{
let mut v = Vec::new();
$( v.push($x); )*
v
}
};
// Con trailing comma
($($x:expr),+ ,) => { mivec![$($x),+] };
}
// Constructor de HashMap
macro_rules! mapa {
($($k:expr => $v:expr),* $(,)?) => {
{
let mut m = std::collections::HashMap::new();
$( m.insert($k, $v); )*
m
}
};
}
fn main() {
let v = mivec![1, 2, 3, 4, 5];
println!("{v:?}"); // [1, 2, 3, 4, 5]
let m = mapa![
"uno" => 1,
"dos" => 2,
"tres" => 3,
];
println!("{m:?}");
}
DSL de rutas HTTP
macro_rules! rutas {
($($metodo:ident $ruta:literal => $handler:expr),+ $(,)?) => {
{
let mut tabla: Vec<(&str, &str, Box<dyn Fn() -> String>)> = vec![];
$(
tabla.push((
stringify!($metodo),
$ruta,
Box::new($handler),
));
)+
tabla
}
};
}
fn main() {
let rutas = rutas![
GET "/api/usuarios" => || "lista de usuarios".to_string(),
POST "/api/usuarios" => || "usuario creado".to_string(),
GET "/api/articulos" => || "lista de artículos".to_string(),
];
for (metodo, ruta, handler) in &rutas {
println!("{metodo} {ruta} ? {}", handler());
}
}
Depurar con cargo expand
// Instalar: cargo install cargo-expand
// Ejecutar: cargo expand
// Esto expande toda la macro y muestra el código generado.
// Indispensable cuando la macro no compila o hace algo inesperado.
macro_rules! suma_y_muestra {
($a:expr, $b:expr) => {
let resultado = $a + $b;
println!("{} + {} = {}", $a, $b, resultado);
};
}
fn main() {
suma_y_muestra!(3, 4);
// cargo expand muestra la expansión completa de esta llamada
}
Errores típicos
// ERROR: usar $x antes de capturarlo
macro_rules! malo {
() => { println!("{}", $x) }; // $x no existe en este brazo
}
// ERROR: tipo de fragmento incorrecto
macro_rules! necesita_ident {
($i:ident) => { let $i = 42; };
}
// necesita_ident!(3 + 5); // ERROR: 3+5 es expr, no ident
// necesita_ident!(mi_var); // OK
// AVISO: las macros son higiénicas
// Las variables creadas dentro no filtran al scope exterior
macro_rules! crea_var {
($nombre:ident) => {
let $nombre = 99;
};
}
fn main() {
crea_var!(x);
println!("{x}"); // 99: funciona porque $nombre es ident del caller
}
Resumen
macro_rules! nombre { (patrón) => { expansión }; }define una macro declarativa.- Los fragmentos más usados:
$x:expr,$i:ident,$t:ty,$l:literal. $($x:expr),*captura cero o más expresiones separadas por coma; usa+para una o más.$(,)?al final acepta una coma trailing opcional.- Las macros son higiénicas: las variables internas no contaminan el scope del caller.
cargo expandmuestra el código generado, esencial para depurar.
