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ì 20 dicembre 2019

sprintf? No, grazie!
considerazioni sull'uso della sprintf in C

(...una premessa: questo post è un remake di un mio vecchio post. L'ho riadattato e "modernizzato" per pubblicarlo su quell'altro bel blog collettivo dove scrivo. Visto che le modifiche sono molte e (forse) interessanti, lo ripubblico anche qui. Questo potrebbe ripetersi in futuro...)
Peppe: Eva come fa uno a sapere se è innamorato di un'altra persona?
Eva:
Eh, lo chiedi a me?
Peppe:
Eh, sei tu che studi queste cose.
Bianca:
Te lo dico io, allora se ci parli trenta minuti al giorno sei innamorato.
Peppe:
E se ci parlo sessanta?
Carlotta:
Eh allora vuol dire che sei molto innamorato.
Lele: Poi se non ce parli più, vordì che sei sposato
!
Nel bel Perfetti Sconosciuti il personaggio Lele si caratterizza per le frasi a effetto come quella sopra. Con una leggera forzatura possiamo dire che quella frase e questo post si riferiscono allo stesso tema: l'abitudine. Lele pensa che lui e sua moglie si sono già detti tutto e, analogamente, i programmatori C usano la sprintf() solo perché è una pratica comune, una abitudine. Ecco, bisognerebbe fare un piccolo sforzo per risvegliare la passione originale (Lele, parla di più con tua moglie!) e cercare di evitare di trasformare una abitudine in una cattiva abitudine.
...poi, se non funziona, vordì che hai usato la sprintf...
Premessa: la sprintf() se la usi bene e hai il 100% di controllo sul codice scritto la puoi usare senza grossi problemi ma, come dicono gli inglesi, è una funzione "error prone", e cioè induce facilmente a commettere errori, anche gravi. Il problema più grave ed evidente con la sprintf() si chiama buffer overflow, e non credo che sia necessario spenderci molte parole: se il buffer che passiamo come primo argomento non è correttamente dimensionato il disastro è dietro l'angolo.

La soluzione però è semplice, visto che, fortunatamente, ci viene in aiuto la snprintf(), che è della stessa famiglia ma molto più sicura. Vediamo i due prototipi a confronto:
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
Come si può ben vedere, la snprintf() ci obbliga a mettere il size del buffer come secondo argomento, per cui è facilissimo abituarsi a scrivere in un modo error-free come questo:
char buffer[32];
snprintf(buffer, sizeof(buffer), "Hello world!");
E, se al posto di "Hello world!" avessimo scritto una stringa di più di 32 chars, nessun problema: la snprintf() la tronca opportunamente e siamo salvi. Fantastico.

E adesso siamo pronti per analizzare un semplicissimo esempio reale: scriviamo una funzione che ritorna una stringa formattata con data e ora (inclusi i microsecondi), e la implementiamo in due versioni, una buona che chiameremo getDateUsec(), e una cattiva che chiameremo badGetDateUsec(). Noterete che la versione cattiva passa solo l'array destinazione e internamente usa la sprintf(), mentre la versione buona passa l'array destinazione e il suo size, e internamente usa la snprintf(). Notare che è veramente una ottima abitudine quella di passare "destinazione + size" senza usare una allocazione interna alla funzione (con malloc()) che richiederebbe l'uso della corrispondente deallocazione (con free()) da parte del chiamante, come visto recentemente qui. Ed è anche una ottima abitudine evitare l'uso di una destinazione statica dentro la funzione, per i noti problemi con il multithreading (anche questo già visto qui). Ma bando alle ciance, vai col codice!
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>

// prototipi locali
char *getDateUsec(char *dest, size_t size);
char *badGetDateUsec(char *dest);

// funzione main
int main(int argc, char* argv[])
{
    // chiama getDateUsec (o badGetDateUsec) e scrive il risultato
    char dest[12];
    printf("date con usec: %s\n", getDateUsec(dest, sizeof(dest)));
    //printf("date con usec: %s\n", badGetDateUsec(dest));

    return EXIT_SUCCESS;
}

// getDateUsec() - Genera una stringa con data e ora (usa i microsecondi)
char *getDateUsec(char *dest, size_t size)
{
    // get time: gettimeofday()+localtime() invece di time()+localtime() per ottenere i usec
    struct timeval tv;
    gettimeofday(&tv, NULL);
    struct tm *tmp = localtime(&tv.tv_sec);

    // format stringa destinazione dest (deve essere allocata dal chiamante) e aggiunge i usec
    char fmt[128];
    strftime(fmt, sizeof(fmt), "%Y-%m-%d %H:%M:%S.%%06u", tmp);
    snprintf(dest, size, fmt, tv.tv_usec);

    // return stringa destinazione dest
    return dest;
}

// badGetDateUsec() - Genera una stringa con data e ora (usa i microsecondi) (versione bad)
char *badGetDateUsec(char *dest)
{
    // get time: gettimeofday()+localtime() invece di time()+localtime() per ottenere i usec
    struct timeval tv;
    gettimeofday(&tv, NULL);
    struct tm *tmp = localtime(&tv.tv_sec);

    // format stringa destinazione dest (deve essere allocata dal chiamante) e aggiunge i usec
    char fmt[128];
    strftime(fmt, sizeof(fmt), "%Y-%m-%d %H:%M:%S.%%06u", tmp);
    sprintf(dest, fmt, tv.tv_usec);

    // return stringa destinazione dest
    return dest;
}
Provate a compilare l'esempio dove, volutamente, ho sottodimensionato il buffer di destinazione: commentando la badGetDateUsec() e usando la getDateUsec(), funziona perfettamente, troncando l'output a 12 chars. Se, invece, si commenta la getDateUsec() e si usa la badGetDateUsec() il programma si schianta durante l'esecuzione. Provare per credere!

