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 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!