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

Nessun commento:

Posta un commento