E già che siamo in argomento sprintf() un piccolo consiglio un po' OT: se dovete aggiungere sequenzialmente delle stringhe (in un loop, ad esempio) su una stringa base (per comporre un testo, ad esempio) non fate mai cosi:
char buf[256] = "";
for (int i = 0; i < 10; i++)
    sprintf(buf, "%s aggiunto alla stringa %d\n", buf, i);
il metodo qui sopra sembra funzionare, ma, in realtà, funziona quando c'ha voglia lui. Fate invece così:
char buf[256] = "";
for (int i = 0; i < 10; i++) {
    char tmpbuf[256];
    sprintf(tmpbuf, "%s aggiunto alla stringa %d\n", buf, i);
    sprintf(buf, "%s", tmpbuf);
}
E se non ci credete provate a passare il codice cattivo con un lint tipo cppchek, e il risultato sarà questo:
(error) Undefined behavior: Variable 'buf' is used as parameter and destination in s[n]printf().
Quanto sopra è, del resto, ben specificato nel manuale della sprintf():
C99 and POSIX.1-2001 specify that the results are undefined if  a  call
to  sprintf(), snprintf(), vsprintf(), or vsnprintf() would cause copy‐
ing to take place between objects that overlap  (e.g.,  if  the  target
string  array and one of the supplied input arguments refer to the same
buffer).
E, ovviamente, anche in quest’ultimo esempio, fatto per semplicità con la sprintf() ,sarebbe stato raccomandabile usare la snprintf(). E per oggi è tutto, sono già entrato in fase pre-natalizia, quindi cominciamo a rilassarci...
 

Ciao e al prossimo post!

martedì 19 novembre 2019

Amore e malloc
considerazioni sull'uso della malloc() in C

(...una premessa: questo post è un remake di un mio vecchio post. L'ho riadattato e "modernizzato" per pubblicarlo su quell'altro bel blog collettivo dove scrivo. Visto che le modifiche sono molte e (forse) interessanti, lo ripubblico anche qui. Questo potrebbe ripetersi in futuro...)
Boris: Sonja, e se Dio non esistesse?
Sonja: Boris Dimitrovic, stai scherzando?
Boris: E se fossimo solo un branco di gente assurda che corre intorno senza nesso o ragione?
Sonja: Ma se non esiste Dio la vita non avrebbe alcun significato, perché dovremmo continuare a vivere? Perché allora non suicidarsi?
Boris: Be', non facciamo gli isterici, potrei sbagliare. Io oggi mi uccido e domani "lui" concede un'intervista!
I film del Maestro Woody Allen sono sempre infarciti di disquisizioni filosofiche sul senso della vita e altre cosucce del genere. Il bel Amore e Guerra, non fa eccezione, e ci propone un quesito pesante che possiamo (ehm...) equiparare a uno dei grandi dilemmi che tormentano i programmatori C: ebbene si, oggi parleremo di allocazione dinamica della memoria: come, quando e perché usarla (uff, argomento pesantissimo...).
...e se la malloc() non esistesse?...
Allora: se facciamo una rapida ricerca con Google sulla famigerata malloc() e sul suo uso (provare con "why use malloc", per esempio), noteremo una notevole quantità di interrogativi sull'argomento che vanno dai semplici dubbi ("ma come si usa ?") ai dubbi esistenziali ("perchè si usa ?"). Beh, ci troviamo su un argomento pseudo-filosofico dovuto al fatto che, effettivamente, è possibile programmare a lungo in C senza mai usare la malloc()...

Cominciamo con una "premessa metodologica" per definire di cosa non stiamo parlando: chiunque (incluso il sottoscritto) si trovi con la necessità di scrivere (molto) rapidamente un piccolo programma, per testare rapidamente qualcosa - chissà per una urgenza improvvisa durante qualche manutenzione software critica e urgentissima (da terminare per ieri) - come lo scrive ? Ovviamente usando a man bassa variabili automatiche, array sovradimensionati ("memoria ce n'è tanta"), magari qualche variabile statica e (orrore, orrore) anche variabili globali. Ecco, insomma, uno di quei programmi che già mentre lo scrivi lo battezzi "temp_qualcosa.c" perché sai che dovrai rifarlo da capo appena possibile o dovrai cancellarlo per la vergogna (mica che qualcuno lo legge per sbaglio).

Ma oggi non parleremo di casi limite come quello della premessa qui sopra, parleremo di codice ben scritto, quello in cui abbiamo il tempo e i mezzi per decidere se usare o meno la malloc(). Procediamo: tutti sappiamo che il C ha una potente e flessibile gestione della memoria (è un linguaggio con i puntatori !), e scrivere in C facendo finta di non saperlo è un errore. Attenzione però: questo non significa che bisogna per forza usare la malloc() ("se no non sei un buon programmatore"), anzi è vero il contrario. All'interno di una funzione le variabili automatiche saranno sempre (e giustamente) la maggioranza, anche perché (quando è possibile e corretto) usare lo stack invece dello heap migliora le prestazioni del programma e, tra l'altro, lo rende più semplice da scrivere, leggere e manutenere. La allocazione dinamica della memoria è una operazione dispendiosa per il sistema operativo, aggiunge (evidentemente) complicazione al codice e aumenta la possibilità di errori (alzi la mano chi non si è mai dimenticato di usare la corrispondente free() di una malloc()...).

(...apro una parentesi: si, lo so, lo so, parlare solo di stack e heap è un po' generico, bisognerebbe parlare anche di Free Store, Global/Static, ecc. E poi, il termine stack non è mai esplicitamente usato nello Standard del C... ma sappiamo di cosa stiamo parlando, no? Non facciamo troppo i pedanti, come pedante basto e avanzo io...)

Ma allora quando e perché usare la malloc()? Scusate la risposta un po' lapalissiana, ma io direi:

    1) quando è indispensabile
    2) quando è meglio

E allora vediamo il primo punto di La Palice:

