En los títulos y los textos vais a encontrar unas cuantas citaciones cinematográficas (y si, soy un cinéfilo). Si no os interesan podéis fingir no verlas, ya que no son fundamentales para la comprensión de los post...

Este blog es la versión en Español de mi blog en Italiano L'arte della programmazione in C. Espero que mis traducciones sean comprensibles...

sábado, 11 de noviembre de 2017

Strerror y sus hermanas
qué strerror escoger en C

Esta es una historia de encuentros extraños y relaciones equivocadas, como en la obra maestra (otra más) del gran Woody, "Hannah y sus hermanas". La vida a menudo nos lleva a tomar decisiones importantes, como le sucedió a Hannah, y otras un poco menos importantes como las que veremos en breve... sin embargo, siempre de decisiones se trata.
Hannah Strerror y sus hermanas
(Abro un paréntesis: en este post hablaremos de la strerror() y sus variantes. La strerror() es una función de la libc que, al pasarle un número de error, te devuelve la cadena descriptiva correspondiente. Muchas funciones de librería y system calls en el caso de un error actualizan el valor de una variable global, errno, que contiene, en cualquier momento, el valor del último error de ejecución. Existe otra variable global, _sys_errlist, que contiene las cadenas correspondientes a cada errno, de modo que antes de que otra parte del programa en ejecución modifique el valor de errno, deberíamos ubicar en _sys_errlist la cadena de error que queremos tratar. Como adelantado, esta última operación se puede realizar usando la strerror(), de la que ya hemos hablado aquí de manera indirecta. Cierro el paréntesis)

Se dijo: decisiones. La strerror() tiene muchas personalidades, entonces, ¿cual elijo? ¿la strerror() o la strerror_r()? Y si utilizo esta última, ¿elijo la versión XSI-compliant o la versión GNU-specific? (sin mencionar, luego, las otras variantes, la strerror_l(), la strerror_s(), etc., pero estas son variantes secundarias).

Comencemos con la primera pregunta: ¿strerror() o strerror_r()? Antiguamente existía solo la primera, pero luego aparecieron los thread y comenzaron los problemas, porque en el código multi-thread hay algunas partes críticas donde solo deberían usarse las funciones thread-safe. La strerror() no está declarada thread-safe en el estándar, y para entender por qué es suficiente analizar una implementación simplificada (aunque muy similar a las implementaciones reales que podemos encontrar en las diversas libc disponibles). ¡Vamos con el código!
#include <stdio.h> // stdio.h incluye sys_errlist.h que declara las variables
                   // globales _sys_errlist (array errores) y _sys_nerr (num.errores)
static char buf[256]; // buffer global estático para la string a retornar

char *strerror(int errnum)
{
    // test si errnum es un valor valido
    if (errnum < 0 || errnum >= _sys_nerr || _sys_errlist[errnum] == NULL) {
        // error desconocido: copio en buf un mensaje de error genérico
        snprintf(buf, sizeof(buf), "Unknown error %d", errnum);
    }
    else {
        // error conocido: copio en buf el mensaje correspondiente
        snprintf(buf, sizeof(buf), "%s", _sys_errlist[errnum]);
    }

    // retorno buf que ahora contiene el mensaje de error
    return buf;
}
es obvio por el código (bien comentado, como siempre, así que no tengo que explicarlo línea por línea) que la strerror() no devuelve directamente _sys_errlist [errnum] (y si fuera así sería thread-safe) sino crea un mensaje de error (para tratar eventuales errnum no válidos) usando un buffer global estático buf: entonces si dos thread de una aplicación usan (casi) al mismo tiempo, la strerror() el contenido de buf no será fiable (prevalece el thread que ha escrito por ultimo).

(Otro paréntesis: no es imposible escribir una strerror() que sea thread-safe, y en algunos sistemas sí lo es: pero dado que según el estándar no lo es, no podemos estar seguros de que en el sistema que estamos usando (o en el sistema en que un día se ejecutará la aplicación que estamos escribiendo) no haya una implementación como la que acabamos de describir, así que...)

