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.

venerdì 18 ottobre 2024

Prendi il makefile e scappa
come scrivere un makefile universale - pt.2

Louise: Sai una cosa? Presto avremo un bambino.
Virgil: Scherzi...
Louise: No! Avremo proprio un bambino: me l'ha detto il dottore, è sicuro. Sarà il mio regalo per Natale.
Virgil: Ma a me bastava una cravatta!

(...una premessa: questo post è un remake di un mio vecchio post (parte 2 di 2). Ma, anche se tratta lo stesso argomento, amplia e perfeziona un po' il discorso è mi è sembrato il caso di riproporlo. Leggete e mi direte...)

Nel divertentissimo e bel mockumentary  Prendi i soldi e scappa del Maestro Woody Allen, il protagonista Virgil Starkwell diceva che ha lui bastava una cravatta... a noi invece basta un bel makefile universale! Spero che abbiate già letto la prima parte di questo articolo... Non l'avete ancora fatto? Subito a leggerla e poi tornate qua!

...presto avremo un makefile...

Rieccoci con il nostro makefile universale. Ora che avete (ri)letto l'articolo precedente sapete già che questo sarà un'altro post veloce, e non propriamente sul C: in questa seconda parte, come promesso, prenderò il nostro makefile per aggiungere una funzionalità (spero) molto interessante: la creazione di una shared-library per Linux (una .so, per gli amici). Vediamo schematicamente quali sono i passi da eseguire (su un Linux della famiglia Debian) per creare e usare una shared-lib:

1. Creare una directory per condividere la shared-lib, ad esempio: "/usr/local/lib/pluto"

2. Modificare (come vedremo tra poco) il makefile per generare la shared-lib (che
chiameremo "libmyutils.so") e copiarla in "/usr/local/lib/pluto".

3. Aggiungere in "/etc/ld.so.conf.d" un nuovo file "libmyutils.conf" che conterrà le
seguenti due linee (la prima è solo un commento):

# libmyutils.so default configuration
/usr/local/lib/pluto

4. Rendere disponibile la nuova shared-lib eseguendo:

sudo ldconfig

E proseguiamo: supponiamo di usare lo stesso progetto dell'altra volta (si chiamava pluto). I nostri file sono organizzati (ancora) in una maniera canonica, questa volta in quattro directory (l'altra volta erano tre): pluto, lib, libmyutils e include. La directory aggiunta è libmyutils, e contiene i sorgenti della shared-lib che vogliamo creare. Nella directory pluto troviamo il main del progetto e il makefile, nella directory lib troviamo gli altri sorgenti dell'applicazione pluto e, infine, nella directory include troviamo gli header-files comuni (ma tutta la struttura era già ben descritta nell'articolo precedente, no? Consultarla per fugare eventuali dubbi). E ora vediamo il nuovo makefile:

