Nei titoli e nei testi troverete qualche rimando cinematografico (eh 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 mando avanti anche una versione in Spagnolo (e, normalmente, i post sono disponibili un po' in ritardo rispetto alla versione in Italiano...). Potete trovarlo su El arte de la programación en C. Buona lettura.

domenica 4 giugno 2017

Il buono, il brutto, il VLA
come usare i Variable Length Arrays in C - pt.2

Dunque, dove eravamo rimasti? Ah si, nell'ultimo post (che avete appena riletto, vero?) avevamo approvato (con riserva) i VLAs, che sono facili da usare, utili e con ottime prestazioni, ma allora... perché ho detto che sono adatti al ruolo del cattivo nel mitico "Il buono, il brutto, il cattivo"?

non fidatevi del VLA, parola del buono!
Presto detto: oltre ai (notevoli) pro ci sono anche alcuni (pesanti) contro. Prima di seguire ricordiamoci sempre che un VLA si alloca dinamicamente nello stack come una variabile automatica con scope limitato al blocco di codice dove avviene l'allocazione: dopodiché i (principali) possibili problemi sono:
  1. la gestione degli errori è problematica, perché non c'è maniera di sapere se il VLA è stato allocato bene o ha provocato un stack overflow (in questo caso provoca effetti simili a quelli di un errore per ricorsione infinita).
  2. il size del VLA si decide a run-time, quindi il compilatore deve fare dei giochi un po' strani: in base all'implementazione è possibile che una parte (anche importante) dello stack di una funzione venga riservato per un VLA, limitando molto la memoria locale disponibile. Quindi lo stack overflow è sempre dietro l'angolo.
  3. la portabilità del codice va un po' a farsi benedire: il codice diventa molto compiler-dependent e, soprattutto, visto che una buona fetta di programmatori C scrivono anche codice per sistemi embedded (dove lo stack è, spesso, limitato) risulta complicato il porting di funzioni da applicazioni normali a applicazioni embedded. Funzioni che, magari, smetterebbero di funzionare per motivi misteriosi (beh, neanche tanto misteriosi).
  4. dulcis in fundo: forse per i motivi appena elencati (o per altri ancora) da C11 in avanti i VLAs sono opzionali e subordinati a una variabile del compilatore __STDC_NO_VLA__: brutto segno.
Che fare allora? Meglio non usarli o usarli con le precauzioni del caso, anche perché le alternative non mancano. Cattivo trovato!

E adesso ci tocca cercare qualcuno che sia adatto ai ruoli del buono e del brutto. Ecco, per il buono non c'è problema, il candidato ideale è la cara, buona, vecchia malloc() che è sempre una garanzia ed è uscita molto bene dal test. Sulla malloc() è inutile dilungarci, è un punto fermo del C e ne abbiamo già parlato abbondantemente qui.

E il brutto? Beh, per cercare uno adatto dovremo, ahimè, addentrarci nel lato oscuro della forza, e cioè in territorio C++...

(apro una parentesi: non parlo mai di argomenti che non conosco, perché penso che sia stupido farlo. Per fare un esempio: io non capisco niente di moto e, vi assicuro, nessuno ha mai avuto l'onore di sentirmi disquisire sul mondiale di MotoGP. Seguo una filosofia, che, sfortunatamente, non è seguita da molta gente, è cioè: "meglio stare zitti che parlare solo per dare aria alla bocca". Proprio in virtù di questa coerenza penso di avere i titoli per parlare del C++: lo uso in parallelo al mio amato C da quasi trenta (!) anni, e, modestia a parte, penso si saperlo usare bene. Quindi ne posso disquisire, nel bene e nel male. Chiudo la parentesi).

Allora, ho ripreso pari pari l'esempio C del post precedente e (facendo il minimo sindacale di modifiche) l'ho trasformato in codice C++, per poter, così, aggiungere un test nuovo che usa std::vector (questo è un oggetto particolarmente caro ai C++ lovers, che lo usano anche per condire l'insalata). Per non ripetere tutto il codice dello scorso post vi riporto solo il main() e la nuova funzione di test aggiunta (il resto è, praticamente, invariato). Vai col codice!
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <vector>

#define MYSIZE  1000000

// variabile dummy per evitare lo svuotamento totale delle funzioni usando G++ -O2
int avoid_optimization;

// prototipi locali
void testVLA(int size);
void testMallocVLA(int size);
void testStackFLA(int dum);
void testHeapFLA(int dum);
void testVectorVLA(int size);
void runTest(int iterations, void (*funcptr)(int), int size, const char *name);

// funzione main()
int main(int argc, char* argv[])
{
    // test argomenti
    if (argc != 2) {
        // errore: conteggio argomenti errato
        printf("%s: wrong arguments counts\n", argv[0]);
        printf("usage: %s vla iterations [e.g.: %s 10000]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // estrae iterazioni
    int iterations = atoi(argv[1]);

    // esegue test
    runTest(iterations, &testVLA, MYSIZE, "testVLA");
    runTest(iterations, &testMallocVLA, MYSIZE, "testMallocVLA");
    runTest(iterations, &testStackFLA, 0, "testStackFLA");
    runTest(iterations, &testHeapFLA, 0, "testHeapFLA");
    runTest(iterations, &testVectorVLA, MYSIZE, "testVectorVLA");

    // esce
    return EXIT_SUCCESS;
}

// funzione testVectorVLA()
void testVectorVLA(
    int size)       // size per std::vector
{
    std::vector<int> vectorvla(size);

    // loop di test
    for (int i = 0; i < size; i++)
        vectorvla[i] = i;

    // istruzione per evitare lo svuotamento totale della funzione usando G++ -O2
    avoid_optimization = vectorvla[size / 2];
}
(Nota: grazie alla segnalazione di Ponchietto, un lettore attento e preparato, mi sono reso conto che avevo dimenticato di aggiungere, in ogni funzione di test, una semplice istruzione che usasse l'array creato, così da evitare che l'ottimizzatore del G++ azzerasse il contenuto della funzione stessa (visto che l'array non lo usava nessuno). Ovviamente così anche i risultati dei test con ottimizzazione cambiano. Adesso il post ha codice e risultati rinnovati)

Compilando (con/senza ottimizzazioni) ed eseguendo questo codice i risultati sono i seguenti:
aldo@ao-linux-nb:~/blogtest$ g++ vlacpp.cpp -o vlacpp
aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000
testVLA       -  Tempo trascorso: 4.274441 secondi
testMallocVLA -  Tempo trascorso: 3.641508 secondi
testStackFLA  -  Tempo trascorso: 4.340430 secondi
testHeapFLA   -  Tempo trascorso: 4.312986 secondi
testVectorVLA -  Tempo trascorso: 10.660610 secondi
aldo@ao-linux-nb:~/blogtest$ g++ -O2 vlacpp.cpp -o vlacpp
aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000
testVLA       -  Tempo trascorso: 0.768702 secondi
testMallocVLA -  Tempo trascorso: 0.694418 secondi
testStackFLA  -  Tempo trascorso: 0.682241 secondi
testHeapFLA   -  Tempo trascorso: 0.694299 secondi
testVectorVLA -  Tempo trascorso: 1.364321 secondi
Come si è comportato std::vector? direi che i numeri parlano da soli... va beh, mettiamola giù in maniera diplomatica: diciamo che abbiamo due notizie, una buona e una cattiva:
  • la buona notizia è che il C++ è efficiente come il C (e su questo non avevo dubbi), infatti il nostro programma C trasformato in C++ ottiene (nei primi quattro test) le stesse prestazioni (andate a controllare la, se non ci credete, eh!).
  • la brutta notizia è che il C++ è efficiente come il C, ma solo se lo usate come il C, quindi niente STL e ammennicoli vari.
(apro un altra parentesi: ovviamente la brutta notizia qui sopra non deriva solo dal semplice test proposto in questo post: deriva da anni ed anni di osservazioni ed uso intensivo di entrambi i linguaggi, ci mancherebbe solo. Chiudo la parentesi).

Senza dilungarmi troppo (magari un giorno scriverò un post specifico sull'argomento) vi espongo la mia opinione: il C++ è un grande linguaggio potente, efficiente ed espressivo (è parente stretto del C!), con cui si può scrivere del Software di alta qualità. Ma i risultati migliori (perlomeno in termini di prestazioni e fluidità del codice) si ottengono usandolo per quello che era stato concepito originalmente, e cioè come un C a oggetti. La piega che ha preso in seguito (da quando è caduto nelle mani dei committee ISO) non mi piace e non mi convince... ma, fortunatamente (e questo è importante), continua a poter essere usato nella sua essenza, quella che permette di scrivere a oggetti usando un linguaggio (quasi) identico al C (e questo si aggancia alla buona notizia qui sopra. Ovviamente, se prestazioni e fluidità del codice non si considerano qualità importanti, allora tutte queste considerazioni perdono di significato...).

Ah, una ultima precisazione per chi si è sorpreso del codice C++ (qui sopra) che include un VLA: è una gentile offerta dal nostro amato GCC (nella sua incarnazione G++). Quindi è un estensione del linguaggio fornita dal compilatore, visto che i VLAs non fanno parte del C++ standard (neanche nelle ultime versioni C++11 e C++14).

Nel prossimo post, per chiudere il cerchio, parleremo di un parente stretto dei VLAs, e cioè della funzione alloca(). Sarà un altro buono, un altro brutto o un altro cattivo?

Ciao e al prossimo post!

domenica 14 maggio 2017

Il buono, il brutto, il VLA
come usare i Variable Length Arrays in C - pt.1

La referenza cinematografica di questo mese calza proprio a pennello: un Variable Length Array (VLA per gli amici) sarebbe perfetto per fare la parte del cattivo nel capolavoro "Il buono, il brutto, il cattivo" del mitico Sergio Leone. E alla fine del (prossimo) post sarà chiaro il perché.
...ciao sono un VLA: inizia a preoccuparti...
I VLAs sono una cosa relativamente nuova del C: sono stati introdotti nel C99, e sono, apparentemente, il sogno fatto realtà del mondo C: "Finalmente gli array con dimensione variabile! Ah, se li avessi avuti prima del '99!" Allora: l'idea è semplice, con un VLA potete scrivere cosucce tipo queste:
void myVla(
    int size1,
    int size2)
{
    // il mio VLA di int
    int ivla[size1];

    // fai qualcosa con il VLA di int
    ...

    // il mio VLA bidimensionale di float
    float fvla[size1][size2]:

    // fai qualcosa con il VLA bidimensionale di float
    ...
}
Fantastico, no? Troppo bello per essere vero... ma ci saranno delle controindicazioni? Sicuramente non nelle prestazioni: ho scritto giustappunto un po' di codice per testare le prestazioni dei VLAs rispetto alle alternative più immediate: array dinamici (con malloc()) e array statici (in heap e stack). Vai col codice!
#include <stdio.h>
#include <time.h>
#include <stdlib.h>

#define MYSIZE  1000000

// variabile dummy per evitare lo svuotamento totale delle funzioni usando GCC -O2
int avoid_optimization;

// prototipi locali
void testVLA(int size);
void testMallocVLA(int size);
void testStackFLA(int dum);
void testHeapFLA(int dum);
void runTest(int iterations, void (*funcptr)(int), int size, const char *name);

// funzione main()
int main(int argc, char* argv[])
{
    // test argomenti
    if (argc != 2) {
        // errore: conteggio argomenti errato
        printf("%s: wrong arguments counts\n", argv[0]);
        printf("usage: %s vla iterations [e.g.: %s 10000]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // estrae iterazioni
    int iterations = atoi(argv[1]);

    // esegue test
    runTest(iterations, &testVLA, MYSIZE, "testVLA");
    runTest(iterations, &testMallocVLA, MYSIZE, "testMallocVLA");
    runTest(iterations, &testStackFLA, 0, "testStackFLA");
    runTest(iterations, &testHeapFLA, 0, "testHeapFLA");

    // esce
    return EXIT_SUCCESS;
}

// funzione runTest()
void runTest(
    int        iterations,      // iterazioni del test
    void       (*funcptr)(int), // funzione di test
    int        size,            // size dell'array
    const char *name)           // nome funzione di test
{
    // prende start time
    clock_t t_start = clock();

    // esegue iterazioni test
    for (int i = 0; i < iterations; i++)
        (*funcptr)(size);

    // prende end time e mostra il risultato
    clock_t t_end = clock();
    double t_passed = ((double)(t_end - t_start)) / CLOCKS_PER_SEC;
    printf("%-13s -  Tempo trascorso: %f secondi\n", name, t_passed);
}

// funzione testVLA()
void testVLA(
    int size)       // size per VLA
{
    int vla[size];

    // loop di test
    for (int i = 0; i < size; i++)
        vla[i] = i;

    // istruzione per evitare lo svuotamento totale della funzione usando GCC -O2
    avoid_optimization = vla[size / 2];
}

// funzione testMallocVLA()
void testMallocVLA(
    int size)      // size per malloc()
{
    int *mallocvla = malloc(size * sizeof(int));

    // loop di test
    for (int i = 0; i < size; i++)
        mallocvla[i] = i;

    // istruzione per evitare lo svuotamento totale della funzione usando GCC -O2
    avoid_optimization = mallocvla[size / 2];

    free(mallocvla);
}

// funzione testStackFLA()
void testStackFLA(
    int dum)        // parametro dummy
{
    int stackfla[MYSIZE];

    // loop di test
    for (int i = 0; i < MYSIZE; i++)
        stackfla[i] = i;

    // istruzione per evitare lo svuotamento totale della funzione usando GCC -O2
    avoid_optimization = stackfla[MYSIZE / 2];
}

// funzione testHeapFLA()
int heapfla[MYSIZE];
void testHeapFLA(
    int dum)        // parametro dummy
{
    // loop di test
    for (int i = 0; i < MYSIZE; i++)
        heapfla[i] = i;
}
Qui, come sempre, copio-e-incollo quello che scrivo sempre dopo aver mostrato il codice (squadra che vince non si cambia...): Ok, come vedete è ampiamente commentato e quindi è auto-esplicativo, per cui non mi dilungherò sulle singole istruzioni e/o gruppi di istruzioni (leggete i commenti! sono li per quello!), ma aggiungerò, solo, qualche dettaglio strutturale.

Allora: visto che si tratta di un test comparativo ho scritto una funzione runTest() che chiama n-iterazioni della funzione da testare e conta il tempo impiegato. Il main() si limita a chiamare quattro volte runTest(), una per ogni funzione. Le quattro funzioni di test che ho scritto testano (come richiamato dai nomi, ovviamente): un C99-VLA, un tradizionale malloc-VLA, un Fixed-LA allocato nello stack e un Fixed-LA allocato nello heap. Per ogni test viene usato un (gran) array-size di 1000000 e il numero di iterazioni si decide al lancio dell'applicazione (questo è molto utile come vedremo tra poco).

Notare che runTest() usa un function pointer per lanciare il test (avevamo visto qualcosa del genere parlando qui delle callback): ho usato la versione estesa della dichiarazione (void (*funcptr)(int) + passaggio della funzione con l'operatore &) ma vi ricordo che, ad esempio, GCC digerisce facilmente anche la dichiarazione semplificata (void funcptr(int) + passaggio senza l'operatore &). La versione estesa è, ovviamente, più portatile. E visto che siamo in tema di compilatori: anche se i VLAs (e i loop for (int...)) che uso nel codice di questo mese sono ammessi solo da C99 in avanti non c'è bisogno (se usate GCC) di specificare il flag -std=c99 in compilazione: le versioni recenti di GCC includono di default (come minimo) anche il C99 (oltre alle estensioni del GNU C): se proprio volete essere sicuri che quello che avete scritto rispetta uno standard in particolare  dovete usare altri flag: ad esempio, se volete scrivere usando solo il C89, dovete aggiungere sulla linea di compilazione: -std=c89 -pedantic. Se poi state usando un GCC più datato allora la compilazione dell'esempio vi darà warning e/o errori, e dovrete ricompilare forzando la compatibilità col C99.

(Nota: grazie alla segnalazione di Ponchietto, un lettore attento e preparato, mi sono reso conto che avevo dimenticato di aggiungere, in ogni funzione di test, una semplice istruzione che usasse l'array creato, così da evitare che l'ottimizzatore del GCC azzerasse il contenuto della funzione stessa (visto che l'array non lo usava nessuno). Ovviamente così i risultati dei test con ottimizzazione cambiano. Adesso il post ha codice e risultati rinnovati)

I risultati sono i seguenti:
aldo@ao-linux-nb:~/blogtest$ gcc vla.c -o vla
aldo@ao-linux-nb:~/blogtest$ ./vla 2000
testVLA       -  Tempo trascorso: 4.263985 secondi
testMallocVLA -  Tempo trascorso: 3.641929 secondi
testStackFLA  -  Tempo trascorso: 4.292963 secondi
testHeapFLA   -  Tempo trascorso: 4.285660 secondi
aldo@ao-linux-nb:~/blogtest$ gcc -O2 vla.c -o vla
aldo@ao-linux-nb:~/blogtest$ ./vla 2000
testVLA       -  Tempo trascorso: 0.767087 secondi
testMallocVLA -  Tempo trascorso: 0.690925 secondi
testStackFLA  -  Tempo trascorso: 0.678178 secondi
testHeapFLA   -  Tempo trascorso: 0.687785 secondi
Come vedete ho eseguito due test con/senza ottimizzazione (flag GCC -O2) e, ovviamente, è tornato utile il parametro n-iterazioni dell'applicazione, che mi ha permesso di trovare un valore adatto a ottenere risultati significativi e, allo stesso tempo, a evitare tempi di esecuzione biblici per la versione senza ottimizzazioni. Come possiamo commentare? Beh, il VLA se la cava egregiamente, con/senza ottimizzazioni! Ottiene, praticamente, gli stessi risultati del suo diretto concorrente, il malloc-VLA, ed è più semplice da usare!

Allora, torniamo al pezzo: VLA approvato!

MA PERÒ...

beh, il però del VLA cattivo ve lo spiegherò meglio nel prossimo post, e sappiate che non è tutto oro quello che luccica... e tanto per farvi un piccolo spoiler sulle considerazioni finali: io non uso mai i VLAs nel codice che scrivo!

Ciao e al prossimo post!

lunedì 24 aprile 2017

The FileCopy
come scrivere una funzione di File Copy in C - pt.2

Ok, riprendiamo da dove ci eravamo lasciati nell'ultimo post (l'avete letto vero?) e, come promesso, questa volta l'argomento sarà una versione con buffered I/O della funzione cpFile(). È doveroso, prima, fornire un altra immagine tratta da The Thing, se no potrebbe sembrare che questo sia solo un blog di programmazione, mentre, come ben sapete, è un blog per programmatori cinefili...

...con un cappello così si programma meglio...
Allora, ripetiamo: I/O bufferizzato (e quindi, per esempio, fread(3) invece di read(2), perche l'obbiettivo questa volta è la portabilità e cosa c'è di più portabile (nel C) che usare il contenuto di stdio.h? Il codice che vedremo tra un attimo usa (quasi) lo stesso main() della versione unbuffered e gli unici cambi sono interni alla funzione cpFile(). In realtà, anche la cpFile() è quasi identica da un punto di vista logico, visto che le funzioni buffered hanno una sintassi d'uso e un funzionamento molto simile alle equivalenti versioni unbuffered. Ecco, se la versione buffered vi viene molto diversa (esteticamente e logicamente) dalla unbuffered c'è qualcosa che non va... ma su questo punto farò una breve digressione alla fine del post. Per il momento: vai col codice!
#include <stdio.h>
#include <stdlib.h>

// prototipi locali
static int cpFile(const char* src, const char* dest);

// funzione main()
int main(int argc, char *argv[])
{
    // test argumenti
    if (argc != 3) {
        // errore: conteggio argomenti errato
        printf("%s: wrong arguments counts\n", argv[0]);
        printf("usage: %s srcfile destfile [e.g.: %s try.c try.save]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // esegue copy
    int retval;
    if ((retval = cpFile(argv[2], argv[1])) < 0) {
        // mostra errore ed esce
        fprintf(stderr, "%s: error: %d\n", argv[0], retval);
        exit(EXIT_FAILURE);
    }

    // esce
    return EXIT_SUCCESS;
}

// funzione cpFile()
static int cpFile(
    const char *dest,               // file destinazione
    const char *src)                // file sorgente
{
    // apre il file sorgente
    FILE *fp_in;
    if ((fp_in = fopen(src, "r")) == NULL) {
        // return con errore
        return -1;
    }

    // apre il file destinazione
    FILE *fp_out;
    if ((fp_out = fopen(dest, "w")) == NULL) {
        // chiude il file e return con errore
        fclose(fp_in);
        return -2;
    }

    // r/w loop per la copia usando buffered I/O
    size_t n_read;
    char buffer[BUFSIZ];
    while ((n_read = fread(buffer, 1, sizeof(buffer), fp_in)) > 0) {
        if (! ferror(fp_in)) {
            // write buffer
            fwrite(buffer, 1, n_read, fp_out);
            if (ferror(fp_out)) {
                // chiude i file e return con errore
                fclose(fp_in);
                fclose(fp_out);
                return -3;
            }
        }
        else {
            // chiude i file e return con errore
            fclose(fp_in);
            fclose(fp_out);
            return -4;
        }
    }

    // chiude i file
    fclose(fp_in);
    fclose(fp_out);

    // return con Ok
    return 0;
}
Ok, come vedete è ampiamente commentato e quindi è auto-esplicativo, per cui non mi dilungherò sulle singole istruzioni e/o gruppi di istruzioni (leggete i commenti! sono li per quello!), ma aggiungerò, solo, qualche dettaglio strutturale. Il main(), come anticipato, è praticamente identico, mentre nella cpFile() si ripetono, esattamente, le spesse operazioni della versione unbuffered, usando però fopen(3) invece di open(2), fclose(3) invece di close(2), ecc. Dove troviamo qualche (piccola) differenza? Solo nel test di (eventuali) errori di lettura/scrittura, che nella versione unbuffered erano impliciti nelle operazioni di read/write (testando se il risultato era uguale a -1), mentre che in questo caso bisogna usare una funzione a parte, ferror(3), e questo perché:  
    On  success,  fread()  and  fwrite() return the number of items read or
    written.  This number equals the number of bytes transferred only  when
    size  is 1.  If an error occurs, or the end of the file is reached, the
    return value is a short item count (or zero).
    fread() does not distinguish between end-of-file and error, and callers
    must use feof(3) and ferror(3) to determine which occurred.
Quello sopra è quello che riporta la man-page di fread(3)/fwrite(3), e credo che sia una giustificazione sufficiente sul perché ho scritto il codice così (e, per lo stesso motivo, non possiamo usare strerror(3) nel main() per segnalare gli errori). Quindi, come anticipato, le versioni buffered e unbuffered devono essere quasi sovrapponibili, e mi sembra che sia esattamente il risultato raggiunto.

Ed ora la digressione promessa: mi è capitato di trovare in rete (anche in pregevoli blog/siti di programmazione) esempi di implementazione di buffered-copy di file che usano dei loop di questo tipo:
while (!feof(fp_in)) {
    // legge e scrive buffer
    ...
}
ecco, come si può commentare questo? Con una sola parola:

NO

Se uno scrive il loop in questa maniera vuol dire che non ha letto la man-page di fread(3)/fwrite(3) o l'ha letta e non ne ha capito il contenuto. Non c'è bisogno di reinventare la ruota, ripeto: fread(3)/fwrite(3) funzionano quasi nello stesso modo di read(2)/write(2), quindi, se l'esempio della cpFile() unbuffered del post precedente era buono (e lo era!), allora l'esempio del post attuale deve risultare (quasi) uguale. Il loop con un while() che testa feof(3) è corretto sintatticamente ma non lo è logicamente, perché inizia testando qualcosa che non è ancora usabile (uhmm, un test predittivo?) e che, oltretutto, non serve testare. Bah, non voglio dilungarmi ulteriormente e vi rimando alla ottima analisi contenuta là (nel sempre ottimo stackoverflow.com).

Ovviamente spero di non aver offeso nessuno (con la digressione precedente): ricordate, errare humanum est... e, sicuramente, anche in questo blog avrò scritto in passato qualche scemata (spero non grave come quella appena mostrata). Vi assicuro, però, che sto sempre attentissimo a non proporre soluzioni che non ho avuto tempo di scrivere e provare in maniera approfondita, se no invece di un blog di programmazione artistica questo sarebbe un blog di programmazione alla speriamo che funziona...

Ciao e al prossimo post!

sabato 11 marzo 2017

The FileCopy
come scrivere una funzione di File Copy in C - pt.1

Ok, questo post non centra niente con The Thing, il capolavoro immortale di John Carpenter (a parte la piccola assonanza del nome). Anzi, è solo una scusa per celebrarlo, dato che l'ho rivisto (per la millesima volta) da poco. Comunque, dato che ci siamo, parleremo anche un po' di C...

capolavoro immortale
Con questo post vedremo come scrivere una funzione per fare una copia di un file, dato che i sistemi POSIX (come Linux) non prevedono una funzione specifica di libreria per farlo. Ci sono, evidentemente, mille maniere per scriverla, e questa volta ne ho pensate due. Vai con la prima!
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/sendfile.h>

// prototipi locali
static int cpFile(const char* src, const char* dest);

// funzione main()
int main(int argc, char *argv[])
{
    // test argumenti
    if (argc != 3) {
        // errore: conteggio argomenti errato
        printf("%s: wrong arguments counts\n", argv[0]);
        printf("usage: %s srcfile destfile [e.g.: %s try.c try.save]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // esegue copy
    if (cpFile(argv[2], argv[1]) == -1) {
        // mostra errore ed esce
        fprintf(stderr, "%s: error: %s\n", argv[0], strerror(errno));
        exit(EXIT_FAILURE);
    }

    // esce
    return EXIT_SUCCESS;
}

// funzione cpFile()
static int cpFile(
    const char *dest,               // file destinazione
    const char *src)                // file sorgente
{
    // apre il file sorgente
    int fd_in;
    if ((fd_in = open(src, O_RDONLY)) == -1) {
        // return con errore
        return -1;
    }

    // apre il file destinazione
    int fd_out;
    if ((fd_out = open(dest, O_WRONLY | O_CREAT | O_TRUNC, 00644)) == -1) {
        // chiude il file e return con errore
        close(fd_in);
        return -1;
    }

    // r/w loop per la copia usando unbuffered I/O
    size_t n_read;
    char buffer[BUFSIZ];
    while ((n_read = read(fd_in, buffer, sizeof(buffer))) > 0) {
        // write buffer
        if (write(fd_out, buffer, n_read) == -1) {
            // chiude i file e return con errore
            close(fd_in);
            close(fd_out);
            return -1;
        }
    }

    // chiude i file
    close(fd_in);
    close(fd_out);

    // esce con l'ultimo risultato di read() (0 o -1)
    return n_read;
}
Ok, come vedete è ampiamente commentato e quindi è auto-esplicativo, per cui non mi dilungherò sulle singole istruzioni e/o gruppi di istruzioni (leggete i commenti! sono li per quello!), ma aggiungerò, solo, qualche dettaglio strutturale. Il main(), in questo caso, serve solo per testare la funzione di copia e il programma generato si comporta (a livello basico) come la funzione POSIX cp(1), che è proprio quella che vogliamo emulare usando la nostra nuova funzione di libreria.

La funzione che esegue il lavoro l'ho chiamata cpFile() ed è abbastanza semplice, come si vede. Usa l'I/O non bufferizzato (quindi, per esempio, read(2) invece di fread(3)) e, pur essendo compattissima ed efficiente, tratta anche in maniera esaustiva gli errori ed è scritta per essere una funzione di libreria, quindi non scrive nulla su stderr e stdout ma si limita a eseguire il lavoro e a restituire un codice di ritorno (0 o -1) che può essere trattato dal chiamante (in questo caso il main()) per visualizzare eventuali errori usando strerror(3) ed errno. Tutto il lavoro viene eseguito in un loop che legge un buffer dal file sorgente e lo scrive nel file destinazione, fino alla fine del file. Il resto del codice è apertura/chiusura dei file e trattamento degli errori. Visto che usiamo l'unbuffered I/O ho dimensionato il buffer di lettura/scrittura usando la define BUFSIZ del sistema che dovrebbe garantire la dimensione ottimale per le operazioni di I/O.

Avevo detto che avrei proposto due versioni: fermo restando il main() (che va bene per entrambi i casi) la versione alternativa è questa:
// funzione cpFile()
static int cpFile(
    const char* dest,               // file destinazione
    const char* src)                // file sorgente
{
    // apre il file sorgente
    int fd_in;
    if ((fd_in = open(src, O_RDONLY)) == -1) {
        // return con errore
        return -1;
    }

    // apre il file destinazione
    int fd_out;
    if ((fd_out = open(dest, O_WRONLY | O_CREAT | O_TRUNC, 00644)) == -1) {
        // chiude il file e return con errore
        close(fd_in);
        return -1;
    }

    // copia in kernel-space usando la funzione sendfile()
    off_t bytesCopied = 0;
    struct stat fileinfo = {0};
    fstat(fd_in, &fileinfo);
    int result = sendfile(fd_out, fd_in, &bytesCopied, fileinfo.st_size);

    // chiude i file
    close(fd_in);
    close(fd_out);

    // esce con il risultato di sendfile()
    return result;
}
Come vedete è quasi sovrapponibile alla precedente ma è diversa proprio nella parte che esegue il lavoro di copia: al posto del loop viene usata la funzione sendfile(2), che ci permette di eseguire una copia diretta e super-efficiente a livello kernel-space (mentre la prima versione lavorava in user-space).

Senza entrare nei dettagli profondi che tutto questo comporta (kernel-space e user-space dei sistemi della famiglia UNIX), mi limito a precisare che questa seconda versione è migliore della prima ma è meno portabile, visto che la sendfile(2) ha comportamenti diversi in base al sistema (per esempio su Linux si può usare solo dal Kernel 2.6.33 in avanti, mentre su macOS, viceversa, funziona solo fino alla versione 10.8). E già che ci siamo specifichiamo meglio: anche la prima versione non è completamente portabile, visto che su alcuni sistemi (tipo quello che comincia con W e che preferisco non nominare neanche) la system call read(2) non c'è.

Ok, allora potete già intuire che l'argomento del prossimo post sarà una versione con buffered I/O della funzione cpFile(), ossia una versione intrinsecamente portabile, già che userà l'I/O standard del C (quello contenuto in stdio.h, per intenderci).

Non trattenete il respiro nell'attesa, mi raccomando...

Ciao e al prossimo post!

sabato 18 febbraio 2017

Prendi il makefile e scappa
come scrivere un makefile universale

Questo è un post veloce. E non è neanche propriamente un post sul C. Il consiglio è di prendere l'informazione, scappare e conservarla gelosamente per il futuro, perché potrebbe tornare molto utile. E non fatevi prendere, se no potreste fare la fine di Virgil Starkwell.

faccia da "ma ho solo rubato un makefile..."
Allora, supponiamo che dobbiamo fare un progetto (che chiameremo, per esempio, pluto) e, per vari motivi, non vogliamo (siamo della vecchia scuola) o non possiamo (non ce n'è uno adatto) usare un IDE. Organizziamo i nostri file in una maniera canonica, in tre directory: pluto, lib e include. Ovviamente scriveremo il codice in C e piazzeremo i file in maniera logica (evidentemente il file con il main() andrà nella directory pluto). I file sono tanti e  e ogni volta che ricompiliamo non vogliamo riscrivere il comando a mano e vogliamo ricompilare solo quello che serve (solo i sorgenti modificati) soddisfacendo automaticamente le dipendenze dagli header (ricompilare solo i sorgenti che dipendono da un header modificato)... Ma ci serve un makefile! Ok, tutti voi sapete già cosa è un makefile, ma... sapete scriverne uno veramente semplice e, al tempo stesso, super funzionale e, soprattutto, generico e universale? Se la risposta è NO questo è il vostro post (e se la risposta è SI allora Ciao e al prossimo post!).

Bando alle ciance: se state leggendo questa riga avete risposto NO alla domanda precedente, e quindi vai con l'esempio!
# variabili
CC = gcc
SRCS = $(wildcard *.c)
SRCS_LIB = $(wildcard ../lib/*.c)
OBJS = $(SRCS:.c=.o)
OBJS_LIB = $(SRCS_LIB:.c=.o)
DEPS = $(SRCS:.c=.d)
DEPS_LIB = $(SRCS_LIB:.c=.d)

# creazione del target file eseguibile
pluto: $(OBJS) $(OBJS_LIB)
    $(CC) $^ -o $@ -lcurl

# creazione degli object files
%.o: %.c
    $(CC) -MMD -MP -I../include -c $< -o $@ -g -Wall -std=c11 -D SIMULATION

# direttive phony
.PHONY: clean

# pulizia progetto ($(RM) è di default "rm -f")
clean:
    $(RM) $(OBJS) $(OBJS_LIB) $(DEPS) $(DEPS_LIB)

# creazione dipendenze
    -include $(DEPS) $(DEPS_LIB)
Come vedete il makefile presentato è veramente semplice. Però è anche veramente completo: fa tutto quello che serve, compresa la generazione dei file di dipendenza dagli header, e possiamo usarlo per qualsiasi progetto, indipendentemente dal numero di file (le directory lib e include potrebbero essere vuote oppure contenere centinaia di file). Possiamo aggiungere e togliere sorgenti e header e ricompilare senza modificare una sola linea del makefile, perché lui si adatta automaticamente a quello che trova nelle tre directory del progetto: cosa vogliamo di più?

Qualche piccolo dettaglio sui blocchi (commentati) che compongono il makefile:

# variabili
Qua si mettono le variabili che vengono usate nel resto del makefile. In particolare la variabile CC indica il compilatore da usare: nel nostro caso è gcc, ma potrebbe essere, per esempio, g++ (per il C++). Ovviamente in questo caso i sorgenti sarebbero dei .cpp o .cc, quindi bisogna ricordarsi di modificare anche le altre variabili che fanno riferimento ai .c.

# creazione del target file eseguibile
Qua si mette il comando per linkare i file oggetto creati e produrre il file eseguibile finale. Se usiamo qualche libreria esterna il riferimento si aggiunge qui (nell'esempio si linka la libcurl usando -lcurl).

# creazione degli object files
Qua si mette il comando per compilare ogni sorgente e creare il file oggetto corrispondente, attivando tutte le opzioni del compilatore che ci servono. Se usiamo qualche #ifdef particolare (come quelle viste la) la attivazione si mette qui (nell'esempio si attiva una define SIMULATION usata nei sorgenti).

# direttive phony
Qua si mettono tutte le direttive phony (è un po' lungo da spiegare: guardate il link, che è chiarissimo).

# pulizia progetto ($(RM) è di default "rm -f")
Qua si mette il comando di cancellazione degli oggetti per, eventualmente, forzare una successiva ricompilazione completa.

# creazione dipendenze
Qua si mette il comando per generare i file di dipendenza che ci permettono di ricompilare solo quello che serve quando modifichiamo un header file.

Il makefile presentato è un esempio reale, pronto all'uso. Ovviamente le direttive -lcurl e -D SIMULATION sono state aggiunte come esempio per indicare come estendere le funzionalità del makefile: se non ci servono possiamo toglierle senza problemi (e aggiungeremo quelle che ci servono usando lo stessa sintassi).

Che ne dite? L'obbiettivo non era di spiegare cosa è un makefile e come si scrive (uff, c'è in rete una documentazione enorme sull'argomento). E neppure era di spiegare i segreti della sintassi (che permette anche soluzioni complesse). L'obbiettivo era di fornire un makefile basico e completo allo stesso tempo, un makefile universale per (quasi) qualsiasi progetto. Io direi che l'obbiettivo è compiuto... poi, se dobbiamo fare progetti complessi e portabili, con auto-installatori, ecc. magari ci troveremo più comodi usando un IDE di buona qualità oppure usando a mano tools come Autotools o CMake... ma vi assicuro che il metodo rapido e vecchia-scuola che ho descritto è usabile sempre e senza limitazioni. Sono soddisfazioni...

Ciao e al prossimo post!