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.

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!

sabato 14 gennaio 2017

Per un pugno di ifdef
come usare il preprocessore in C

Dopo i bagordi di fine anno è meglio riprendere con un post leggero. Parleremo, quindi del preprocessore... be, in realtà se parlassimo del preprocessore in maniera approfondita non sarebbe un post molto leggero, quindi ci limiteremo ad un caso semplice, cioè ad un uso interessante della direttiva #ifdef. E vi consiglio di seguire i consigli di Joe (lo straniero) perché è uno che si arrabbia facilmente...
...e chi non usa la ifdef dovrà vedersela con me...
Allora: riprendiamo un vecchio pezzo di codice mostrato qui (subito a rileggerlo!), quello del Socket Server. Dando per scontato che abbiate ben chiaro come funziona, lo modificheremo seguendo un possibile caso reale, che sarebbe il seguente: supponiamo di dover compilare il nostro codice per due diversi ambienti operativi, ad esempio per un Linux Desktop/Server recente, e per un Linux Embedded un po' datato (con compilatore, Kernel e glibc di qualche annetto fa). Abbiamo deciso per motivi vari che il nuovo codice deve creare un socket non bloccante nella fase di accept, quindi, ad esempio, si potrebbe sostituire la chiamata ad accept() con una ad accept4() che dispone di un argomento flags che si può impostare a SOCK_NONBLOCK che è proprio quello che ci serve. Sfortunatamente il nostro sistema embedded (datato, come detto) usa un kernel più vecchio del 2.6.28 e una glibc più vecchia della 2.10 (che sono le due condizioni minime per poter usare la accept4()). Che fare? Facciamo due versioni del codice? NO! Perché così ci toccherebbe (in futuro) fare anche doppia manutenzione, che è una situazione da evitare. Manterremo, invece, una sola versione con delle opportune #ifdef per gestire la compilazione nei due ambienti operativi.

Allora, il pezzo di codice (estratto da quel post lì) che dobbiamo modificare è questo:
...
// accetta connessioni da un client entrante
printf("%s: attesa connessioni entranti...\n", argv[0]);
socklen_t socksize = sizeof(struct sockaddr_in);
struct sockaddr_in client;          // (remote) client socket info
int client_sock;
if ((client_sock = accept(my_socket, (struct sockaddr *)&client, &socksize)) < 0) {
    // errore accept()
    printf("%s: accept failed (%s)\n", argv[0], strerror(errno));
    return EXIT_FAILURE;
}
...
E la nuova versione con compilazione condizionale via #ifdef sarà la seguente:
...
// accetta connessioni da un client entrante (in non blocking mode: my_socket è SOCK_NONBLOCK)
printf("%s: attesa connessioni entranti...\n", argv[0]);
socklen_t socksize = sizeof(struct sockaddr_in);
struct sockaddr_in client;          // (remote) client socket info
int client_sock;
#ifdef OLD_LINUX
if ((client_sock = accept(my_socket, (struct sockaddr *)&client, &socksize)) < 0) {
#else
if ((client_sock = accept4(my_socket, (struct sockaddr *)&client, &socksize, SOCK_NONBLOCK)) < 0) {
#endif
    // errore accept()
    printf("%s: accept failed (%s)\n", argv[0], strerror(errno));
    return EXIT_FAILURE;
}
#ifdef OLD_LINUX
else {
    // accept eseguita: set socket a non-blocking
    int flags;
    if ((flags = fcntl(client_sock, F_GETFL, 0)) >= 0) {
        if (fcntl(client_sock, F_SETFL, flags | O_NONBLOCK) < 0) {
            // errore accept()
            printf("%s: fcntl failed (%s)\n", argv[0], strerror(errno));
            return EXIT_FAILURE;
        }
    }
}
#endif
...
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.

Nelle parti incluse negli #ifdef OLD_LINUX c'è la versione di codice che NON usa la accept4(), ed è, ovviamente, un po' più complicata dell'altra, visto che ci tocca usare fcntl() per rendere non-bloccante il socket creato, mentre la accept4() lo crea direttamente passandogli, come detto, il flag SOCK_NONBLOCK. Comunque, come potete notare, il codice risultante è abbastanza chiaro e leggibile e, tutto sommato, non ha un brutto aspetto (lo stile prima di tutto!).

Qualcuno potrebbe dire: la versione sotto #ifdef funziona anche con un Linux recente, quindi perché non lasciare solo quella e togliere le #ifdef? NO! NO! e ancora NO! Non dobbiamo scrivere codice old-style per essere retro-compatibili: bisogna sempre cercare di scrivere in maniera moderna, e in caso di necessità (come nell'esempio) il vecchiume lo mettiamo in #ifdef. E quando sarà il momento (quando, per esempio, non ci servirà più un doppio ambiente operativo) faremo pulizia e lasceremo solo un bel codice moderno.

Ovviamente la compilazione condizionale la realizzeremo inserendo (o non inserendo) una istruzione -D OLD_LINUX nella linea del nostro makefile che genera i file oggetto, oppure direttamente nella linea di comando se non usiamo un makefile. Bene, finalmente abbiamo scritto un unico codice per due ambienti operativi diversi: missione compiuta!

Solo un ultima precisazione sul nostro socket server modificato (e che non centra niente con gli ifdef): se il non-blocking socket ci serve per eseguire delle recv() non bloccanti nella fase successiva a quella di accept, è molto più semplice modificare opportunamente il loop di ricezione e passare il flag MSG_DONTWAIT alla recv() nell'argomento flags (il quarto). Così la recv() non blocca in entrambi gli ambienti operativi, e tutto senza usare le #ifdef. Ma questa è un altra storia...

Ciao e al prossimo post!