El coste de una proc-macro y cuándo vale la pena
Las macros procedurales no son gratis: exigen un crate propio de tipo proc-macro, arrastran syn y quote, y su coste se paga en cada compilación de cada usuario. Frente a ellas están macro_rules! —más barato pero sin introspección— y escribir el código a mano —sin dependencias pero repetitivo—. Un marco de decisión honesto para elegir la herramienta antes de alcanzar la más pesada por reflejo.
Terminamos el nivel con la pregunta que un ingeniero maduro se hace antes de escribir la macro: ¿de verdad hace falta? Una macro procedural es la herramienta más potente del arsenal de metaprogramación de Rust, y precisamente por eso es la más cara. Cuesta un crate separado, arrastra syn y quote —dos de las dependencias más pesadas del ecosistema— y ese coste no lo pagas solo tú una vez, sino cada usuario de tu librería en cada compilación. Sus alternativas, macro_rules! y escribir el código a mano, son a menudo más que suficientes y órdenes de magnitud más baratas. Este cierre no te disuade de las proc-macros: te da el marco para elegirlas cuando su poder justifique su precio, y para no alcanzarlas por reflejo cuando una herramienta más humilde bastaba.
- Enumerar los costes reales de una proc-macro: crate propio, dependencias pesadas y tiempo de compilación repetido.
- Contrastar proc-macro,
macro_rules!y código a mano según poder, coste y capacidad de introspección. - Reconocer las señales que justifican una proc-macro frente a las que aconsejan una alternativa.
- Aplicar mitigaciones —features de
syn,cargo expand, macros precompiladas— para acotar el coste.
El coste estructural: un crate aparte
La primera factura es organizativa y no es negociable: una proc-macro debe vivir en un crate propio marcado con proc-macro = true, y ese crate solo puede exportar macros procedurales, nada más. No puede ofrecer también los tipos o traits que acompañan a la macro. Por eso el patrón universal en el ecosistema es la pareja de crates: uno normal con la API pública —serde, tokio— y otro -derive o -macros con la maquinaria —serde_derive, tokio-macros—, reexportado desde el primero para que el usuario no lo vea. Es una complejidad de proyecto que un macro_rules! no impone: una macro declarativa vive en cualquier módulo del crate que ya tienes.
# El crate de la macro: aislado, y solo exporta proc-macros.
[package]
name = "mi_cosa_derive"
[lib]
proc-macro = true
[dependencies]
syn = "2"
quote = "1"
proc-macro2 = "1"
El coste continuo: tiempo de compilación
La segunda factura es la que de verdad duele a escala, y se paga en tiempo de compilación. Son tres sumandos. Primero, tu crate de macro depende de syn y quote, y syn —un parser completo de Rust— es notoriamente costoso de compilar, sobre todo con la feature full. Segundo, esa dependencia se inyecta en el grafo de cada proyecto que use tu librería: alguien que solo quería tu tipo acaba compilando syn la primera vez. Tercero, y más sutil, la macro se ejecuta en cada compilación: cada vez que se recompila un módulo que la invoca, tu función corre otra vez, parsea otra vez, genera otra vez. Un derive aplicado a doscientos structs es doscientas ejecuciones de parseo y generación en cada build incremental que los toque.
La asimetría es lo que hace peligroso el abuso de proc-macros en una librería popular. Tú escribes la macro una vez; miles de proyectos la compilan y la ejecutan millones de veces. Una dependencia pesada en una librería base se propaga por transitividad a todo lo que la usa, y los segundos de compilación que añades se multiplican por toda tu base de usuarios y por cada uno de sus builds de CI. Antes de publicar una proc-macro en una librería de amplio alcance, mide su impacto en tiempo de compilación como medirías su impacto en ejecución: es igual de real, solo que lo sufre otra gente.
Las tres herramientas, comparadas
Antes de escribir una proc-macro, sitúala frente a sus dos alternativas. La pregunta decisiva casi siempre es una: ¿necesitas introspección —leer la forma del tipo, sus campos, sus tipos—? Si la respuesta es no, rara vez necesitas una proc-macro.
Proc-macro
Máximo poder: introspecciona tipos, recorre campos, ramifica según la forma del dato y genera código arbitrario. A cambio, exige crate propio, arrastra syn y quote, y encarece cada compilación. La herramienta de los derives que abarcan muchos tipos, los frameworks y los DSLs.
macro_rules!
Sustitución de patrones, declarativa e higiénica. Sin crate aparte, sin dependencias, mucho más barata de compilar. Pero no introspecciona: no puede leer los campos ni los tipos de un struct. Ideal para repetición sintáctica —vec!, envolver llamadas, generar tests parametrizados—.
Código a mano
Cero dependencias, cero magia, coste de compilación mínimo y máxima legibilidad. El lector ve exactamente lo que corre. Su único defecto es la repetición cuando el mismo patrón se copia por decenas de tipos; hasta ese umbral, casi siempre es la respuesta correcta.
La regla práctica se ordena de menor a mayor coste. Si el patrón aparece pocas veces, escríbelo a mano: nada supera en claridad y baratura a código explícito. Si se repite mucho pero es sustitución sintáctica sin necesidad de mirar dentro de los tipos, usa macro_rules!. Solo cuando necesites inspeccionar la estructura de tipos arbitrarios —un derive genérico, un framework, un DSL con validación en compilación— sube el último escalón hasta la proc-macro, porque es lo único que puede leer la forma de un dato que aún no existe.
flowchart TD
A[Necesito generar codigo repetido] --> B{Necesito leer campos o tipos del dato}
B -->|No y aparece poco| C[Escribelo a mano]
B -->|No pero se repite mucho| D[Usa macro_rules]
B -->|Si introspeccion real| E[Usa una proc-macro]
E --> F[Acota el coste con features y precompilacion]
style C fill:#a6e3a1,color:#11111b
style D fill:#89b4fa,color:#11111b
style E fill:#cba6f7,color:#11111b
style F fill:#f9e2af,color:#11111bCuándo sí, y cómo acotar el coste
Hay casos donde la proc-macro no solo se justifica, sino que es la única respuesta sensata: un trait que decenas de tipos deben implementar mecánicamente (Serialize, Debug), la ergonomía de un framework que reescribe funciones en manejadores, o un DSL que valida su sintaxis en compilación como sqlx::query!. En todos, el valor —código correcto generado por miles, errores atrapados antes de ejecutar— desborda con creces el coste de compilación. Cuando ese sea tu caso, acótalo: activa en syn solo las features que uses, evitando full si no parseas ítems completos; apóyate en cargo expand para depurar viendo el código real generado; y considera, para macros muy usadas, técnicas de precompilación que evitan recompilar el motor de la macro en cada proyecto. El objetivo no es rehuir la herramienta pesada, sino pagarla con conciencia.
# Acota syn a lo imprescindible: cada feature se paga en compilacion.
syn = { version = "2", default-features = false, features = ["derive", "parsing", "printing"] }
Dos herramientas domestican el coste y la opacidad de las proc-macros. cargo expand imprime el código que tu macro genera de verdad —la expansión que el compilador ve—, y es indispensable para depurar: convierte la caja negra en algo legible y te ahorra horas de adivinar por qué no compila lo generado. Para el coste de build propagado a tus usuarios, la técnica de las macros precompiladas —popularizada por el enfoque de watt, que distribuye el motor de la macro como WebAssembly ya compilado— evita que cada proyecto recompile syn y quote desde cero. No siempre hace falta, pero saber que existen te permite publicar una macro popular sin cargar a toda tu base de usuarios con tu árbol de dependencias.
Cierra el nivel con el principio que separa al programador que puede escribir una proc-macro del que sabe cuándo hacerlo, porque es una distinción de madurez más que de técnica. Existe una regla profunda en el diseño de software —la regla del mínimo poder— que dice: para cualquier tarea, prefiere la herramienta menos potente que la resuelva. Suena contraintuitivo, porque el instinto empuja a lo contrario: aprendes las proc-macros, sientes su poder, y de pronto todo problema parece pedir una. Pero el poder tiene un coste que no aparece en la demo y sí en el proyecto real —dependencias que se propagan, tiempos de compilación que se multiplican por toda tu base de usuarios, código fuente que deja de decir la verdad sobre lo que corre, una barrera de entrada para quien mantenga tu crate después—. La herramienta menos poderosa que resuelve el problema es casi siempre la más legible, la más barata, la más fácil de depurar y la que menos sorprende al que venga detrás. Por eso la decisión correcta se toma de abajo arriba: empieza preguntando si basta código a mano, sube a macro_rules! solo si la repetición lo pide, y reserva la proc-macro para cuando de verdad necesites lo único que ella ofrece —introspeccionar la estructura de un tipo que aún no existe cuando escribes la macro—. Ese es su superpoder genuino e insustituible, y cuando lo necesitas, ninguna alternativa sirve; pero cuando no lo necesitas, alcanzarla es pagar el precio de un avión para cruzar la calle. La señal de un ingeniero experto no es la lista de herramientas poderosas que domina, sino la sobriedad con que las despliega: conoce las proc-macros al dedillo, y precisamente por eso las usa poco, tarde y con motivo. Que el nivel te haya enseñado a construirlas es la mitad del aprendizaje; saber resistir el impulso de usarlas es la otra mitad, y la más difícil.
Una proc-macro cuesta un crate propio de tipo proc-macro —que solo exporta macros, de ahí el patrón de crate -derive reexportado—, arrastra syn y quote, y se ejecuta en cada compilación de cada usuario, encareciendo el tiempo de build de toda tu base. Frente a ella, macro_rules! es barato y sin dependencias pero no introspecciona, y el código a mano es el más claro y ligero hasta que la repetición se vuelve insostenible. La pregunta decisiva es si necesitas leer la forma del tipo: si no, casi nunca hace falta una proc-macro. Elige de abajo arriba —a mano, luego macro_rules!, luego proc-macro— y, cuando la uses, acota el coste con features mínimas de syn, cargo expand y precompilación. La regla del mínimo poder es ingeniería, no pereza.
- Para cada caso, elige entre código a mano,
macro_rules!y proc-macro, y justifícalo: (a) repetir tres veces un bloque casi idéntico; (b) unvec!propio que acepte una lista variable; (c) derivarSerializepara cualquier struct. - Explica por qué una proc-macro necesita un crate aparte y cómo el patrón de crate
-derivereexportado oculta esa complejidad al usuario final. - Describe los tres sumandos del coste de compilación de una proc-macro y argumenta por qué el peso recae sobre tus usuarios y no solo sobre ti.
- Enuncia la pregunta clave que distingue cuándo
macro_rules!basta y cuándo hace falta una proc-macro, e ilústrala con un ejemplo de cada lado de la frontera. - Enumera tres formas concretas de acotar el coste de una proc-macro ya decidida y explica qué reduce cada una.