Entonces, para el software multi-thread, ha nacido la strerror_r() que es thread-safe. ¿Cómo funciona? ¡Vamos con el código!
#include <stdio.h> // stdio.h incluye sys_errlist.h que declara las variables
                   // globales _sys_errlist (array errores) y _sys_nerr (num.errores)

char *strerror_r(int errnum, char *buf, size_t buflen);
{
    // test si errnum es un valor valido
    if (errnum < 0 || errnum >= _sys_nerr || _sys_errlist[errnum] == NULL) {
        // error desconocido: copio en buf un mensaje de error genérico
        snprintf(buf, buflen, "Unknown error %d", errnum);
    }
    else {
        // error conocido: copio en buf el mensaje correspondiente
        snprintf(buf, buflen, "%s", _sys_errlist[errnum]);
    }

    // retorno buf que ahora contiene el mensaje de error
    return buf;
}
también en este caso se trata de un código simplificado, pero muy cercano a la realidad: el truco es simple, en lugar de usar un buffer global estático (que es el origen de los problemas de la strerror()) el que llama la función debe preocuparse de asignar y pasar un buffer (y su longitud) a la strerror_r(). De esta forma, el buffer que usa strerror_r() es local para el thread que lo llama, y no puede ser sobrescrito por otro thread concurrente. Hemos sacrificado un poco de simplicidad de uso ¡pero hemos conseguido el deseado comportamiento thread-safe!

Y ahora añadimos una pequeña complicación: la versión de strerror_r() que se acaba de mostrar es la GNU-specific. Pero, desafortunadamente, existe también la  XSI-compliant, que es la siguiente:
int strerror_r(int errnum, char *buf, size_t buflen);
Como se puede ver, esta segunda versión no devuelve el buffer con la error-string, y devuelve, en vez, un código de error, y la cadena encontrada hay que buscarla directamente en el buffer que hemos pasado. Con respecto al código de error, es 0 en caso de éxito, y dependiendo de la versión de libc en uso, puede devolver -1 si hay un error (seteando errno con el valor de error específico) o un valor positivo correspondiente a errno (bah, este comportamiento dual no es realmente el máximo en simplicidad de uso...). Para usar esta versión o la GNU-specific, hay que jugar correctamente con los flag _GNU_SOURCE, _POSIX_C_SOURCE y _XOPEN_SOURCE del preprocesador (como se describe en el manual de la strerror()).

Y ahora estamos listos para la segunda decisión: ¿qué usamos, la GNU-specific o la XSI-compliant? Bueno, yo diría que cuando escribimos código para tratar los códigos de error probablemente no nos interesa tratar también los errores generados en esta fase (y en la siguiente fase, etc., etc., un loop infinito de búsqueda de errores); estamos interesados, en vez, en escribir código lineal y simple... para quitarnos la duda podemos analizar dos pequeños ejemplos de uso:
GNU-specific
if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    // error socket()
    char errbuf[MAX_ERROR_LEN];    // buffer para strerror_r()
    printf("socket() error (%s)\n", strerror_r(errno, errbuf, sizeof(errbuf)));
    return EXIT_FAILURE;
}
XSI-compliant
if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    // error socket()
    char errbuf[MAX_ERROR_LEN];    // buffer para strerror_r()
    int my_error = strerror_r(errno, errbuf, sizeof(errbuf)));
    if (! my_error)
        printf("socket() error (%s)\n", errbuf);
    else {
        // proceso el error (¿quizás usando otra vez strerror_r()?)
        ...
    }

    return EXIT_FAILURE;
}
No sé lo que pensáis vosotros, ¡pero yo siempre uso la versión GNU-specific! A vosotros la eleccion...

¡Hasta el próximo post!

No hay comentarios:

Publicar un comentario