Argumentos variádicos y la librería stdarg.h

Cuando diseñas una API que requiere flexibilidad, como un logger o una función de formateo, a menudo te encuentras con la necesidad de aceptar un número indeterminado de argumentos. En C, esto se logra mediante las funciones variádicas, que permiten a una función declarar uno o más parámetros fijos seguidos de un elipsis (...).

Para que una función pueda procesar estos argumentos adicionales, debe interactuar con el stack (pila) o con los registros de la arquitectura de forma manual, ya que el compilador no sabe cuántos argumentos se han pasado ni de qué tipo son. Para esto, el encabezado <stdarg.h> proporciona un conjunto de macros y un tipo opaco llamado va_list.

El flujo de trabajo siempre sigue un patrón estricto: se inicializa el estado con va_start, se extraen los valores con va_arg indicando el tipo esperado, y se limpia el estado con va_end. Es fundamental entender que la función variádica es “ciega”: no tiene forma de saber cuándo termina la lista de argumentos por sí misma. Necesita un mecanismo externo para detener el proceso, como una cadena de formato (como en printf), un número entero que indique la cantidad de argumentos, o un valor centinela (como un NULL al final).

Si intentas extraer un tipo de dato distinto al que realmente se pasó (por ejemplo, llamar a va_arg(args, int) cuando el argumento fue un double), entrarás directamente en el terreno del Undefined Behavior (UB), ya que estarás interpretando bytes de memoria de forma incorrecta, lo que puede resultar en basura o un crash por alineación de memoria.

#include <stdarg.h>
#include <stdio.h>
#include <stddef.h>

/**
 * Una implementación simplificada de un logger que permite 
 * formatear mensajes con enteros y strings.
 */
void mi_logger_custom(int nivel, const char *formato, ...) {
    va_list args;
    
    // Inicializamos 'args' para que apunte a los argumentos tras 'formato'
    va_start(args, formato);

    printf("[LOG NIVEL %d] ", nivel);

    // Recorremos la cadena de formato para saber qué tipos extraer
    for (const char *p = formato; *p != '\0'; p++) {
        if (*p == '%' && *(p + 1) == 'd') {
            // Extraemos un int y avanzamos el puntero interno de args
            int valor_int = va_arg(args, int);
            printf("%d", valor_int);
            p++; // Saltamos la 'd'
        } else if (*p == '%' && *(p + 1) == 's') {
            // Extraemos un puntero a char. 
            // Nota: va_arg no puede promover tipos automáticamente como en printf
            char *valor_str = va_arg(args, char *);
            printf("%s", valor_str);
            p++; // Saltamos la 's'
        } else {
            putchar(*p);
        }
    }
    putchar('\n');

    // Limpieza obligatoria del estado de la lista
    va_end(args);
}

int main(void) {
    // Caso 1: Uso estándar con un solo string y un entero
    mi_logger_custom(1, "Operacion completada: %s, code: %d\n", "Sync", 200);

    // Caso 2: Múltiples argumentos
    mi_logger_custom(3, "Error crítico: %s detectado en id %d\n", "Timeout", 404);

    return 0;
}

En el ejemplo anterior, mi_logger_custom utiliza va_list args para mantener el estado de la iteración sobre la pila de argumentos. La macro va_start(args, formato) prepara a args para empezar a leer justo después del último parámetro con nombre, que en este caso es formato.

Cuando el bucle encuentra %d, llamamos a va_arg(args, int). En este momento, la CPU lee la cantidad de bytes correspondiente a un int desde la posición actual de la pila y mueve el puntero interno de args hacia el siguiente argumento. Si hubiéramos llamado a va_arg(args, float) en esa misma línea, el programa intentaría leer 4 bytes (en la mayoría de sistemas) cuando el argumento original ocupaba 8, dejando el puntero de args desalineado para el resto de la ejecución. Finalmente, va_end(args) se asegura de que la limpieza sea consistente con la arquitectura, lo cual es crucial para la portabilidad.

El error frecuente

Un error clásico es el desajuste de tipos (type mismatch). Supongamos que llamas a la función así:
mi_logger_custom(1, "Valor: %d", 3.14);

Aquí, el argumento real es un double, pero la función ejecutará va_arg(args, int). El compilador no verificará esto en tiempo de ejecución. Al intentar leer un double como si fuera un int, el valor resultante será un número completamente errático y el puntero de args quedará desplazado una cantidad de bytes incorrecta, corrompiendo cualquier lectura posterior. Herramientas como UBSan (Undefined Behavior Sanitizer) son excelentes para detectar esto durante el desarrollo.

Para mitigar la inseguridad inherente de estas funciones, si estás usando gcc o clang, puedes usar el atributo __attribute__((format(printf, n, m))) en tu declaración de función. Esto le indica al compilador que la función debe ser validada con el mismo rigor que printf, comprobando que los argumentos coincidan con la cadena de formato proporcionada.

38

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio