Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

venerdì 10 novembre 2017

Strerror e le sue sorelle
quale strerror scegliere in C

Questa è una storia di strani incontri e rapporti sbagliati, come nel capolavoro (l'ennesimo) del grande Woody, "Hannah e le sue sorelle". La vita, spesso ci porta a prendere decisioni importanti, come è successo ad Hannah, e  altre un po' meno importanti come quelle che analizzeremo tra poco... comunque sempre di decisioni si tratta.
Hannah Strerror e le sue sorelle
(apro una parentesi: in questo post parleremo della strerror() e delle sue varianti. La strerror() è una funzione della libc che, passandogli un numero di errore, ti restituisce la stringa descrittiva corrispondente. Molte funzioni di libreria e system calls in caso di errore aggiornano il valore di una variabile globale, errno, che quindi contiene, in ogni momento, il valore dell'ultimo errore di esecuzione. Esiste poi un altra variabile globale, _sys_errlist, che contiene le stringhe corrispondenti a ogni errno, per cui, prima che qualche altra parte del programma in esecuzione alteri il valore di errno, bisognerebbe localizzare in _sys_errlist la stringa di errore che vogliamo trattare. Come anticipato, questa ultima operazione si può fare usando la strerror(), di cui abbiamo già parlato qui in maniera indiretta. Chiudo la parentesi) 

Si era detto: decisioni. La strerror() ha molte personalità, quindi quale scelgo? la strerror() o la strerror_r()? E se uso quest'ultima quale scelgo, la versione XSI-compliant o la versione GNU-specific? (per non parlare, poi, delle altre varianti, la strerror_l(), la strerror_s(), ecc., ma queste sono varianti secondarie).

Cominciamo con la prima domanda: strerror() o strerror_r()? Anticamente esisteva solo la prima, ma poi sono apparsi i thread e sono cominciati i problemi, perché nel codice multi-thread ci sono alcune parti critiche dove bisognerebbe usare solo funzioni thread-safe. La strerror() non è dichiarata thread-safe nello standard, e per capire il perché basta analizzare una implementazione semplificata (però molto simile alle implementazioni reali che possiamo trovare nelle varie libc disponibili). Vai col codice!
#include <stdio.h> // stdio.h include sys_errlist.h che dichiara le variabili
                   // globali _sys_errlist (array errori) e _sys_nerr (num.errori)
static char buf[256]; // buffer globale statico per la stringa da ritornare

char *strerror(int errnum)
{
    // test se errnum è un valore valido
    if (errnum < 0 || errnum >= _sys_nerr || _sys_errlist[errnum] == NULL) {
        // errore sconosciuto: copio in buf un messaggio di errore generico
        snprintf(buf, sizeof(buf), "Unknown error %d", errnum);
    }
    else {
        // errore conosciuto: copio in buf il messaggio corrispondente
        snprintf(buf, sizeof(buf), "%s", _sys_errlist[errnum]);
    }

    // ritorno buf che ora contiene il messaggio di errore
    return buf;
}
risulta evidente dal codice (ben commentato, come sempre, così non devo spiegarlo riga per riga) che la strerror() non ritorna direttamente _sys_errlist[errnum] (e se fosse così sarebbe thread-safe) ma compone un messaggio di errore (per trattare anche gli errnum non validi) usando un buffer globale statico buf: quindi, se due thread di una applicazione usano (quasi) contemporaneamente la strerror() il contenuto di buf non sarà attendibile (prevale il thread che ha scritto per ultimo). 

(altra parentesi: non è impossibile scrivere una strerror() che sia thread-safe, e in alcuni sistemi lo è: ma visto che secondo lo standard non lo è, non possiamo essere sicuri che sul sistema che stiamo usando (o sul sistema su cui, un giorno, girerà la applicazione che stiamo scrivendo) non ci sia una implementazione come quella appena descritta, quindi...)

Allora, per il Software multi-thread è nata la strerror_r() che è thread-safe. Come funziona? vai col codice!
#include <stdio.h> // stdio.h include sys_errlist.h che dichiara le variabili
                   // globali _sys_errlist (array errori) e _sys_nerr (num.errori)

char *strerror_r(int errnum, char *buf, size_t buflen);
{
    // test se errnum è un valore valido
    if (errnum < 0 || errnum >= _sys_nerr || _sys_errlist[errnum] == NULL) {
        // errore sconosciuto: copio in buf un messaggio di errore generico
        snprintf(buf, buflen, "Unknown error %d", errnum);
    }
    else {
        // errore conosciuto: copio in buf il messaggio corrispondente
        snprintf(buf, buflen, "%s", _sys_errlist[errnum]);
    }

    // ritorno buf che ora contiene il messaggio di errore
    return buf;
}
anche in questo caso si tratta di codice semplificato, ma molto vicino alla realtà: il trucco è semplice, invece di usare un buffer globale statico (che è la fonte dei problemi della strerror()) il chiamante della funzione si deve preoccupare di allocare e passare un buffer (e la sua lunghezza) alla strerror_r(). In questo modo il buffer che usa la strerror_r() è locale al thread che la chiama, e non può essere sovrascritto da un altro thread concorrente. Abbiamo sacrificato un po' di semplicità d'uso ma abbiamo ottenuto l'agognato comportamento thread-safe!

Ed ora aggiungiamo un po' di complicazione: la versione di strerror_r() appena mostrata è la GNU-specific. Ma, sfortunatamente, esiste anche la XSI-compliant, che è la seguente:
int strerror_r(int errnum, char *buf, size_t buflen);
Come si nota questa seconda versione non ritorna il buffer con la error-string, ma ritorna, invece, un codice di errore, e la stringa trovata bisogna ripescarla direttamente nel buffer che abbiamo passato. Per quanto riguarda il codice di ritorno è 0 in caso di successo e, in base alla versione di libc in uso, potrebbe ritornare -1 in caso di errore (settando errno al valore specifico di errore) oppure un valore positivo corrispondente a errno (bah, questo doppio comportamento non è proprio il massimo della semplicità d'uso...). Per usare questa versione o la GNU-specific bisogna giocare opportunamente con i flag _GNU_SOURCE, _POSIX_C_SOURCE e _XOPEN_SOURCE del preprocessore (come descritto nel manuale della strerror()).

E ora siamo pronti per la seconda decisione: quale usiamo, la GNU-specific o la XSI-compliant? Beh, io direi che quando scriviamo del codice per trattare dei codici di errore probabilmente non ci interessa trattare anche gli errori generati in questa fase (e nella fase successiva, ecc., ecc., un loop infinito di ricerca degli errori!); ci interessa, invece, scrivere codice lineare e semplice... per toglierci il dubbio possiamo analizzare due piccoli esempi d'uso:
GNU-specific
if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    // errore socket()
    char errbuf[MAX_ERROR_LEN];    // buffer per 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) {
    // errore socket()
    char errbuf[MAX_ERROR_LEN];    // buffer per strerror_r()
    int my_error = strerror_r(errno, errbuf, sizeof(errbuf)));
    if (! my_error)
        printf("socket() error (%s)\n", errbuf);
    else {
        // tratto l'errore (magari usando di nuovo strerror_r()?)
        ...
    }

    return EXIT_FAILURE;
}
Non so voi cosa ne pensate, ma io uso sempre la versione GNU-specific! A voi la scelta...

Ciao, e al prossimo post!

Nessun commento:

Posta un commento