Higiene: por qué las macros de Rust no filtran variables
Los identificadores que introduce una macro viven en un contexto sintáctico propio: un let interno no colisiona ni captura las variables del sitio de uso, y viceversa. Ese mecanismo mata de raíz el bug clásico del preprocesador de C, aunque la higiene de Rust es parcial: total para variables locales, con costuras para rutas y por eso existe $crate.
Escribe una macro que declare internamente let temporal = ... y úsala en un sitio donde el usuario ya tenga su propia variable temporal. En C, esto sería un desastre silencioso: los dos temporal chocarían y uno pisaría al otro. En Rust no pasa absolutamente nada, y esa ausencia de catástrofe tiene nombre: higiene. Los identificadores que una macro introduce viven en un contexto sintáctico distinto del código que la invoca, de modo que no se ven, no se pisan y no se filtran entre sí. Es una de las propiedades más profundas de macro_rules! y la que explica por qué las macros de Rust se pueden usar sin el ritual defensivo que exigen las de C.
- Definir la higiene: identificadores de la macro y del sitio de uso en contextos separados.
- Ver el bug clásico de C que la higiene de Rust hace directamente imposible.
- Distinguir qué protege la higiene por completo y qué queda con costuras.
- Entender el papel de
$cratepara referirse a ítems sin depender del import del usuario.
Qué es la higiene
La idea es sencilla de enunciar y poderosa en consecuencias: cada identificador que aparece en la expansión de una macro lleva adosada una marca invisible, su contexto sintáctico, que registra de dónde vino. El resolvedor de nombres del compilador no compara identificadores solo por cómo se escriben, sino por el par nombre-más-contexto. Dos variables llamadas igual pero con contextos distintos son, para todos los efectos, variables distintas —tan distintas como si se llamaran diferente.
De ahí sale la garantía central. Un let que la macro introduce en su expansión pertenece al contexto de la macro; cualquier variable homónima en el sitio de la llamada pertenece al contexto del usuario. Nunca se confunden:
macro_rules! con_temporal {
($e:expr) => {{
let resultado = 40; // el "resultado" de la macro, contexto de la macro
resultado + $e
}};
}
fn main() {
let resultado = 2; // el "resultado" del usuario, otro contexto
let total = con_temporal!(resultado); // pasa SU resultado, que vale 2
assert_eq!(total, 42); // 40 + 2, sin ninguna colision
}
Léelo despacio, porque encierra dos hechos a la vez. El resultado interno de la macro vale 40 y no es tocado por el resultado del usuario que vale 2. Y el $e, que expande a la palabra resultado escrita por el usuario, se resuelve en el contexto del usuario: apunta a su variable de valor 2, no a la de la macro. Por eso el total es 42 y no 80. La macro y su llamante conviven sin verse.
El bug clásico de C que Rust hace imposible
Compara con el ejemplo de manual del preprocesador. En C, una macro que intercambia dos valores usando un temporal:
#define SWAP(a, b) { int tmp = a; a = b; b = tmp; }
int x = 1, tmp = 2;
SWAP(x, tmp); // se expande a { int tmp = x; x = tmp; tmp = tmp; }
El desastre es total y mudo. El tmp que introduce la macro colisiona con el tmp del usuario: tras la expansión, el segundo argumento ha quedado capturado por la variable interna y el intercambio produce basura. La única defensa en C es rezar y bautizar el temporal con un nombre improbable, __swap_tmp_xyz, confiando en que nadie lo use jamás. Es una convención frágil, no una garantía.
En Rust la misma macro es correcta por construcción, porque su tmp vive en otro contexto y jamás puede capturar el tmp del usuario:
macro_rules! intercambia {
($a:expr, $b:expr) => {{
let tmp = $a; // este tmp es de la macro, invisible fuera
$a = $b;
$b = tmp;
}};
}
let mut x = 1;
let mut tmp = 2;
intercambia!(x, tmp); // funciona: el tmp interno no choca con el del usuario
assert_eq!((x, tmp), (2, 1));
Todo el catálogo de trucos defensivos de C —nombres feos, do { } while (0) para no romper la sintaxis, parentizar cada argumento— existe únicamente para simular a mano lo que Rust te da gratis: aislamiento de nombres y fragmentos que respetan la gramática. No necesitas ninguno.
flowchart TD macro[let resultado dentro de la macro] --> ca[Contexto A variable propia valor 40] user[let resultado del usuario] --> cb[Contexto B variable propia valor 2] meta[La metavariable e expande a resultado del usuario] --> cb ca --> sin[Nombres iguales contextos distintos no colisionan] cb --> sin style macro fill:#89b4fa,color:#11111b style user fill:#cba6f7,color:#11111b style sin fill:#a6e3a1,color:#11111b
Higiene parcial: qué protege y qué no
Un matiz de rigor: la higiene de macro_rules! es parcial, no absoluta, y conviene saber dónde están las costuras. Lo que protege por completo son las variables locales y las etiquetas de bucle: un let o un 'salida: introducidos por la macro están perfectamente aislados. Las etiquetas de bucle merecen una demostración, porque son un caso menos obvio:
macro_rules! busca_par {
($iter:expr) => {{
let mut hallado = None;
'salida: for x in $iter { // esta etiqueta pertenece a la macro
if *x % 2 == 0 { hallado = Some(x); break 'salida; }
}
hallado
}};
}
fn main() {
let datos = [1, 3, 4, 7];
'salida: for _ in 0..1 { // el usuario reusa el nombre 'salida
let par = busca_par!(&datos);
assert_eq!(par, Some(&4));
break 'salida; // rompe SU bucle, no el de la macro
}
}
La 'salida de la macro y la 'salida del usuario son etiquetas distintas pese a escribirse igual: un break 'salida dentro de la macro nunca saltaría al bucle del usuario, ni al revés. Ahí la garantía es total.
Pero no todo se rige por contextos separados. Las rutas a ítems —funciones, tipos, constantes que la macro nombra— no son plenamente higiénicas: se resuelven según lo que esté importado en el sitio de uso. Una macro que en su expansión escriba HashMap::new() fallará si el usuario no ha hecho use std::collections::HashMap, aunque la macro “sepa” a cuál se refiere. Tampoco son higiénicos los campos y métodos, que se resuelven por el tipo del valor, ni los nombres de ítems que la macro define para que el usuario los use.
La herramienta para coser la costura de las rutas es la metavariable especial $crate, que se expande a una ruta inequívoca hacia el crate donde se definió la macro. Así una macro de librería puede referirse a sus propios ítems sin depender de cómo el usuario haya organizado sus use:
macro_rules! nuevo_registro {
() => {
// funciona aunque el usuario no haya importado nada de este crate
$crate::registro::Registro::new()
};
}
Sin $crate, la macro rompería en cuanto alguien la usara desde un módulo donde Registro no estuviera a la vista. Con $crate, la ruta es absoluta y robusta. La regla mental: la higiene te regala el aislamiento de lo local; para lo global —las rutas a ítems— sigues teniendo que ser explícito, y $crate es tu aliado.
Un corolario poco intuitivo: como cada identificador arrastra su contexto y los fragmentos son opacos, macro_rules! no puede concatenar dos identificadores para forjar uno nuevo. No hay forma de que, a partir de $nombre igual a usuario, la macro genere el identificador get_usuario: los tokens no se pegan como si fueran texto, precisamente porque no se tratan como texto. Es el reverso de la moneda de la higiene: el mismo rechazo a manejar los nombres como cadenas que te protege de las colisiones te niega construirlos por concatenación. Cuando de verdad necesitas nombres derivados, la vía idiomática es el crate paste —cuya macro sí sabe unir tokens en un identificador nuevo— o directamente una macro procedural. Toparte con este límite es una señal fiable de que rozas el techo de lo declarativo.
La historia de la higiene es, en pequeño, la historia de por qué Rust existe. Durante décadas, los programadores de C convivieron con un peligro conocido —las macros filtran y capturan nombres— y respondieron con disciplina: bautizar los temporales con nombres grotescos, parentizarlo todo, envolver los cuerpos en artificios sintácticos. Funcionaba mientras nadie se equivocara, y alguien siempre se equivocaba, normalmente a las tres de la madrugada y en producción. La higiene es la respuesta de Rust a ese patrón, y su lección trasciende a las macros: coge una responsabilidad que el lenguaje descargaba sobre la vigilancia humana y la traslada, entera, al sistema. Ya no confías en no chocar de nombre con una macro que ni siquiera escribiste; el compilador lo garantiza, porque cada identificador arrastra la memoria de su origen y el resolvedor la respeta. Es el mismo movimiento que el ownership hace con la memoria o que los tipos hacen con la corrección: convertir una precaución que había que recordar en una imposibilidad que no hace falta recordar. Y es honesto sobre sus límites, lo que la hace aún más valiosa. La higiene de macro_rules! es parcial a propósito: total para las variables locales, donde la captura accidental es el desastre clásico, y deliberadamente permeable para las rutas, porque una macro a menudo quiere referirse a los tipos del usuario o exponerle nombres nuevos. Ahí entra $crate, que no es un parche sino el reconocimiento de que “local” y “global” son problemas distintos: lo local se aísla, lo global se nombra sin ambigüedad. Entender esta división es entender que la higiene no pretende encerrar la macro en una burbuja, sino darte control preciso sobre qué se aísla y qué se comparte. Esa precisión, y no una prohibición general, es lo que hace de las macros de Rust una herramienta en la que se puede confiar sin escribir un solo nombre feo.
- Escribe
con_temporal!con unlet resultadointerno, úsala pasándole una variableresultadodel sitio de uso y verifica que no hay colisión; explica adónde resuelve cada nombre. - Traduce a Rust la macro
SWAPde C con un temporal y llámala pasando una variable que se llame igual que el temporal; comprueba que funciona y razona por qué en C no lo haría. - Escribe una macro cuya expansión referencie
HashMap::new()sin ruta completa y úsala en un módulo sin eluse; observa el error y arréglalo, primero conusey luego pensando en$crate. - Investiga si la higiene protege las etiquetas de bucle: mete un
'bucle:en una macro y comprueba que no choca con un'bucle:del usuario. - Explica con un ejemplo por qué una macro de librería debería usar
$crate::...para sus propios tipos en vez de confiar en losusede quien la llame.