wandres.dev
TÁCTICO · El preprocesador a fondo

El preprocesador a fondo

Macros y sus trampas, #embed para incrustar binarios, X-macros para generar código, y _Generic para funciones polimórficas. La metaprogramación que sí existe en C.

⏱ 15 min

El preprocesador es una máquina de sustituir texto que corre antes de compilar. Poderosa y peligrosa a partes iguales: mal usada genera bugs imposibles; bien usada, habilita la única metaprogramación que C tiene. Con C23 gana herramientas modernas como #embed.

🎯 Al terminar esta lección sabrás
  • Macros de objeto y de función, y sus trampas.
  • Include guards y compilación condicional.
  • #embed: incrustar archivos en el binario.
  • X-macros y _Generic.

Macros y sus trampas

Una macro sustituye texto, sin entender C. Eso causa los bugs clásicos:

#define CUADRADO(x) x * x
CUADRADO(2 + 3)          // → 2 + 3 * 2 + 3 = 11, ¡no 25!

#define CUADRADO(x) ((x) * (x))   // paréntesis: → ((2+3)*(2+3)) = 25
CUADRADO(i++)                     // → ((i++)*(i++)): i++ dos veces, ¡UB!
⚠️
Reglas para macros de función

Si escribes una macro tipo función: (1) envuelve cada parámetro y toda la macro en paréntesis: #define M(x) ((x)*(x)). (2) Nunca pases expresiones con efectos secundarios (i++, llamadas) — la macro puede evaluarlas varias veces. (3) Cuando puedas, prefiere una función static inline: es más segura (evalúa una vez, respeta tipos) y con -O2 es igual de rápida. Reserva las macros para lo que una función no puede hacer: manipular el propio texto del código.

Include guards y compilación condicional

Para que un header no se incluya dos veces (error de redefinición), protégelo:

#ifndef MI_HEADER_H
#define MI_HEADER_H
// … contenido …
#endif

// alternativa moderna y más simple:
#pragma once

La compilación condicional adapta el código a la plataforma o a opciones de build:

#ifdef __linux__
    // código específico de Linux
#elif defined(_WIN32)
    // código de Windows
#endif

#if __STDC_VERSION__ >= 202311L
    // usar features de C23
#endif

#embed: incrustar archivos (C23)

Una de las mejores novedades de C23. #embed incrusta el contenido de un archivo directamente como bytes, sin herramientas externas:

// antes: un script convertía logo.png en un array de C
// C23: directo
const unsigned char logo[] = {
    #embed "logo.png"
};
const size_t logo_size = sizeof logo;
#embed elimina toda una categoría de tooling

Antes de C23, incrustar un recurso (una fuente, un icono, un shader, datos de arranque) en el binario requería un paso de build extra: un script o herramienta que leía el archivo y escupía un array de C gigante. Frágil, lento y una dependencia más. #embed lo hace el preprocesador, nativo y portable. Para firmware, juegos y binarios autocontenidos —donde quieres un único ejecutable sin archivos externos— es una mejora enorme de ergonomía. Es C23 modernizando una necesidad real de la programación de sistemas.

X-macros: generar código

Un patrón ingenioso: defines una lista una sola vez y la “reproduces” en varios contextos (declaraciones, strings, tablas), manteniéndolos sincronizados:

#define COLORES \
    X(ROJO,  "rojo")  \
    X(VERDE, "verde") \
    X(AZUL,  "azul")

enum Color {            // genera el enum
    #define X(id, str) id,
    COLORES
    #undef X
};

const char *nombre(enum Color c) {   // genera la tabla de nombres
    switch (c) {
        #define X(id, str) case id: return str;
        COLORES
        #undef X
    }
    return "?";
}

Añadir un color en un solo sitio actualiza el enum y la tabla. Imposible que se desincronicen.

_Generic: polimorfismo en compilación

_Generic (C11) elige una expresión según el tipo de su argumento — la base de macros que funcionan con varios tipos:

#define abs_gen(x) _Generic((x), \
    int:    abs,   \
    long:   labs,  \
    double: fabs   \
)(x)

abs_gen(-5);      // llama a abs
abs_gen(-5.0);    // llama a fabs

Combinado con typeof (nivel 4), habilita utilidades genéricas seguras sin C++.

⚔️ Domina el preprocesador
  1. Escribe una macro con las trampas (x*x) y arréglala con paréntesis; luego conviértela en static inline.
  2. Protege un header con #pragma once.
  3. Usa #embed para incrustar un archivo pequeño como array de bytes.
  4. Implementa una lista con X-macros que genere un enum y su tabla de nombres.
  5. Escribe una macro con _Generic que despache según el tipo.