Quando è indispensabile? La allocazione dinamica è indispensabile in almeno due casi: il primo è quello delle funzioni che ritornano un indirizzo. Vediamo un semplice esempio di funzione che duplica una stringa (ce n'è uno quasi identico sul K&R):
// myStrdup() - crea un duplicato di una stringa
char *myStrdup(
    const char *src)
{
    // crea un duplicato di src (con spazio per il '\0')
    char *dest = malloc(strlen(src) + 1);
    if (dest != NULL)
        strcpy(dest, src);

    // ritorna il duplicato
    return dest;
}
Semplice, no? Inutile dire (ma lo dico lo stesso) che, in una funzione come questa, si deve per forza usare malloc() e non si può optare per usare l'indirizzo di un array automatico allocato sullo stack, che, proprio perché è sullo stack, si perde dopo il return (errore frequentissimo questo, ahimè).

Il secondo caso con scelta obbligata è quello delle linked list (o, più in generale, queues, stacks e oggetti similari). Vediamo un esempio molto semplificato (mentre un esempio completo compilabile e testabile lo potete trovare qui):
// nodo di una linked list singola con campo dati
typedef struct snode {
    int data;
    struct snode *next;
} node_t;

// addNode - alloca in testa a una lista un node con dati e puntatore al prossimo elemento
void addNode(node_t **head, int data)
{
    // alloca un nuovo node
    node_t *node = malloc(sizeof(node_t));
    node->data = data;
    node->next = *head;

    // assegna head lista al nuovo node
    *head = node;
}
E anche in questo caso risulta evidente che scrivere una linked list senza usare malloc() sarebbe veramente problematico.

E ora passiamo al secondo punto di La Palice:

Quando è meglio? Sicuramente tutte le volte che si devono maneggiare dei dati (tipicamente array di tipi semplici, array di strutture, testi, ecc.), di cui non si conosce a compile-time la dimensione: se non usassimo la malloc() bisognerebbe allocare dati sovradimensionati (per evitare che manchi spazio a run-time). E quest'ultimo dettaglio ci indica un altro punto: se dobbiamo maneggiare grosse quantità di dati non possiamo confidare troppo nello stack, il cui spazio non è infinito, anche se lavoriamo con moderni OS su macchine piene di memoria (e se programmiamo applicazioni embedded con forti limitazioni Hardware... ancora peggio).

Un esempio classico? Scriviamo una funzione che estrae il testo di un file e lo ritorna al chiamante:
// readFile() - legge un file di testo e lo ritorna in un buffer
char *readFile(
    const char *file_name)  // il nome del file
{
    // apre il file
    int fd;
    if ((fd = open(file_name, O_RDONLY)) < 0)
        return NULL;

    // ricava il size
    struct stat stbuf;
    if (fstat(fd, &stbuf) < 0)
        return NULL;

    // alloca il buffer
    char *buffer;
    off_t file_size = stbuf.st_size;
    if ((buffer = (char*)malloc(file_size + 1)) == NULL)
        return NULL;

    // legge il file
    size_t n_read;
    if ((n_read = read(fd, buffer, file_size)) <= 0)
        return NULL;

    // aggiunge il terminatore e ritorna il buffer
    buffer[n_read] = 0;
    return buffer;  // questo buffer deve liberarlo il chiamante con free()
}
Nel semplice esempio qui sopra il chiamante della funzione readFile() deve solo fornire il nome del file da leggere (e di cui a compile-time non si conosce né il nome né il size). La funzione alloca un buffer (del size necessario a contenere il testo) e lo ritorna riempito al chiamante che deve solo preoccuparsi di cancellarlo (con free()) quando non ne ha più bisogno (è un codice semplificato, eh!). 

E a questo punto ci vuole una bella tabella riassuntiva, per fornire una visione schematica e immediata degli argomenti trattati. Vai con la tabella!
Elementi da considerare per decidere se usare l'allocazione dinamica

Scope e Lifetime (visibilità e durata di una variabile in una funzione)
  stack valide solo dentro la funzione, si perdono quando la funzione esce (variabili 
        automatiche).
  heap  valide dalla creazione alla cancellazione, non si perdono quando la funzione 
        esce (variabili "mallocate").

Spazio
  stack lo spazio è limitato: da usare solo per oggetti piccoli (notare che 
        anche una funzione che usa piccole variabili automatiche potrebbe causare 
        stack-overflow se chiamata ricorsivamente).
  heap  lo spazio è molto grande: da usare per oggetti grandi che, altrimenti, 
        potrebbero causare stack-overflow.

Dimensionamento
  stack è preferibile per variabili con dimensione conosciuta a compile-time.
  heap  da usare per variabili con dimensione che si conoscerà a run-time.

Velocità
  stack è molto veloce, con tempo (quasi) costante e non frammenta la memoria.
  heap  è lento, con tempo non predicibile e frammenta la memoria (quindi bisogna
        prestare attenzione a dove e come usare la malloc(), per evitare di 
        appesantire il programma).

Affidabilità
  stack un eventuale stack-overflow è molto difficile da valutare e debuggare.
  heap  è facile intercettare/interpretare gli errori e prevenirli.

Casi Reali
  stack uso raccomandato per tutti i casi in cui scope e lifetime estesi non 
        sono una necessità.
  heap  uso indispensabile per scrivere funzioni che ritornano un indirizzo e per 
        linked-lists, stacks e queues. Uso raccomandato per oggetti molto grandi.
Con quanto detto fin'ora spero, almeno, di aver contribuito a fare un po' di chiarezza su quest'argomento controverso. Ah, nel post ho citato sempre, per semplicità, la malloc(), ma, come ben tutti sanno, per l'allocazione dinamica della memoria c'è una vera famiglia di funzioni (malloc(), calloc(), realloc() e free()) che permettono una notevole flessibilità e varietà di soluzioni.

Beh, con questo è tutto, e ricordate: per ogni malloc() c'è una free(): se i conti non vi tornano cominciate a preoccuparvi...

Ciao e al prossimo post!

domenica 20 ottobre 2019

Debugger? No, grazie!
considerazioni sull'uso del Debugger con il C

(...una premessa: questo è un articolo della serie "filosofica" come lo erano, ad esempio, "Variabili Globali? No, grazie!" oppure "No Comment". Quindi quella che esporrò è la mia personale filosofia, che non necessariamente è perfetta o è l'unica possibile. E lo preciserò di nuovo, come ho fatto nell'ultimo articolo: non sono il messia, sicuramente dico e faccio tante scemate, ma a volte ci azzecco, e quindi: i permalosi e/o privi di senso dell'umorismo sono esentati dalla lettura dell'articolo, e chi continua non dica che non l'avevo avvertito...)
Michele: Mi si nota di più se vengo e me ne sto in disparte o se non vengo per niente? Vengo. Vengo e mi metto, così, vicino a una finestra, di profilo, in controluce. Voi mi fate "Michele vieni di là con noi, dai" ed io "andate, andate, vi raggiungo dopo".
Nel bel Ecce Bombo, Michele, il protagonista, ha molti problemi esistenziali e filosofici, tipo quello sulla convenienza ad andare/non andare a una festa. E oggi di quello parleremo, di filosofia e gravi problemi della vita odierna, tipo: "Mi si nota di più se uso il Debugger o se non lo uso?". Per la precisione, tutto quello che segue da qui in avanti si riferisce all'uso "classico" o, se preferite, alla "accezione comune" del Debugger, e cioè: "Ho un blocco o un comportamento inaspettato di un programma: analizzo e risolvo il problema eseguendo passo-passo con un Debugger". Gli altri usi più profondi tipo l'analisi di un Core Dump o il Disassembly per usi (si spera leciti) di analisi di un codice eseguibile, non fanno parte di questa trattazione.
...mi si nota di più se uso il Debugger o se non lo uso?...
Allora, tanto per evitare fraintesi vi mostro subito quale è il mio pensiero al riguardo, e userò per l'occasione una interessante definizione circolare:
- Il Debugger serve solo a risolvere problemi facili.
- Se un problema è facile posso risolverlo senza usare il Debugger.
Il succo della definizione qua sopra è evidente: Il Debugger non serve a niente! Ovviamente non metto in dubbio che ci sono Debugger ben fatti e con alte prestazioni ma, semplicemente, io penso che la loro utilità sia molto limitata, perlomeno per il debugging classico ed elementare. E c'è un dato di fatto: io faccio l'Analista Programmatore (e lo faccio egregiamente, modestia a parte) da molto tempo e non uso praticamente mai il Debugger: sono un miracolato? Sono una mosca bianca? Sono solo molto fortunato? Boh, fate vobis...

Ed è già ora di fare qualche esempio reale:
  • Sei un Programmatore Esperto, stai lavorando su una tua applicazione e devi risolvere un problema: l'applicazione è tua, il codice l'hai scritto tu, quindi dovresti (anzi: devi) avere il controllo del flusso del programma, e quindi hai tutti i mezzi per capire quello che sta succedendo. Se per seguire "il giro del fumo" hai bisogno di scomodare un Debugger... mai pensato di cambiare lavoro? 
  • Sei un Programmatore Esperto, stai lavorando su una applicazione non tua, ma ben scritta e ben documentata, e devi risolvere un problema: l'applicazione è, ripeto, ben scritta e ben documentata, quindi dovresti (e vabbè, tolgo il devi) avere ugualmente il controllo del flusso, e quindi hai tutti i mezzi per capire quello che sta succedendo. Se per seguire "il giro del fumo" hai bisogno di scomodare un Debugger... mai pensato di fare un piccolo sforzo per leggere attentamente codice e documentazione e acquisire il controllo totale del flusso del programma? Ti potrebbe tornare molto utile nel prosieguo del lavoro...
  • Sei un Programmatore Esperto, stai lavorando su una applicazione non tua, scritta da cani e senza documentazione, e devi risolvere un problema: a parte inviare le dovute maledizioni al pregiato autore del codice-ciofeca su cui stai lavorando, cosa puoi fare? E vabbè, sarò generoso: in questo caso ti permetto di usare un Debugger, ma non te lo consiglio: anche in tali condizioni sarebbe molto utile cercare di acquisire il controllo completo e, magari, approfittare per documentare e migliorare il codice. Comunque a fine lavoro ricordati di inviare le dovute maledizioni, mi raccomando... e già che ci sei: riserva una rispettosa maledizione anche a un tuo (eventuale) capo se ti ha detto "risolvi solo il problema, è inutile che migliori e documenti il codice, non abbiamo tempo": il tuo capo è colpevole come l'autore del codice-ciofeca, anzi è un suo complice.
Avete notato che negli esempi precedenti non ho specificato se il problema da risolvere era piccolo o grande? Beh, se il motivo non vi è chiaro vi consiglio di rileggere la definizione circolare con cui ho introdotto l'argomento. E notate bene: ho evidenziato il termine "Programmatore Esperto", perché è di questo che stiamo parlando. I Programmatori Inesperti o Novizi, sono un caso a parte, e hanno una Dispensa Papale sull'uso del Debugger, visto che per loro potrebbe risultare istruttivo. Poi, se poi continuano a usarlo anche quando passano allo stadio di Programmatore Esperto, per loro è pronta una Scomunica Papale (con relativa retrocessione allo stato di Novizio). So di cosa parlo, sono stato un Programmatore Novizio anch'io...

Ed ora definiamo il concetto di problema facile o difficile: molti dicono: "il Debugger bisogna usarlo sempre, forse solo in alcuni casi di complessi programmi multithread un normale Debugger non aiuta molto, e bisogna seguire un approccio diverso"... uhm, sbaglio o rientriamo nella definizione circolare introduttiva? Io lavoro praticamente sempre su "complessi programmi multithread": sono solo sfortunato o quello di risolvere problemi complicati è, giustamente, il compito dei Programmatori Esperti? Senza offesa eh, ma se vi danno da fare solo lavori facili in cui potete usare liberamente (anche se non serve) il vostro amato Debugger, forse non vi considerano Esperti...

E poi veniamo alla parte più strana, quella delle leggende urbane, quelle che "Non usi il Debugger? Allora sei della vecchia generazione!". In questo caso mi tocca, mio malgrado, chiedere aiuto a quell'irascibile di mio cuggino, che ha sempre qualche aneddoto da raccontare. True Story? Può darsi...
un programmatore: Ah, ma tu non usi il Debugger perché sei della vecchia generazione, tu sei abituato a usare solo un editor e un compilatore e via!
mio cuggino: Non è vero: ai miei tempi, quando scolpivo il codice nella roccia tra una caccia al Mammut e l'altra, usavo già il Debugger, ma solo quando era strettamente necessario, esattamente come faccio oggi. 
un programmatore: Ma come? Esistevano già i Debugger? 
mio cuggino: Uhm... fai un po' te, su UNIX negli anni '70 esistevano già DB, ADB e SDB. Negli anni '80 poi sono usciti DBX e (nel 1986) GDB. Per quanto riguarda il MS-DOS, nel 1989 era già disponibile il famosissimo Borland Turbo Debugger. 
un programmatore: DB, ADB, SDB, DBX, GDB, Turbo Debugger... mai sentiti, ma di cosa stai parlando?
mio cuggino: E vabbè, continuiamo così, facciamoci del male... 
Per concludere citerò una bella frase che è molto usata ultimamente: "Non sono d'accordo con quello che dici, ma difenderò fino alla morte il tuo diritto a dirlo". È, spesso, attribuita erroneamente a Voltaire ma è invece di una sua biografa, Evelyn Beatrice Hall: ma questo dettaglio non importa, quello che importa è il potentissimo contenuto, che io vorrei sempre applicare. Quindi, lungi da me criticare i colleghi che lavorano in modo diverso dal mio, anzi, quando posso li appoggio. Le mie azioni e i miei articoli sono (o vorrebbero essere) pura critica costruttiva, e devo dire (con somma soddisfazione) che alcuni colleghi, lavorandomi affianco, hanno cambiato un po' lo stile (tipo smettere di essere schiavi del debugger!). Comunque ho il massimo rispetto per chi ha altre convinzioni e non vuole cambiarle. E ho detto tutto!

(...apro una parentesi un po' OT: la frase qui sopra è adatta a esperimenti sociologici interessanti, tipo attribuirla apposta a Voltaire per vedere se qualcuno, invece di soffermarsi sulla bellezza della frase, ti riprende unicamente e furiosamente per l'errore di attribuzione. Diffidate di questi irosi tuttologi che "guardano il dito invece della luna": frequentano blog, fori e conferenze sperando, ansiosamente, in qualche errore del relatore che gli fornisca una scusa per evidenziare che solo loro detengono la sapienza universale. Gentaccia...)

Beh, credo che con questo posso considerare concluso un altro capitolo della mia personale lotta ai "Luoghi Comuni dell'Informatica", e ne seguiranno altri. E se volete chiamarmi "Il Pedante Informatico" fate pure, in effetti lo sono...

Ciao, e al prossimo post!

mercoledì 18 settembre 2019

L'invasione dei Multithread
considerazioni sulla Programmazione Multithread in C

(...una premessa: questo è il seguito naturale (e promesso) dell'articolo Errno Miei (correre a rileggerlo, prego). Potrebbe apparire un po' polemico (ebbene si, lo è), ma prendetelo con le pinze: io non sono un buonista e preferisco dire "pane al pane e vino al vino", e questo potrebbe urtare qualcuno (che, in tal caso, direi che se lo merita), però potrebbe alimentare la fame di conoscenza che è il fiore all'occhiello di tutti gli appassionati di informatica, e sono sicuro che quasi tutti voi disponete di questa caratteristica. Ah, e ci tengo a precisare: non sono il messia, sicuramente dico e faccio tante scemate, ma a volte ci azzecco, e quindi: i permalosi e/o privi di senso dell'umorismo sono esentati dalla lettura dell'articolo, e chi continua non dica che non l'avevo avvertito...)
Dr. Bassett: Allora, che ne pensa, dottor Hill?
Dr. Hill: Secondo me si tratta di un incubo. 
Dr. Bassett: Altro non può essere. Semi che provengono da altri mondi e che generano esseri umani! Mh, roba da pazzi!
Nel mitico L'invasione degli ultracorpi si narra di una invasione di extraterrestri che si sostituiscono agli umani, e i sostituti sono molto somiglianti ma smascherabili analizzandone il comportamento. Ecco, a volte ho l'impressione che, nel nostro mondo (informatico) reale, stiamo assistendo a un'altra invasione, quella dei (falsi) "Programmatori specialisti in Multithread" che non sono quei programmatori specializzati nel fare 10 lavori alla volta (in quello, ahimè, siamo tutti specializzati, chi più chi meno...), ma sono quei programmatori che sanno scrivere applicazioni multithread.
...scappiamo, mica che ci prendono per Programmatori Multithread...
Oramai gli annunci di ricerca sono quasi tutti del tipo "Cercasi A.P. Junior (anche primo impiego) con 10 anni di esperienza in Applicazioni Multithread", e anche l'annuncio dell'arrivo in ufficio di un nuovo collega è sempre del tipo "domani arriva tizio: è uno specialista in Multithreading... no, non fa 10 lavori per volta". Poi ti capita (true story capitata a mio cuggino) che metti le mani sul codice scritto da uno di questi specialisti e ti senti obbligato a chiedere:
mio cuggino: Scusa, solo per curiosità, com'è che hai scritto questa enorme applicazione multithread senza mai usare una funzione della famiglia "nomefunzione_r"?
specialista multithread: "nomefunzione_r"? Uhm... in questo momento mi sfugge il significato... A cosa servono?
mio cuggino: ...e poi ho visto che hai usato un po' di variabili globali senza proteggerle...
specialista multithread:  Ah, le variabili globali... dici che è meglio non usarle?
mio cuggino: E vabbè, continuiamo così, facciamoci del male...
Il problema è che, per essere un vero programmatore multithread non basta aver scritto una (sola) volta del "codice che faceva uso di ben due thread", e non basta nemmeno (per la proprietà transitiva) "avere stretto la mano a uno che ha parlato una volta con un vero specialista in multithreading": bisogna avere le basi e averci sbattuto la testa (veramente) per qualche annetto (...hey, ci sono passato anch'io quando ero ggiovane: ricordatevi che nessuno nasce imparato...). Adesso: visto che le basi sono tante (ho detto "race condition"? oppure "deadlock"? oppure bla, bla, bla) e non è che con un articolo scritto da un volenteroso e umile (ehm...) programmatore si possa affrontarle tutte, ma, tanto per cominciare, si può iniziare chiarendo due concetti misteriosi e fondamentali: sto parlando di "Thread safety" e "Reentrancy".

Ebbene, la teoria dice che:
Thread safety: una funzione è thread-safe se può essere eseguita da più thread in modo sicuro (ovvero ottenendo sempre il risultato che ci si aspetta), anche se le chiamate si verificano contemporaneamente su più thread e si manipolano dati condivisi. 
Reentrancy: una funzione è rientrante se può essere interrotta in qualsiasi momento durante la sua esecuzione e quindi richiamata in modo sicuro ("rientrata") prima che le sue precedenti invocazioni completino l'esecuzione.
(...le definizioni qui sopra le potete trovare in rete in versioni più o meno simili: questo è un argomento un po' controverso, quindi per alcuni queste definizioni potrebbero risultare limitate, oppure eccessive, oppure non completamente esatte. Comunque credo che rendano bene l'idea...)

Come si nota i due concetti descritti sono simili e parzialmente sovrapponibili, ma si può dire che la "Reentrancy" sia, come dire? A un livello superiore, perché è una specie di "Thread safety" applicabile anche in applicazioni single-thread, e difatti è un concetto più antico, applicato da molto tempo (perfino in sistemi come l'orribile MS-DOS), visto che quando si lavora con gli Interrupt (o coi Segnali) le funzioni di servizio invocate devono essere rientranti... ma non voglio mettere troppa carne al fuoco: parleremo di Interrupt, Segnali e routine di servizio in un altro post, promesso.

Riepiloghiamo: una funzione thread-safe può essere chiamata tranquillamente su più thread, mentre che una funzione rientrante può fare le stesse cose e, in più, ti permette di interrompere e richiamare in sicurezza più istanze della stessa funzione all'interno dello stesso thread. Quindi, se usiamo sempre funzioni rientranti siamo al sicuro? Beh, in pratica si, ma in teoria no, perché in realtà abbiamo quattro possibilità (...eh si: la magica potenza del 2...):
  1. Funzioni thread-safe e rientranti
  2. Funzioni thread-safe e non rientranti
  3. Funzioni non thread-safe e rientranti
  4. Funzioni non thread-safe e non rientranti
il caso 1 (thread-safe e rientranti) è, fortunatamente, il più probabile e frequente: come già anticipato nel post Errno Miei esiste una famiglia di funzioni "nomefunzione_r" della libc (tipo la strerror_r()) dove la desinenza "_r" indica "rientrante", e sono, per nostra fortuna, scritte come si deve, quindi sono anche thread-safe. Uno specialista in multithreading sa che nelle applicazioni che scrive deve sempre preferire l'uso di queste funzioni. Un'altra cosa che deve sapere lo specialista in multithreading è che anche le funzioni che scrive lui nell'applicazione devono essere del tipo 1 (thread-safe e rientranti) o, come minimo, del tipo 2 (solo thread-safe). E, infine, uno specialista non scriverà mai una funzione del tipo 3 o del tipo 4, e se è costretto a a usare una funzione di libreria non thread-safe la dovrà usare con le dovute accortezze (più avanti vedremo qualche esempio).

Le linee guida che deve seguire uno specialista quando scrive una funzione che userà in una applicazione multithread sono varie, ma per semplificare possiamo descrivere le due fondamentali:
  1. Non usare mai variabili globali o statiche (che sono delle globali travestite).
  2. Non chiamare mai (internamente alla funzione) altre funzioni non thread-safe (logico, no?).
E ora possiamo vedere in pratica quanto detto finora: scriveremo una semplice funzione per trasformare in minuscolo una stringa, e lo faremo MALE (non thread-safe e non rientrante), BENINO (thread-safe e non rientrante) e BENE (thread-safe e rientrante). Sono, evidentemente, esempi stra-semplificati, senza controllo degli errori, ecc. (ma sono solo degli esempi, cosa pretendete?). Vai col codice!

// MALE: versione non thread-safe e non rientrante
char *strLower1(const char *src)
{
    // questo buffer statico rende strLower() non rientrante
    static char buf[256];

    // loop per scrivere la versione lower sulla destinazione
    int i = 0;
    while (*src)
        buf[i++] = tolower(*src++);  // Ok, tolower() è thread-safe

    buf[i] = 0;     // aggiungo il terminatore

    // ritorno il buffer destinazione
    return buf;
}

// BENINO: versione thread-safe e non rientrante (con Thread Local Storage)
char *strLower2(const char *src)
{
    // questo buffer statico rende strLower() non rientrante
    static __thread char buf[256];  // con __thread il buffer è locale ad ogni thread

    // loop per scrivere la versione lower sulla destinazione
    int i = 0;
    while (*src)
        buf[i++] = tolower(*src++);  // Ok, tolower() è thread-safe

    buf[i] = 0;     // aggiungo il terminatore

    // ritorno il buffer destinazione
    return buf;
}

// BENINO: versione thread-safe e non rientrante (con mutex)
pthread_mutex_t buf_lock;   // con un mutex proteggiamo gli accessi al buffer statico

char *strLower3(const char *src)
{
    // questo buffer statico rende strLower() non rientrante
    static char buf[256];

    // loop per scrivere la versione lower sulla destinazione
    pthread_mutex_lock(&buf_lock);      // lock del buffer
    int i = 0;
    while (*src)
        buf[i++] = tolower(*src++);      // Ok, tolower() è thread-safe

    buf[i] = 0;     // aggiungo il terminatore
    pthread_mutex_unlock(&buf_lock);    // unlock del buffer

    // ritorno il buffer destinazione
    return buf;
}

// BENE: versione thread-safe e rientrante
char *strLower4(char *dest, const char *src)
{
    // salvo localmente dest
    char *my_dest = dest;

    // loop per scrivere la versione lower sulla destinazione
    while (*src)
        *my_dest++ = tolower(*src++);    // Ok, tolower() è thread-safe

    *my_dest = 0;   // aggiungo il terminatore

    // ritorno il dest salvato
    return dest;
}
Credo che gli abbondanti commenti nel codice abbiano già chiarito come e perché le varie versioni mostrate hanno comportamenti differenti a livello di sicurezza. Come si nota l'unica versione che rispetta le due linee guida fondamentali riportate sopra è la strLower4(), che ha l'unico inconveniente di avere una interfaccia (e quindi un uso) diverso, visto che bisogna passargli il buffer destinazione: questo che il chiamante si deve occupare di allocare e passare la destinazione è il classico "trucco" usato nelle versioni "_r" della libc (l'avevamo già visto a proposito della strerror_r(), ricordate?).

E, come al solito, il MALE sta nelle variabili globali (e anche di questo ne abbiamo già parlato): se proprio non possiamo farne a meno dobbiamo proteggerle, come negli esempi, usando Thead Local Storage o, ad esempio, un mutex. Faccio notare che strLower2() e strLower3() sono simili (thread-safe e non rientranti) ma strLower3() è peggio: usando (per sbaglio!) strLower2() in una routine di servizio sotto Interrupt (o Segnale) avremmo solo degli errori, mentre con strLower3() potremmo avere anche un deadlock (il mutex viene bloccato alla prima interruzione e una seconda interruzione lo lascia bloccato indefinitamente), quindi attenzione!

Qualcuno avrà notato che non ho fornito un esempio del caso 3 (non thread-safe e rientranti): il fatto è che solo con un codice molto contorto si può arrivare a ottenere questo (cioè: bisogna proprio farlo apposta!), quindi direi che è meglio non fornire un esempio che fornirne uso assurdo.

La funzione strLower3() ci può essere utile come base di descrizione su come  fare a inserire funzioni di libreria non thread-safe che, per qualche strano motivo, non possiamo evitare di di usare: possiamo proteggerle con un mutex, più o meno così:
// versione thread-safe e non rientrante (con una ipotetica tolower() non thread-safe)
pthread_mutex_t fun_lock;   // con un mutex proteggiamo l'uso di tolower()

char *strLower5(char *dest, const char *src)
{
    // salvo localmente dest
    char *my_dest = dest;

    // loop per scrivere la versione lower sulla destinazione
    pthread_mutex_lock(&fun_lock);      // lock della funzione
    while (*src)
        *my_dest++ = tolower(*src++);    // e se tolower non fosse thread-safe?

    *my_dest = 0;   // aggiungo il terminatore
    pthread_mutex_unlock(&fun_lock);    // unlock della funzione

    // ritorno il dest salvato
    return dest;
}
Ovviamente questo sopra è un esempio sbagliato, visto che tolower() è sicuramente thread-safe, ma serve ugualmente a rendere l'idea. E direi che per oggi può bastare. Come anticipato, in un futuro articolo (non necessariamente il prossimo) completeremo il discorso con alcuni esempi di uso di funzioni rientranti sotto Interrupt (o Segnali). Non trattenete il respiro nell'attesa, mi raccomando...

Ciao, e al prossimo post!

giovedì 8 agosto 2019

Lo chiamavano Jeeg OpenSSL
come scrivere TCP Server e Client con OpenSSL in C - pt.3

Enzo/Jeeg: T'è annata male, Zingaro! Si vòi diventà famoso te conviene torna' a fa' l'imitazione der Grande Fratello! 
Fabio/Zingaro: Era Buona Domenica, cojone!
E ci risiamo un'altra volta: anche per questa terza puntata di Lo chiamavano Jeeg Robot (oops: Lo chiamavano Jeeg OpenSSL) abbiamo due notizie, una buona e una cattiva. La buona è che questo post completa (finalmente) il discorso della libreria MySSL. La cattiva notizia è che ho, finalmente, scoperto che diventare ricchi e famosi col Software è veramente molto complicato... Un consiglio: iscrivetevi, invece, al casting del Grande Fratello, quello è il futuro! (no, è mejo Buona Domenica, cojone!).
...e usa sta' MySSL cojone!...
(...si lo so, lo so: nell'ultimo articolo avevo promesso che avremmo proseguito e approfondito il discorso errno/strerror disquisendo di reentrant/thread-safe: ma siamo ad agosto, e l'articolo promesso potrebbe risultare troppo pesante per chi ha già la testa (e anche il corpo, spero) in vacanza... Vi/mi faccio un favore: ne riparleremo a Settembre, così oltre alla depressione post-vacanze vi beccherete anche un articolo pesantone in grado di schiantarvi del tutto...

Allora: perché una terza puntata di Lo chiamavano Jeeg OpenSSL? Se ben ricordate (e se non vi ricordate questa è una occasione imperdibile per rileggere la parte 1 e la parte 2 della serie) abbiamo lasciato la saga con la proposta di una semplice libreria per scrivere in maniera user-friendly applicazioni che usano OpenSSL (che è in effetti un po' ostica da usare così com'è). Era una piccola libreria di smart-wrapper per alcune delle funzioni OpenSSL e l'avevo scritta ad-hoc per il blog, usando una struttura ultra-semplificata: era composta solo da due file, myssl.c e myssl.h, e doveva essere compilata e linkata direttamente con la applicazione che la usava (ad esempio un SSL Server).

Per questa terza parte del post ho deciso di rendere la libreria un po' più professionale, come struttura e come distribuzione, quindi l'ho rivoluzionata (abbastanza) e l'ho trasformata in un repository GitHub. La potete trovare qui (...in realtà già ai tempi della seconda parte del post il mio ottimo collega Andrea Simone Costa mi aveva suggerito di farlo, ma avevo rimandato la cosa a tempi più propizi, tipo ora...). Vi riporto subito il file README.md del repository GitHub così non ci perdiamo in chiacchiere (per l'occasione tradotto in Italiano, ma su GitHub lo trovate in Inglese): 
MySSL 
Un'interfaccia user-friendly per la libreria OpenSSL

Panoramica
----------
La libreria MySSL è una semplice interfaccia per la libreria OpenSSL che 
consente una scrittura user-friendly (in C o C++) di SSL Server e SSL Client.

Note
----
- Licenza GNU MySSL (omissis)
- Licenza OpenSSL (omissis)
- La libreria "OpenSSL" di riferimento è la ver.1.0.2g
- Attualmente la libreria MySSL supporta solo sistemi Linux

Installazione
-------------
Questa è attualmente una versione beta e utilizza un Makefile molto semplice: 
quindi per generare e testare la libreria MySSL è necessario eseguire alcuni 
passaggi manuali:

1. Posizionarsi nella directory src e generare la libreria con 
   "make clean && make". Questo genera una libreria condivisa (libmyssl.so) e 
   la copia nella directory tests/lib. Il header-file myssl.h viene copiato 
   nella directory tests/include.
2. Posizionarsi nella directory tests e generare i due programmi di test
   (sslserver e sslclient) con "make clean && make".
3. Leggere il paragrafo "Testing" di questo file per vedere come eseguire un 
   semplice test della libreria MySSL utilizzando due terminali.
4. Notare che in questa versione beta la libreria non viene installata 
   automaticamente, quindi per eseguire il test è necessario installare 
   libmyssl.so nel sistema oppure è possibile eseguire un'installazione 
   temporanea in entrambi i due terminali in questo modo:

   export LD_LIBRARY_PATH = "~/path-di-myssl-package/tests/lib"

La prossima versione della libreria MySSL utilizzerà Autotools per generare e 
installare automaticamente e per rendere il pacchetto portatile su molti sistemi
Unix-like.

Testing
-------
Due test sono forniti nelle directory tests/server e tests/client e si può 
modificare liberamente il codice sorgente in base alle proprie esigenze (è Free
Software :).

I due programmi di test sono un SSL Server e un SSL Client che utilizzano la 
libreria MySSL e, oltre a testare la libreria, possono servire da esempio su 
come scrivere codice usando la libreria MySSL.

Le due directory di test forniscono esempi di base (e funzionanti) dei 
certificati SSL utilizzati dalla libreria MySSL per funzionare.

Per un rapido test della libreria MySSL, è possibile eseguire i seguenti 
programmi in due shell aperte nelle directory tests/server e tests/client:

1. ./sslserver 8888
2. ./sslclient 127.0.0.1 8888

TODO list
---------
- Generazione e installazione della libreria con Autotools
- Documentazione
- Supporto Windows
Ecco, penso che il README.md sia più che sufficiente a descrivere la nuova MySSL (l'ho scritto apposta!), quindi approfitto dell'occasione per descrivere, invece, i passi che ho eseguito per trasformare una libreria con struttura semplice in una più complessa per GitHub: questa guida è generica ed è, ovviamente, molto soggettiva, ma secondo me potrebbe essere un buon spunto per tutti quelli che si accingono a fare una operazione del genere. Vediamo i passi che ho eseguito:
  1. Ho diviso il "sorgentone" originale myssl.c in più sorgenti, uno per ogni funzione della libreria (sslread.c, sslwrite.c, ecc.).
  2. Ho diviso il "headerone" originale myssl.h in due header, myssl.h (pubblico) e myssl-private.h (privato): i sorgenti della libreria MySSL includono entrambi i file, mentre gli applicativi che usano MySSL includeranno solo l'header pubblico.
  3. Ho aggiunto opportuni header in testa a tutti i file con varie informazioni descrittive (seguendo un po' lo stile dei manuali Unix/Linux e lo standard POSIX).
  4. Ho aggiunto opportuni header in testa a tutte le funzioni della libreria (seguendo un po' lo stile dei manuali Unix/Linux e lo standard POSIX).
  5. Ho tradotto tutto (header e commenti nei sorgenti) in inglese: sono tutt'altro che un esterofilo però, visto che GitHub è un sito di uso globale, bisogna dare una opportunità di lettura diretta a tutto il pubblico potenziale.
  6. Ho creato una struttura di directory semplice e funzionale: src (sorgenti della libreria) e tests (applicazioni di test): per entrambe ho scritto un Makefile. Il Makefile in src genera la libreria, mentre quello in tests genera le due applicazioni di test (un SSL Server e un SSL Client).
  7. Ho scritto i "canonici" file GitHub (README.md, LICENSE e .gitignore) e li ho copiati nella root-directory della libreria.
  8. E, finalmente, ho copiato tutto nel repository GitHub. Missione compiuta!
That's all folks! E buone vacanze!

Ciao e al prossimo post!