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.

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!