Scrivere Software è un piacere. Un programma non solo deve funzionare bene ed essere efficiente (questo si dà per scontato), ma deve essere anche bello ed elegante da leggere, comprensibile e facile da manutenere, sia per l'autore che per eventuali lettori futuri. Programmare bene in C è un'arte.
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 21 dicembre 2024
lunedì 16 dicembre 2024
Thread Runner
come usare i thread in C - pt.2
Eldon Tyrell: Quale sarebbe il tuo problema?
Roy Batty: La morte.
Eldon Tyrell: La morte... beh questo temo sia un po' fuori della mia giurisdizione.
Roy Batty: Io voglio più vita, padre!
(...una premessa: questo post è un remake di un mio vecchio post (parte 2 di 3). Ma, anche se tratta lo stesso argomento, amplia e perfeziona un po' il discorso è mi è sembrato il caso di riproporlo. Leggete e mi direte...)
![]() |
...ti spiego: io sono un POSIX thread e tu un C11 thread... |
Ma torniamo al nostro amato C: il committee ISO del C non poteva rimanere indietro, e quindi hanno pensato di fare la stessa cosa con il C11, ovvero far si che i thread siano direttamente una parte del C... e ci sono riusciti? Prima di rispondere vi anticipo una considerazione: ho una stima del committee ISO del C maggiore di quella che ho di quello del del C++ (e non ci voleva molto...), ma in questo caso devo proprio dire che non ci siamo: a seguire vedremo il perché.
Come sono stati pensati i nuovi C11 Threads? Allora, hanno preso tutte le funzioni e variabili che compongono i POSIX Threads e gli hanno cambiato il nome (e devo ammettere che quelli nuovi sono più semplici e immediati); inoltre, in alcuni casi (pochi, per fortuna), hanno cambiato i tipi dei codici di ritorno e degli argomenti delle funzioni. Punto. Geniale? Non proprio direi, e niente a che vedere con la soluzione brillante usata nel C++11. Motivi per usare questa nuova versione? Zero, direi, e non vi ho ancora esposto l problemi principali...
Comunque, per farla breve, ho riscritto l'esempio dello scorso post usando i C11 Threads. Vai col codice!
#include <stdio.h>#include <string.h>#include <unistd.h>#include <stdbool.h>#include <threads.h>#define NUMTHREADS 2 // numero di thread da trattare, in questo caso 2// struttura per i dati condivisi dai threadtypedef struct {mtx_t mutex; // mutex di sincronizzazione comune ai threadbool stop; // flag per stop threadunsigned long cnt; // counter condiviso dai thread} Thdata;// prototipi localiint threadFunc(void *arg);// funzione main()int main(int argc, char* argv[]){char errmsg_buf[256]; // buffer per strerror_r(3)// init dei dati condivisi dai threadThdata thdata;thdata.stop = false;thdata.cnt = 0;// init del mutex di Thdataint error;if ((error = mtx_init(&thdata.mutex, mtx_plain)) != thrd_success) {// errore!printf("%s: non posso creare il mutex (%d)\n", argv[0], error);return 1;}// loop di avvio dei threadthrd_t tid[2];for (int i = 0; i < NUMTHREADS; i++) {// creo un threadif ((error = thrd_create(&tid[i], threadFunc, &thdata)) != thrd_success) {// errore!printf("%s: non posso creare il thread %d (%d)\n", argv[0], i, error);return 1;}}// dopo 10 secondi fermo tutti i threadsleep(10);thdata.stop = true;// loop di join dei threadfor (int i = 0; i < NUMTHREADS; i++) {if ((error = thrd_join(tid[i], NULL)) != thrd_success) {// errore!printf("%s: non posso attendere il thread %d (%d)\n", argv[0], i, error);return 1;}}// cancello il mutex di sincronizzazionemtx_destroy(&thdata.mutex);// escoprintf("%s: thread terminati: thdata.cnt=%lu\n", argv[0], thdata.cnt);return 0;}// threadFunc() - funzione per i threadint threadFunc(void *arg){// ottengo i dati del thread con un cast (Thdata *) di (void *) argThdata *thdata = (Thdata *)arg;// loop del threadprintf("thread partito\n");unsigned long loc_cnt = 0;for (;;) {// lock del mutexmtx_lock(&thdata->mutex);// incremento il counter locale e quello condivisoloc_cnt++;thdata->cnt++;// unlock del mutexmtx_unlock(&thdata->mutex);// test dello stop flagif (thdata->stop) {// il thread esceprintf("thread terminato dal main: loc_cnt=%lu\n", loc_cnt);thrd_exit(0);}// sleep del thread (uso usleep solo per comodità invece della nanosleep(2))usleep(1000);}// il thread esce per altro motivo diverso dallo stop flagprintf("thread terminato localmente: loc_cnt=%lu\n", loc_cnt);thrd_exit(0);}
Come vedete il codice nuovo è praticamente identico al vecchio, mi sono limitato a usare le nuove funzioni al posto di quelle vecchie (per esempio thrd_create() invece di pthread_create(3)) , ho usato i nuovi tipi (per esempio mtx_t invece di pthread_mutex_t) e ho leggermente modificato il test dei valori di ritorno: poche differenze, devo dire, e, in alcuni casi, in peggio: ad esempio è sparito il parametro attr di pthread_create(3), che (per semplicità) nello scorso esempio avevo lasciato a NULL, ma che a volte può risultare utile (leggere il manuale della funzione per rendersene conto). Comunque si potrebbe dire (senza fare troppo gli schizzinosi) che la nuova interfaccia non ci offre nessun vantaggio sostanziale, ma neanche un peggioramento decisivo, quindi si potrebbe anche usare (de gustibus).
Ma c'è un problema: pare che i C11 Threads non siano stati considerati una priorità per chi scrive i compilatori e le varie libc, quindi per molti anni dall'introduzione è stato difficile compilare/eseguire un programma come quello che ho mostrato. Perfino il nostro amato GCC (che di solito è il primo a fornire supporto per le ultime novità) ci ha messo una vita a supportare i nuovi thread (in realtà a causa della mancata integrazione nella glibc). Questo è un brutto segno, per niente incoraggiante: se il mondo che gira intorno ai linguaggi e ai Sistemi Operativi non approva rapidamente vuol dire che ci sono delle perplessità.
Comunque dopo tanto tempo qualcosa è arrivato, e adesso è possibile compilare i C11 Threads su Linux (con versioni recenti di GCC e con una glibc dalla v.2.28 in su), sui vari sistemi BSD, su Windows, ma non ancora su macOS (se non mi sbaglio). Aggiungo un dettaglio illuminante sulla coppia GCC/glibc: in attesa di un supporto veramente nativo, la compilazione era basata su una implementazione che era, praticamente, un wrapper che simulava i C11 Threads usando i POSIX Threads (infatti bisognava linkare la libpthread): un po' assurdo, no?
E anche se ora si può usare direttamente GCC con una glibc recente (la v.2.28 è del 2018), io ho deciso di seguire con la stessa soluzione che è stata disponibile quasi da subito (già nel 2014) e ho già usato in passato: ho usato la musl libc che è una alternativa alla glibc, ed è dotata di un wrapper per GCC (musl-gcc). musl fornisce (su Linux) il supporto completo al C11, ed è stata (credo) la prima in assoluto a fornire il supporto nativo ai C11 Thread. E come funziona tutto questo? Una volta compilato il programma si comporta correttamente, come potete vedere qui sotto:
aldo@Linux $ musl-gcc c11threads.c -o c11threadsaldo@Linux $ ./c11threadsthread partitothread partitothread terminato dal main: loc_cnt=8597thread terminato dal main: loc_cnt=8597./c11threads: thread terminati: thdata.cnt=17194
Ma il gioco vale la candela? No, per quel che mi riguarda continuerò ad usare i POSIX Threads, che uso da molti anni e rimangono il riferimento d'eccellenza. I C11 Threads sono uno strano ibrido che non rispetta la classica sintassi della glibc (ad esempio in caso di errore non ritorna -1 ma un codice di errore e, oltretutto, non aggiorna errno), e non rispetta pienamente neanche lo standard POSIX (i codici di errore non sono inseriti in una lista analizzabile con strerror(3)). E poi, come già detto sopra, le nuove funzioni sono equivalenti alle vecchie ma non abbastanza, alcune funzionalità non sono più disponibili e alcune cose semplici (come stampare il thread Id interno della libreria o le stringhe degli errori) sono molto più complicate... e, dulcis in fundo, nelle classiche pagine dei manuali Linux e POSIX non c'è ancora traccia delle nuove funzioni. Son problemi, no?
Ok, per oggi chiudo qui. Nel prossimo e ultimo episodio di Thread Runner vedremo un esempio pratico. Rimanete sintonizzati, eh!
Ciao e al prossimo post!
venerdì 22 novembre 2024
Thread Runner
come usare i thread in C - pt.1
Roy Batty: Io ne ho viste cose che voi umani non potreste immaginarvi. Navi da combattimento in fiamme al largo dei bastioni di Orione... e ho visto i raggi B balenare nel buio vicino alle porte di Tannhäuser. E tutti quei momenti andranno perduti nel tempo come lacrime nella pioggia. È tempo di morire.
(...una premessa: questo post è un remake di un mio vecchio post (parte 1 di 3). Ma, anche se tratta lo stesso argomento, amplia e perfeziona un po' il discorso è mi è sembrato il caso di riproporlo. Leggete e mi direte...)
I thread sono un po' come i replicanti del mitico capolavoro Blade Runner del Maestro Ridley Scott: sono copie dei loro creatori, sembrano tutti uguali ma poi divergono e hanno una vita propria... e poi terminano, come tristemente descritto nel mitico ed emozionante monologo del bravissimo Rutger Hauer. E quindi oggi parleremo di thread, che è un argomento che ho già più volte affrontato, e in varie sfumature: i problemi con la sleep(3) in Sleep? No, grazie!, il controllo in Totò, Peppino e il Watchdog, il mal uso in Thread Cancel? No, grazie!, l'uso inflazionato in L'invasione dei Multithread, le alternative in Processi o Thread? e poi in molti altri articoli in cui li ho usati negli esempi senza citarli direttamente. Ma ho sempre affrontato l'argomento come se fosse qualcosa di consolidato, qualcosa su cui era inutile entrare in dettagli elementari. Beh, è giunto il momento di di fare un bell'articolo sulle basi, un articolo introduttivo su come usare i thread in C, un articolo che arriva (forse) un po' in ritardo, ma meglio tardi che mai!
![]() |
...come lacrime nella pioggia... |
In questo post (che è il primo di tre) vedremo un esempio semplice semplice di come usare i thread in C: ovviamente l'argomento è molto vasto e complicabile a piacere, ma il nostro esempio contiene già le basi per capire come funziona il tutto, ovvero: la creazione, la sincronizzazione e la terminazione dei thread. Ovviamente in questa prima parte cominceremo usando la versione base (quasi) universale, ovvero useremo i POSIX Threads. E ora bando alle ciance, vai col codice!
#define _GNU_SOURCE // per usare strerror_r(3) GNU-specific version#include <stdio.h>#include <string.h>#include <unistd.h>#include <stdbool.h>#include <pthread.h>#define NUMTHREADS 2 // numero di thread da trattare, in questo caso 2// struttura per i dati condivisi dai threadtypedef struct {pthread_mutex_t mutex; // mutex di sincronizzazione comune ai threadbool stop; // flag per stop threadunsigned long cnt; // counter condiviso dai thread} Thdata;// prototipi localivoid* threadFunc(void *arg);// funzione main()int main(int argc, char* argv[]){char errmsg_buf[256]; // buffer per strerror_r(3)// init dei dati condivisi dai threadThdata thdata;thdata.stop = false;thdata.cnt = 0;// init del mutex di Thdataint error;if ((error = pthread_mutex_init(&thdata.mutex, NULL)) != 0) {// errore!printf("%s: non posso creare il mutex (%s)\n",argv[0], strerror_r(error, errmsg_buf, sizeof(errmsg_buf)));return 1;}// loop di avvio dei threadpthread_t tid[2];for (int i = 0; i < NUMTHREADS; i++) {// creo un threadif ((error = pthread_create(&tid[i], NULL, &threadFunc, (void *)&thdata)) != 0) {// errore!printf("%s: non posso creare il thread %d (%s)\n",argv[0], i, strerror_r(error, errmsg_buf, sizeof(errmsg_buf)));return 1;}}// dopo 10 secondi fermo tutti i threadsleep(10);thdata.stop = true;// loop di join dei threadfor (int i = 0; i < NUMTHREADS; i++)if ((error = pthread_join(tid[i], NULL)) != 0) {// errore!printf("%s: non posso attendere il thread %d (%s)\n",argv[0], i, strerror_r(error, errmsg_buf, sizeof(errmsg_buf)));return 1;}// cancello il mutex di sincronizzazioneif ((error = pthread_mutex_destroy(&thdata.mutex)) != 0) {// errore!printf("%s: non posso cancellare il mutex (%s)\n",argv[0], strerror_r(error, errmsg_buf, sizeof(errmsg_buf)));return 1;}// escoprintf("%s: thread terminati: thdata.cnt=%lu\n", argv[0], thdata.cnt);return 0;}// threadFunc() - funzione per i threadvoid *threadFunc(void *arg){// ottengo i dati del thread con un cast (Thdata *) di (void *) argThdata *thdata = (Thdata *)arg;// loop del threadprintf("thread %ld partito\n", pthread_self());unsigned long loc_cnt = 0;for (;;) {// lock del mutexpthread_mutex_lock(&thdata->mutex);// incremento il counter locale e quello condivisoloc_cnt++;thdata->cnt++;// unlock del mutexpthread_mutex_unlock(&thdata->mutex);// test dello stop flagif (thdata->stop) {// il thread esceprintf("thread %ld terminato dal main: loc_cnt=%lu\n", pthread_self(), loc_cnt);pthread_exit(NULL);}// sleep del thread (uso usleep solo per comodità invece della nanosleep(2))usleep(1000);}// il thread esce per altro motivo diverso dallo stop flagprintf("thread %ld terminato localmente: loc_cnt=%lu\n", pthread_self(), loc_cnt);pthread_exit(NULL);}
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.
Supponendo che già sappiate cosa sono e a cosa servono i thread (se no leggetevi prima qualche guida introduttiva, in rete ce ne sono di ottime) il flusso del codice è evidente: si inizializzano i dati da passare ai thread, compreso il mutex di sincronizzazione che deve essere creato con pthread_mutex_init(3); dopodiché con pthread_create(3) si creano/avviano i due thread che useremo, usando un loop (ma si potevano anche creare ripetendo due volte i passi, ovviamente). A questo punto si blocca il main() con una sleep(3) di 10 secondi e alla scadenza si attiva il flag di stop dei thread e ci si mette in attesa (usando pthread_join(3)) della terminazione dei thread; quando sono terminati entrambi il main() distrugge il mutex (con pthread_mutex_destroy(3)) ed esce.
Come si nota pthread_create(3) ha quattro parametri, che sono (nell'ordine): un pointer a un thread descriptor che identifica univocamente il thread creato, un pointer a un contenitore di attributi del thread da creare, un function pointer alla funzione che esegue il thread e, infine, un pointer all'unico argomento che si può passare alla funzione suddetta. In particolare, nel nostro esempio (semplice semplice), ho usato gli attributi di default (usando NULL per il secondo parametro), e ho creato (con typedef) un nuovo tipo ad-hoc per passare più parametri alla funzione che esegue il thread, sfruttando il fatto che l'argomento di default è un void* che si può facilmente trasformare (con una operazione di cast) in qualsiasi tipo complesso (nel nostro caso nel nuovo tipo Thdata).
In questo esempio i due thread creati eseguono la stessa funzione, che ho chiamato (con molta originalità, ah aha ah) threadFunc() (ma avrebbero anche potuto eseguire due funzioni completamente differenti: in questo caso, ovviamente, avrei dovuto scrivere una threadFunc1() e una threadFunc2()). Il flusso della funzione è molto semplice: prima esegue un cast sull'argomento arg per poter usare i dati del tipo Thdata, poi entra in un classico thread-loop infinito con uscita controllata dallo stato dello stop flag inserito nella struttura Thdata. Notare che il thread-loop usa una sleep di 1 ms: provate a dimenticarvi di mettere la sleep in un thread-loop veramente infinito e vedrete i salti di gioia che farà la CPU del vostro PC!
Notare anche che ho usato, per comodità, usleeep(3) ma, come già fatto notare in un altro articolo, bisognerebbe usare sempre nanosleep(2) (usleep(3), come dice il manuale, è deprecata! ). E, comunque, anche la sleep del thread andrebbe usata con parsimonia come ho già scritto in un altro articolo, ci sono maniere più efficienti per controllare un loop infinito...
Ma cosa esegue il thread-loop? In questo semplice esempio si limita a incrementare un contatore locale e il contatore condiviso inizializzato nel main(), e lo fa in maniera sincronizzata usando pthread_mutex_lock(3) e pthread_mutex_unlock(3) sul mutex condiviso: questo serve per sincronizzare gli accessi ai dati condivisi, evitando che i due thread tentino di modificare contemporaneamente il contatore.
Compilando con GCC su macchina Linux (ovviamente) ed eseguendo, il risultato è:
aldo@Linux $ gcc -Wall threads.c -o threads -pthreadaldo@Linux $ ./threadsthread 137560485529280 partitothread 137560475043520 partitothread 137560475043520 terminato dal main: loc_cnt=8671thread 137560485529280 terminato dal main: loc_cnt=8670./threads: thread terminati: thdata.cnt=17341
Notare il numerone corrispondente al thread_id: stiamo stampando il risultato di pthread_self(3) che è di tipo pthread_t, ossia un tipo opaco (in questo caso anche machine-dependent): su Linux è, normalmente, un long unsigned int, quindi il risultato è corretto usando una printf(3) con format=%lu, ma in altri casi bisognerebbe trattarlo differentemente; comunque per questo semplice esempio va bene così, tanto l'ho stampato solo a titolo informativo.
E i risultati? Sono Ok: il valore dei due contatori è, esattamente quello sperato: ogni thread ha incrementato quando era il suo turno e i due valori locali sono (praticamente) identici e anche il contatore condiviso corrisponde alla somma dei due... perfetto! Thread sincronizzati alla grande!
E per oggi può bastare, non voglio far addormentate nessuno... Nel prossimo post parleremo di una interfaccia alternativa ai POSIX Threads. E, come sempre, vi raccomando di non trattenere il respiro nell'attesa, può nuocere gravemente alla salute!
Ciao e al prossimo post!
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...)
![]() |
...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 (chechiameremo "libmyutils.so") e copiarla in "/usr/local/lib/pluto".3. Aggiungere in "/etc/ld.so.conf.d" un nuovo file "libmyutils.conf" che conterrà leseguenti due linee (la prima è solo un commento):# libmyutils.so default configuration/usr/local/lib/pluto4. 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:
# variabiliSRCS = $(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/plutoNAMELIB = libmyutils.soOBJS_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=c11CPPFLAGS_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 -pthreadLDFLAGS_SHLIB = -shared -fpic# i due target: shared-lib e applicazioneall: libmyutils pluto# creazione del target applicazione "pluto"pluto: $(OBJS_APP)$(CC) $^ -o $@ $(LDFLAGS)# creazione del target shared-lib "libmyutils.so" con copia nella directory destinazionelibmyutils: $(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:
- 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.
- 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.
- 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!
# variabiliSRCS = $(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 = gccLD = gcc # NOTA: usualmente si omette e si usa solo CC# flag per il preprocessore di CC durante la creazione dei file oggettoCPPFLAGS = -I../include -g -O2 -Wall -pedantic -pthread -DUNA_MIA_DEFINE -MMD -MP# flag per il compilatore CC durante la creazione dei file oggettoCFLAGS = -std=c11 # NOTA: CFLAGS usualmente si omette e va tutto in CPPFLAGS# flag per il linker LD durante la creazione del programma eseguibileLDFLAGS = -Lpath_delle_librerie -pthread# librerie che il linker LD deve collegareLDLIBS = -lcurl # NOTA: LDLIBS usualmente si omette e va tutto in LDFLAGS# creazione del target file eseguibilepluto: $(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 o 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!