# variabili
SRCS = $(wildcard *.c)
SRCS_LIB = $(wildcard ../lib/*.c)
SRCS_SHLIB = $(wildcard ../libmyutils/*.c)
OBJS = $(SRCS:.c=.o)
OBJS_LIB = $(SRCS_LIB:.c=.o)
OBJS_SHLIB = $(SRCS_SHLIB:.c=.o)
DEPS = $(SRCS:.c=.d)
DEPS_LIB = $(SRCS_LIB:.c=.d)
DEPS_SHLIB = $(SRCS_SHLIB:.c=.d)
PATHLIB = /usr/local/lib/pluto
NAMELIB = libmyutils.so
OBJS_APP = $(OBJS) $(OBJS_LIB)

# compilatore e linker (ometto per semplicità la variabile LD)
CC = gcc

# flag per il preprocessore di CC per i file oggetto dei due target (qui ometto CFLAGS)
CPPFLAGS = -I../include -g -O2 -Wall -pedantic -pthread -DUNA_MIA_DEFINE -MMD -MP -std=c11
CPPFLAGS_SHLIB = -fpic -I../include -g -O2 -Wall -pedantic -MMD -MP -std=c11

# flag per il linker LD per la creazione dei due target (qui ometto LDLIBS)
LDFLAGS = -L$(PATHLIB) -lmyutils -pthread
LDFLAGS_SHLIB = -shared -fpic

# i due target: shared-lib e applicazione
all: libmyutils pluto

# creazione del target applicazione "pluto"
pluto: $(OBJS_APP)
$(CC) $^ -o $@ $(LDFLAGS)

# creazione del target shared-lib "libmyutils.so" con copia nella directory destinazione
libmyutils: $(OBJS_SHLIB)
$(CC) $^ -o $(NAMELIB) $(LDFLAGS_SHLIB)
mv $(NAMELIB) $(PATHLIB)

# creazione degli object files per la applicazione
$(OBJS_APP): %.o: %.c # target (i file .o) e da chi dipende (i file .c)
$(CC) $(CPPFLAGS) -c $< -o $@

# creazione degli object files per la shared-lib
$(OBJS_SHLIB): %.o: %.c # target (i file .o) e da chi dipende (i file .c)
$(CC) $(CPPFLAGS_SHLIB) -c $< -o $@

# direttive phony
.PHONY: clean

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

# creazione dipendenze
-include $(DEPS) $(DEPS_LIB) $(DEPS_SHLIB)

Come avrete notato il nuovo makefile presentato è uno stretto parente di quello vecchio e continua ad essere veramente semplice e universale: 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 directory del progetto: cosa vogliamo di più?

Però adesso è il caso di evidenziare alcune delle differenze rispetto alla versione originale, fermo restando che le descrizioni dei vari punti fatte nell'articolo precedente restano valide e non è il caso di ripeterle qui. Vediamo, allora, solo dettagli e differenze:

  • # variabili
    Qui ci sono le stesse della versione originale a cui ho aggiunto quelle necessarie a descrivere sorgenti, oggetti e dipendenze relativi alla shared-lib: SRCS_SHLIB, OBJS_SHLIB e DEPS_SHLIB. In più ho aggiunto pathname (PATHLIB) e name (NAMELIB) della libreria.
  • # compilatore e linker (ometto per semplicità la variabile LD)
    Qui c'è, ancora, il compilatore da usare, ma ho omesso (come evidenziato nel commento) il linker per semplificare rispetto alla versione originale.
  • # flag per il preprocessore di CC per i file oggetto dei due target (qui ometto CFLAGS)
    Qui ho messo i flag extra da assegnare al preprocessore C e ai programmi che lo utilizzano, e ho messo anche i flag aggiuntivi da fornire al compilatore C, per cui ho omesso (come evidenziato nel commento) la variabile CFLAGS per semplificare rispetto alla versione originale. Come si nota, questa versione ha due tipi di flag: CPPFLAGS e CPPFLAGS_SHLIB, visto che i file dell'applicazione e quelli della shared-lib si devono compilare con modalità differenti.
  • # flag per il linker LD per la creazione dei due target (qui ometto LDLIBS)
    Qui ho messo i flag aggiuntivi da dare al compilatore quando deve invocare il linker ld, e ho messo anche i nomi delle librerie forniti al compilatore, per cui ho omesso (come evidenziato nel commento) la variabile LDLIBS per semplificare rispetto alla versione originale. Come si nota, questa versione ha due tipi di flag: LDFLAGS e LDFLAGS_SHLIB, visto che i file dell'applicazione e quelli della shared-lib si devono linkare con modalità differenti.
  • # i due target: shared-lib e applicazione
    Qui ci sono gli obiettivi di creazione: nel nostro caso la libreria libmyutils e l'applicazione pluto: il comando make senza argomenti esegue entrambi gli obiettivi (la parola chiave è all), ma si può bypassare questo eseguendo, ad esempio, make libmyutils che crea solo la shared-lib.
  • # creazione del target applicazione "pluto"
    Qui si mette il comando per linkare i file oggetto creati e produrre il file eseguibile finale. Notare che con la direttiva -L contenuta in LDFLAGS indichiamo al linker dove si trova la libreria libmyutils.so. Notare anche che questa direttiva serve solo a livello linker, mentre, a livello esecuzione delle applicazioni che usano la nostra shared-lib, servono i passi della lista descritta all'inizio del post (in particolare i passi 3 e 4).
  • # creazione del target shared-lib "libmyutils.so" con copia nella directory destinazione
    Qui ci sono le istruzioni per la creazione della shared-lib libmyutils.so e per spostarla (col comando Linux mv) nella directory destinazione.
  • # creazione degli object files per la applicazione
    Qui si mette il comando per compilare ogni sorgente dell'applicazione e creare il file oggetto corrispondente, attivando (attraverso le variabili definite precedentemente) tutte le opzioni del compilatore che ci servono. Notare il trucco necessario per diversificare la creazione di questi oggetti rispetto agli oggetti della shared-lib: attraverso la direttiva "$(OBJS_APP): %.o: %.c" si dice al comando make che solo questi oggetti si compilano con il comando della linea successiva che usa i flag CPPFLAGS.
  • # creazione degli object files per la shared-lib
    Qui si mette il comando per compilare ogni sorgente della shared-lib e creare il file oggetto corrispondente, attivando (attraverso le variabili definite all'inizio) tutte le opzioni del compilatore che ci servono. Notare il trucco necessario per diversificare la creazione di questi oggetti rispetto agli oggetti dell'applicazione: attraverso la direttiva "$(OBJS_SHLIB): %.o: %.c"si dice al comando make che solo questi oggetti si compilano con il comando della linea successiva che usa i flag CPPFLAGS_SHLIB.

E concludo con tre ultime note generiche e (spero) interessanti:

  1. Sicuramente, avrete notato che il path scelto per la shared-lib è /usr/local/lib/pluto e questa non è una scelta casuale: /usr/local è una delle directory usate su Linux per aggiungere pacchetti non tipici della distribuzione base, quindi Software che si installa a parte o, come nel nostro caso, creato e aggiunto localmente: /usr/local è, in pratica, una directory ombra della root, e contiene tutte le versioni locali  delle directory di sistema (bin, etc, include, lib, ecc.). Nel nostro caso la libreria la aggiungiamo in /usr/local/lib, mentre un (eventuale) header si dovrebbe mettere /usr/local/include.
  2. Si noti che per compilare e linkare  la shared-lib si usano due direttive fondamentali: "-fpic" (in compilazione e link) e "-shared" (solo in link). E ribadisco: "-fpic" si usa sia in compilazione che in link: questo è un dettaglio che molte delle guide che si trovano in rete omettono e può essere una possibile causa di strani malfunzionamenti di una shared-lib.
  3. Nell'esempio ho usato un flag, "-pthread" (in compilazione e link) che si usa solo per applicazioni multithread, quindi avrei potuto ometterlo per questo esempio generico, ma ho voluto evidenziare che anche questo flag si usa sia in compilazione che in link, un particolare che molti dimenticano.

E qua finiscono le differenze e i dettagli. Credo che per oggi possa bastare... fatevi un piccolo progetto di prova (ad esempio usando funzioni semivuote che scrivono solo "Ciao, sono la funzione xyz") e provate il nuovo makefile universale: scoprirete che è veramente facilissimo da usare!

Ciao e al prossimo post!

mercoledì 25 settembre 2024

Prendi il makefile e scappa
come scrivere un makefile universale - pt.1

"In particolare ricordo una volta che rubò una penna. Non volevo umiliarlo. Sa, noi maestri sappiamo come comportarci in tali casi, così dissi alla classe: 'Ora chiuderemo tutti gli occhi e colui che ha preso la penna pensi a restituirla'. Allora, mentre avevamo gli occhi chiusi lui restituì la penna, ma ne approfittò per tastare il culo alla sua compagna di banco. ...in tv si può dire 'tastare'?" [Mrs. Dorothy Lowry, maestra di Virgil Starkwell, intervistata].

(...una premessa: questo post è un remake di un mio vecchio post (parte 1 di 2). Ma, anche se tratta lo stesso argomento, amplia e perfeziona un po' il discorso è mi è sembrato il caso di riproporlo. Leggete e mi direte...)

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, il protagonista del divertentissimo e bel mockumentary  Prendi i soldi e scappa del Maestro Woody Allen. Oggi parleremo di makefile!

...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. Quindi organizziamo (a mano) i nostri file in una maniera canonica, in tre directory: pluto, lib e include. Ovviamente scriveremo il codice in C e piazziamo i file in maniera logica (evidentemente il file con il main va nella directory pluto). I file sono tanti e ogni volta che ricompiliamo non vogliamo riscrivere il tutto il comando e, soprattutto, vogliamo ricompilare solo quello che serve (solo i sorgenti modificati) soddisfacendo automaticamente anche le dipendenze dagli header  (ovvero: 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, allo stesso tempo, super-funzionale, generico e universale? Se la risposta è NO questo è l'articolo che fa per voi (e se la risposta è SI... Ciao e al prossimo post!).

Bando alle ciance: se state leggendo questa riga avete risposto NO alla domanda precedente,  e quindi possiamo procedere con l'esempio!

# variabili
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)

# compilatore e linker (normalmente si usa gcc anche per il link)
CC = gcc
LD = gcc # NOTA: usualmente si omette e si usa solo CC

# flag per il preprocessore di CC durante la creazione dei file oggetto
CPPFLAGS = -I../include -g -O2 -Wall -pedantic -pthread -DUNA_MIA_DEFINE -MMD -MP

# flag per il compilatore CC durante la creazione dei file oggetto
CFLAGS = -std=c11 # NOTA: CFLAGS usualmente si omette e va tutto in CPPFLAGS

# flag per il linker LD durante la creazione del programma eseguibile
LDFLAGS = -Lpath_delle_librerie -pthread

# librerie che il linker LD deve collegare
LDLIBS = -lcurl # NOTA: LDLIBS usualmente si omette e va tutto in LDFLAGS

# creazione del target file eseguibile
pluto: $(OBJS) $(OBJS_LIB) # target (il file pluto) e da chi dipende (i file .o)
$(LD) $(LDFLAGS) $(LDLIBS) $^ -o $@

# creazione degli object files
%.o: %.c # target (i file .o) e da chi dipende (i file .c)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

# direttive phony
.PHONY: clean

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

# creazione dipendenze
-include $(DEPS) $(DEPS_LIB)

Come vedete il makefile presentato è veramente semplice. Però è anche molto 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 pluto 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ù?

Come avrete già notato il makefile proposto è stra-commentato, perché le buone usanze dei commenti del codice si devono estendere (e perché no?) anche ai makefile. Comunque per questo articolo è il caso di aggiungere qualche dettaglio in più sui blocchi che compongono l'esempio (titolati con gli stessi commenti che introducono i blocchi), ricordando che molti dei nomi che vedrete da qui in avanti sono predefiniti dal manuale del GNU make nel paragrafo Implicit Rules. Vai con la lista!

  • # variabili
    Qui si mettono le variabili locali del nostro makefile. In questo caso, per il semplice progetto proposto, avremo solo tre famiglie di variabili: sorgenti SRCS, oggetti OBJS e dipendenze DEPS, e ognuna di queste tre famiglie è divisa in due parti: normali (senza suffisso) e di libreria (con suffisso _LIB) visto che il nostro progetto ha una directory  base e una per una libreria (uhm, e a cosa serve? Potrebbe essere una parte più generica che è in comune con altri progetti, quindi la manteniamo separata dalla directory SRCS che contiene il progetto vero e proprio con la sua personalità base).
  • # compilatore e linker (normalmente si usa gcc anche per il link)
    Qui si mettono le variabili che descrivono il compilatore e il linker da usare: di solito è gcc per entrambi e quindi, normalmente, si usa solo la variabile CC (e LD si omette): LD serve per alcuni casi particolari come quando in un progetto C si vuole linkare una libreria C++ e diventa necessario usare g++ come linker. Notare che per un progetto C++ invece di CC si usa la variabile CXX (vedi di nuovo in Implicit Rules).
  • # flag per il preprocessore di CC durante la creazione dei file oggetto
    Qui si mettono i flag extra da assegnare al preprocessore C e ai programmi che lo utilizzano (i compilatori C, C++ e Fortran, per esempio). Ho messo un po' di flag  di uso frequente a caso (-Ipath_degli_include, -g, -O2, etc.) che sono ben spiegati nel manuale di gcc (usare di volta in volta solo quelli che servono, eh!). Mi preme solo aggiungere due dettagli:
    • Il flag -DUNA_MIA_DEFINE serve a dire al preprocessore: compila anche quello che, nel codice, è incluso in una #ifdef UNA_MIA_DEFINE. Si tratta, quindi, di compilazione condizionale, e si possono usare multiple define usando più flag -D sulla stessa linea.
    • I flag speciali -MMD -MP sono indispensabili in un makefile come questo, perché servono a gestire automaticamente le dipendenze dagli header, come premesso all'inizio.
  • # flag per il compilatore CC durante la creazione dei file oggetto
    Qui si mettono i flag aggiuntivi da fornire al compilatore C. Per semplificare si puó omettere CFLAGS e mettere tutto in CPPFLAGS, ma la forma estesa presentata qui ha dei vantaggi: ad esempio in un makefile misto (C e C++) mettendo in CFLAGS il flag -std=c11 sono sicuro che non verrà usato per i sorgenti C++ (che, automaticamente, saltano CFLAGS e usano, se disponibile, il flag corrispondente del C++ che è CXXFLAGS).
  • # flag per il linker LD durante la creazione del programma eseguibile
    Qui si mettono i flag aggiuntivi da dare al compilatore quando deve invocare il linker  ld, come il flag -Lpath_delle_librerie (nell'esempio è -L../include e si possono aggiungere più path a piacere). Le librerie (e.g.: -lcurl) dovrebbero invece essere aggiunte alla variabile LDLIBS (vedi il prossimo punto).
  • # librerie che il linker LD deve collegare
    Qui si mettono i nomi delle librerie forniti al compilatore quando deve invocare il linker  LD. I flag che non si riferiscono a librerie, come -Lpath_delle_librerie, vanno invece inseriti nella variabile LDFLAGS (come visto nel punto precedente). Per semplificare si può omettere LDLIBS e mettere tutto in LDFLAGS, ma la forma estesa presentata qui può avere dei vantaggi in alcuni casi particolari. Nell'esempio ho linkato la libcurl (e si possono aggiungere più librerie a piacere), ma se il progetto non usa nessuna libreria esterna LDLIBS si può lasciare vuoto.
  • # creazione del target file eseguibile
    Qui si mette il nome dell'eseguibile da creare e da chi dipende e, nella linea successiva, il comando per linkare i file oggetto creati e produrre il file eseguibile finale. Si usa un comando generico che usa le variabili LD e LDFLAGS (e, se usata, anche LDLIBS) e richiama automaticamente tutti gli oggetti compilati (è quel'espressione speciale $^ -o $@ che si espande, più o meno, in "prendi tutti gli oggetti ($^ ) e genera il target corrispondente (-o $@)").
  • # creazione degli object files
    Qui si mette nome (i file .o) e dipendenza (i file .c) dei file oggetto, e, nella linea successiva, il comando generico per compilare ogni sorgente e creare il l'oggetto corrispondente. Si usa un comando generico che usa le variabili CPPFLAGS (e, se usata, anche CFLAGS) e richiama automaticamente tutti i sorgenti da compilare (è quell'espressione speciale $< -o $@  che si espande, più o meno, in "prendi tutti i sorgenti ($<) e genera gli oggetti corrispondenti (-o $@)").
  • # direttive phony
    Qui si mettono tutte le direttive phony (uhm, questo è un po' lungo da spiegare: aprite il link, che è chiarissimo).
  • # pulizia progetto ($(RM) è di default "rm -f")
    Qui si mette il comando di cancellazione degli oggetti e dell'eseguibile per, eventualmente, forzare una successiva ricompilazione completa.
  • # creazione dipendenze
    Qui si mette il comando per generare i file di dipendenza che ci permettono di ricompilare solo quello che serve quando modifichiamo un header file.

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 strumenti come Autotools CMake... ma vi assicuro che il metodo rapido e vecchia-scuola che ho descritto è usabile sempre e senza limitazioni (io l'ho usato in progetti di produzione, giuro!). Sono soddisfazioni...

Nella prossima e seconda parte del post prenderò lo stesso makefile e lo adatterò per creare una shared-library: so che è un argomento molto interessante, ma non trattenete il respiro nell'attesa! (può nuocere gravemente alla salute...).

Ciao e al prossimo post!

martedì 27 agosto 2024

No Comment
come e perché scrivere i commenti nel C

Dom Cobb: Ég hef allt undir stjórn.
Arthur: hið gagnstæða væri alvarlegt.

(...una premessa: questo post è un remake di un mio vecchio post. Ma, anche se tratta lo stesso argomento, amplia un po' il discorso è mi è sembrato il caso di riproporlo. Leggete e mi direte...)

Non so se avete visto lo splendido Inception di Christopher Nolan. Bene, provate a pensare come sarebbe vedere un film con una trama così complessa doppiato in una lingua che non conoscete, come l'islandese per esempio (e senza sottotitoli!). Ci capireste qualcosa? Cosa si dicono Dom Cobb e Arthur qua sotto? Ecco, i sottotitoli sono i commenti del Cinema...

...adesso ti spiego: Ég hef allt undir stjórn...

Se siete di quelli che "Non scrivo commenti. Sono una perdita di tempo"  fermatevi qui. Tanto non riuscirei a convincervi (e neanche ci tengo).

Per tutti gli altri: il commento è un vostro amico fedele, non vi abbandonerà mai. Il commento è l'ultima barriera tra il capire e il non capire "Cosa avevo in testa quando ho scritto quel codice un anno fa ?". Il commento è la vostra dimostrazione di rispetto per i colleghi di lavoro: prima o poi, qualcuno di loro metterà le mani sul vostro Software, e il numero di maledizioni che vi manderà sarà inversamente proporzionale al numero (e alla qualità) dei commenti che avete scritto.

Certo, ci sono varie maniere di commentare il codice: si va dal "Absolute No Comment" al "Auto-Commentante con aggiunta di commenti" (beh, quest'ultimo è un po' esagerato), con tutte le sfumature intermedie possibili...

Facciamo una piccola dimostrazione: esaminiamo tre maniere si scrivere una funzione di lettura di un dispositivo di controllo industriale: si tratta, nel Caso 3, di codice quasi reale (solo quasi, eh!) che ho scritto alcuni anni fa: per questo esempio l'ho solo adattato un po'. Si noti che i tre codici sono praticamente identici, anche se la lettura dà tutt'altra impressione...

Caso 1: codice Absolute No Comment

int smsg(Dev *p)
{
int n=0;
if (p->msg[0]) {
snprintf(p->btx,sizeof(p->btx),"data: %s",p->msg);
if (!(n=(sdata(p->ch,(char*)p->btx,strlen(p->btx)))))
return -1;
p->msg[0]=0;
}
return n;
}

Come avrete notato non ci sono commenti, il codice è (a dir poco) criptico, e si risparmia su tutto: nomi, spazi, linee: non si sa mai che qualcuno riesca perfino a capire cosa c'è scritto. Per maniaci della segretezza.

Caso 2: codice auto-commentante

int sendMessageToDeviceXYZ(
DeviceXYZ *p_deviceXYZ)
{
int number_sent = 0;

if (p_deviceXYZ->message_to_send[0]) {
snprintf(p_deviceXYZ->buffer_tx,
sizeof(p_deviceXYZ->buffer_tx), "data: %s",
p_deviceXYZ->message_to_send);

if ((number_sent = sendDeviceData(p_deviceXYZ->channel_tx,
(char *)p_deviceXYZ->buffer_tx,
strlen(p_deviceXYZ->buffer_tx))) == -1) {
return -1;
}

p_deviceXYZ->message_to_send[0] = 0;
}

return number_sent;
}

Come avrete notato non ci sono commenti, ma il codice è auto-esplicativo e non si va al risparmio. I nomi sono scelti per la massima chiarezza. Per chi non ama i commenti ma vuole essere chiaro a tutti i costi.

Caso 3: codice chiaro e commentato

/* NAME
* sendMsgDev - invia un messaggio al dispositivo
* SYNOPSIS
* int sendMsgDev(
* DevXYZ *dev); // dispositivo destinazione
* DESCRIPTION
* sendMsgDev() invia un messaggio al dispositivo di tipo XYZ. L'argomento <dev>
* è un pointer alla struttura dati del dispositivo.
* RETURN VALUE
* In caso di successo, sendMsgDev() restituirà il numero di caratteri inviati.
* Altrimenti, verrà restituito -1.
*/

int sendMsgDev(
DevXYZ *dev) // dispositivo destinazione
{
// reset numero di char spediti
int n_sent = 0;

// controllo se c'è un messaggio da spedire
if (dev->msg_2_snd[0]) {
// formatto il messaggio nel buffer di trasmissione
snprintf(dev->buf_tx, sizeof(dev->buf_tx), "data: %s", dev->msg_2_snd);

// spedisco il messaggio al dispositivo
if ((n_sent = sendData(dev->chan_tx,
(char *)dev->buf_tx, strlen(dev->buf_tx))) == -1) {
// errore di invio
return -1;
}

// messaggio spedito: reset del contenuto
dev->msg_2_snd[0] = 0;
}

// ritorno il numero di char spediti
return n_sent;
}

Come avrete notato ci sono una intestazione (abbastanza completa, ma si può espandere) e molti brevi commenti. Il codice non è auto-esplicativo ma è sufficientemente chiaro. Per comment-lovers.

Qualcuno potrebbe dire che in questo ultimo caso ci sono fin troppi commenti, e magari molto ovvi: ma io penso che Melius abundare quam deficere e se a qualcuno i commenti non interessano può anche non leggerli! O dà così fastidio che ci siano dei dettagli che solo alcuni possano trovare utili? Bisogna ricordare che l'obiettivo del Caso 3 è che si possa interpretare il codice solo leggendo i commenti, ma non è obbligatorio farlo! Ci sono troppi commenti? E allora non leggerli e non rompermi i cabasisi!

Inutile dirlo: io appartengo alla scuola comment-lovers. Però rispetto anche i seguaci del codice auto-commentante. Sui signori del Absolute No Comment l'unica cosa che posso dire è... No Comment. Comunque, se vi può interessare, il Caso 1 non è una forzatura, e ho visto anche di peggio, non fatemi parlare... Anzi, si, parlerò! E vi racconterò tre aneddoti:

  • Ho sentito con le mie orecchie (true story) frasi di questo tipo: "Ho dovuto fare dei cambi a quel Software che avevi scritto tu: inutile dirti che, grazie al codice chiarissimo e stra-commentato, ho potuto farlo in un attimo. Grazie!" (il "tu" della frase è uno specialista del Codice Chiaro e Commentato, non faccio nomi, ah ah ah).
  • Ho sentito con le mie orecchie (true story) frasi di questo tipo: "Devo mettere le mani sul Software scritto da quel mascalzone di Tizio Caio... Ci metterò una settimana solo per capire dove fare le modifiche! Per fortuna se ne è andato, ma ci ha lasciato una bella eredità..." (il sig."Tizio Caio" è uno specialista del codice Absolute No Comment, ometto il nome vero...).
  • Ho lavorato in posti dove ho visto (giuro!file di migliaia di linee non auto-commentanti e senza neanche un commento (orrore! orrore!).

Ecco, so che quanto sto per dire vi sembrerà un po' radicale (e in effetti lo è), ma lo dirò lo stesso: se quanto ho scritto nell'articolo vi da fastidio o vi sembra inutile può essere dovuto solo a due motivi:

  1. Sicuramente non avete mai lavorato su Software scritto da altri... siete proprio fortunati!
  2. Siete quel mascalzone di Tizio Caio (o siete uguali a lui).

Ok, per oggi può bastare. Chiedo scusa se ho offeso qualcuno ma, tutto sommato, chi si è offeso ha la coda di paglia e se lo merita, ah ah ah.

Ciao e al prossimo post!