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ì 10 agosto 2018

Per qualche strlen in più
come ottimizzare la strlen in C - pt.2

col. Mortimer: Che succede ragazzo?
Il Monco: Niente vecchio... non mi tornavano i conti. Me ne mancava uno.
Ok, siamo pronti per la seconda parte di Per qualche dollaro in più (oops... ma non era Per qualche strlen in più?). Come giustamente diceva Il Monco, un buon programmatore fa sempre bene i conti, quindi in questo post analizzeremo i risultati di un benchmark che ho scritto ad hoc per questo scopo. (...si lo so, avevo anche promesso una sorpresa. Ci sarà...).
...niente vecchio... la strlen è un po' lenta oggi...
Allora, nel benchmark confronteremo i risultati (di velocità, ovviamente) delle quattro versioni di strlen() viste nello scorso post, e cioè: K&R, FreeBSD, glibc, e musl libc e il nostro punto di riferimento sarà, evidentemente, la strlen() di default del sistema, anche perché uno dei nostri obbiettivi è scoprire cosa usa di default Linux, che è, al solito, il nostro sistema di riferimento. Vediamo il codice del benchmark:
#include <stdint.h>
#include <limits.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <string.h>

// prototipi locali
size_t strlenKaR(const char *s);
size_t strlenFreeBSD(const char *str);
size_t strlenGlibc(const char *str);
size_t strlenMusl(const char *s);

int main(int argc, char* argv[])
{
    static char s[1000000000];
    clock_t t_start;
    size_t  result;
    clock_t t_end;
    double  t_passed;
    int i;

    // riempio la stringa di test
    for (i = 0; i < sizeof(s); i++) {
        if (i % 2)
            s[i] = 'a';
        else
            s[i] = 'u';
    }
    s[i - 1] = 0; // terminatore

    // esegue con strlen() di default e calcola tempo
    t_start  = clock();
    result   = strlen(s);
    t_end    = clock();
    t_passed = ((double)(t_end - t_start)) / CLOCKS_PER_SEC;
    printf("strlen(s)        = %zu - Tempo trascorso: %f secondi\n", result, t_passed);

    // esegue con strlenKaR() e calcola tempo
    t_start  = clock();
    result   = strlenKaR(s);
    t_end    = clock();
    t_passed = ((double)(t_end - t_start)) / CLOCKS_PER_SEC;
    printf("strlenKaR(s)     = %zu - Tempo trascorso: %f secondi\n", result, t_passed);

    // esegue con strlenFreeBSD() e calcola tempo
    t_start  = clock();
    result   = strlenFreeBSD(s);
    t_end    = clock();
    t_passed = ((double)(t_end - t_start)) / CLOCKS_PER_SEC;
    printf("strlenFreeBSD(s) = %zu - Tempo trascorso: %f secondi\n", result, t_passed);

    // esegue con strlenGlibc() e calcola tempo
    t_start  = clock();
    result   = strlenGlibc(s);
    t_end    = clock();
    t_passed = ((double)(t_end - t_start)) / CLOCKS_PER_SEC;
    printf("strlenGlibc(s)   = %zu - Tempo trascorso: %f secondi\n", result, t_passed);

    // esegue con strlenMusl() e calcola tempo
    t_start  = clock();
    result   = strlenMusl(s);
    t_end    = clock();
    t_passed = ((double)(t_end - t_start)) / CLOCKS_PER_SEC;
    printf("strlenMusl(s)    = %zu - Tempo trascorso: %f secondi\n", result, t_passed);

    return 0;
}
Come avrete notato, abbiamo creato una stringa enorme (e se no che test era?) e l'abbiamo riempita di una sequenza di "au" (...beh, si poteva riempire di qualsiasi cosa, ma quel giorno mi ero svegliato con un "au" in testa che non vi dico...). Abbiamo aggiunto il dovuto terminatore di stringa e chiamato in sequenza le varie strlen() usando un metodo affidabile (con clock()) per verificare il tempo reale di attività (un metodo che abbiamo già usato in un vecchio post). Le strlen() sono, ovviamente, quelle dello scorso post (i codici li trovate li), ribattezzate come strlenKaR(), strlenFreeBSD(), strlenGlibc() e strlenMusl(), per distinguerle dall'unica funzione non locale, che è la strlen() di libreria. Come si nota per la versione K&R ho modificato il prototipo (e la definizione) per uniformarla alla strlen() standard.

Vediamo ora i risultati del benchmark con compilazione normale (-O0, che è il default e si può anche omettere) e con una notevole ottimizzazione (-O2, che è quella che si usa normalmente nei casi reali):

usando gcc -O0
aldo@mylinux:~/blogtest$ ./strlens 
strlen(s)        = 999999999 - Tempo trascorso: 0.054814 secondi
strlenKaR(s)     = 999999999 - Tempo trascorso: 2.173237 secondi
strlenFreeBSD(s) = 999999999 - Tempo trascorso: 0.308145 secondi
strlenGlibc(s)   = 999999999 - Tempo trascorso: 0.252466 secondi
strlenMusl(s)    = 999999999 - Tempo trascorso: 0.304617 secondi

usando gcc -O2
aldo@mylinux:~/blogtest$ ./strlens 
strlen(s)        = 999999999 - Tempo trascorso: 0.164509 secondi
strlenKaR(s)     = 999999999 - Tempo trascorso: 0.395466 secondi
strlenFreeBSD(s) = 999999999 - Tempo trascorso: 0.091599 secondi
strlenGlibc(s)   = 999999999 - Tempo trascorso: 0.102967 secondi
strlenMusl(s)    = 999999999 - Tempo trascorso: 0.091931 secondi
I risultati sono veramente interessanti. La strlenKaR() è, come ci aspettavamo, molto più lenta delle versioni che usano l'algoritmo descritto in ZeroInWord e, anche compilando con -O2, è più lenta delle altre compilate con -O0, anche se, comunque, il divario tra le versioni ottimizzate è minore di quello tra le non ottimizzate. Ripeto, siamo in un caso limite: la stringa misurata è veramente enorme, e con stringhe "normali" la differenza di prestazioni sarebbe molto più piccola, comunque è evidente che le versioni con algoritmo speciale vanno meglio e sono, giustamente, da preferire, specialmente ricordando che "le super-ottimizzazioni sono sicuramente raccomandabili per le piccole funzioni di uso frequente" (...e questa è una mia auto-citazione dal mio ultimo post...). Le tre funzioni strlenFreeBSD(), strlenGlibc e strlenMusl() hanno prestazioni ottime ed analoghe, visto che usano lo stesso algoritmo, e la versione glibc prevale leggermente compilando con -O0, mentre che compilando con -O2 la migliore è la musl, con la FreeBSD a ruota.

E veniamo alla parte sorprendente: pare che la strlen() di default è nettamente più veloce di tutte le altre compilando con -O0 mentre perde il vantaggio compilando con -O2. Quindi, almeno senza ottimizzazioni, sul mio sistema Linux la strlen() di default non è quella della glibc (come uno si aspetterebbe) ma è qualcos'altro. E cosa è, allora? Proviamo a commentare la linea "#include <string.h>" e a ricompilare, e vediamo cosa ci dice il compilatore...
aldo@mylinux:~/blogtest$ gcc -c -O0 strlens.c -o strlens.o
strlens.c: In function 'main':
strlens.c:35:16: warning: implicit declaration of function 'strlen'
     result   = strlen(s);
                ^
strlens.c:35:16: warning: incompatible implicit declaration of built-in function 'strlen'
strlens.c:35:16: note: include '<string.h>' or provide a declaration of 'strlen'
Ecco, pare che la strlen() di default sia una funzione implicita del compilatore gcc, quindi non si usa la versione della libreria e, del resto "...in alcuni casi queste funzioni possono essere addirittura inlineate e/o scritte in assembler..." (...e questa è un'altra mia auto-citazione dal mio ultimo post...). Per confermare tutto questo ho cercato una versione in assembler della strlen() e l'ho trovata qui su stackoverflow. Ho modificato leggermente Il codice (la originale non restituiva correttamente il risultato) e ho modificato il benchmark per usarla. Il codice è il seguente:
#include <immintrin.h>

// una possibile implementazione in assembler della strlen() implicita in gcc
size_t strlenAsm(const char* src)
{
    size_t result = 0;
    unsigned int tmp1;
    __m128i zero = _mm_setzero_si128(), vectmp;

    // A pointer-increment may perform better than an indexed addressing mode
    asm(
        "\n.Lloop:\n\t"
            "movdqu   (%[src], %[res]), %[vectmp]\n\t" // result reg is used as the loop counter
            "pcmpeqb  %[zerovec], %[vectmp]\n\t"
            "pmovmskb %[vectmp], %[itmp]\n\t"
            "add      $0x10, %[res]\n\t"
            "test     %[itmp], %[itmp]\n\t"
            "jz  .Lloop\n\t"

        "bsf %[itmp], %[itmp]\n\t"
        "add %q[itmp], %q[res]\n\t"                // q modifier to get quadword register.
        : [res] "+r"(result), [vectmp] "=&x" (vectmp), [itmp] "=&r" (tmp1)
        : [zerovec] "x" (zero)  // There might already be a zeroed vector reg when inlining
            , [src] "r" (src)
            , [dummy] "m" (*(const char (*)[])src) // this reads the whole object, however
                                                   // long gcc thinks it is not needed
                                                   // because of the dummy input
    );

    return result - tmp1 - 1;
}
e i risultati diventano così (con -O0, e con -O2 il risultato della strlenAsm() non cambia visto che, essendo in assembler, viene compilata a parte e senza ottimizzazioni, come si può notare nelle linee successive):

usando gcc -O0
aldo@mylinux:~/blogtest$ gcc -c strlenasm.c -o strlenasm.o
aldo@mylinux:~/blogtest$ gcc -c -O0 strlens.c -o strlens.o
aldo@mylinux:~/blogtest$ gcc strlens.o strlenasm.o -o strlens
aldo@mylinux:~/blogtest$ ./strlens 
strlen(s)        = 999999999 - Tempo trascorso: 0.054087 secondi
strlenKaR(s)     = 999999999 - Tempo trascorso: 2.163937 secondi
strlenFreeBSD(s) = 999999999 - Tempo trascorso: 0.306909 secondi
strlenGlibc(s)   = 999999999 - Tempo trascorso: 0.251383 secondi
strlenMusl(s)    = 999999999 - Tempo trascorso: 0.305544 secondi
strlenAsm(s)     = 999999999 - Tempo trascorso: 0.061378 secondi
Ecco, il risultato canta: la versione implicita di gcc è scritta in assembler (e ripeto: non è affatto una scelta stranissima per piccole funzioni di libreria di uso frequente). Resta il mistero del perché compilando con -O2 (riguardare i risultati più sopra) la strlen() di default sembra non essere più la versione implicita, visto che le sue prestazioni peggiorano. Questo è un mistero che mi riprometto di risolvere prima o poi.

(...ma, visto che siamo ad Agosto, sicuramente durante le vacanze me ne dimenticherò, di solito mentre sono in spiaggia penso a ben altro...)

Ciao e al prossimo post!

venerdì 20 luglio 2018

Per qualche strlen in più
come ottimizzare la strlen in C - pt.1

El Indio: Dove vai?
Il Monco: A dormire. Quando devo sparare la sera prima vado a letto presto.
Sapete bene che Sergio Leone è molto amato in questo blog. Per qualche dollaro in più era l'unico che non avevo ancora citato della trilogia del dollaro (gli altri due sono qui e qui), quindi è venuto il suo momento. Oggi parleremo della strlen() e mi raccomando: andate a dormire presto solo per scrivere del buon codice la mattina seguente, e non per fare quello che riesce meglio al Monco...
...sono venuto qui per scrivere del buon Software...
Veniamo al dunque: la strlen() è una funzione molto semplice, che esegue un compito semplice (restituire la lunghezza di una stringa), e infatti si può scrivere, letteralmente, in quattro linee di codice. Vediamo la versione riportata sul magico K&R:
int strlen(char *s)
{
    char *p = s;
    while (*p != '\0')
        p++;

    return p - s;
}
Questa versione, semplicissima e perfetta (e ci mancherebbe solo, visti i mitici autori!) va benissimo ed è soddisfacente nel 99% dei casi, a maggior ragione se compilata con ottimizzazione. Ma... se dovessimo fare un uso intensivo della strlen(), magari per misurare stringhe enormi, sarebbe sufficiente? Beh, secondo gli estensori delle varie libc disponibili, la strlen() deve essere implementata con algoritmi un po' più complicati di quello visto sopra, garantendo (effettivamente) prestazioni elevatissime a patto di pagare il prezzo di usare un codice complicato (e difficile da leggere) per una operazione veramente semplice. Si potrebbe fare una disquisizione molto filosofica sull'argomento e, se andate a rileggervi un mio vecchio post, vi sarà chiaro ciò che penso al riguardo.

Però... ecco, bisogna distinguere tra il codice di libreria (come la libc) e il codice applicativo: il primo si suppone che verrà usato da molti utenti per svariati progetti su molte piattaforme, quindi si presume, giustamente, che sia ottimizzato al massimo e, pertanto, leggibilità e manutenibilità diventano problemi secondari. Il codice applicativo, invece, deve essere scritto bene e deve essere efficiente (ci mancherebbe solo!), ma con un occhio di riguardo anche a leggibilità e manutenibilità, perché una applicazione, spesso, viene toccata e ritoccata nel tempo e da più programmatori (...quindi occhio a quello che c'è scritto sotto il titolo del blog qua sopra: è la dichiarazione della filosofia del blog, e cade a pennello in questo caso...).

Alla fine della fiera, le super-ottimizzazioni sono sicuramente raccomandabili per le piccole funzioni di uso frequente (come quelle della famiglia str, ad esempio), e a maggior ragione quando possono trattare volumi elevati di dati (come stringhe e/o blocchi di memoria molto grandi). In alcuni casi queste funzioni possono essere addirittura inlineate e/o scritte in assembler. Invece, per il "codice normale" è meglio seguire le indicazioni di uno molto più bravo di me che disse, qualche annetto fa:
"The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming" (Donald Knuth, "Computer Programming as an Art", 1974).
Ok, torniamo al punto: la strlen() che usiamo tutti i giorni è, quasi sempre, super-ottimizzata. Vediamo, quindi, alcuni esempi reali:
   
implementazione in FreeBSD
/* Magic numbers for the algorithm */
static const unsigned long mask01 = 0x0101010101010101;
static const unsigned long mask80 = 0x8080808080808080;

#define LONGPTR_MASK (sizeof(long) - 1)

// Helper macro to return string length if we caught the zero byte.
#define testbyte(x)               \
    do {                          \
        if (p[x] == '\0')         \
            return (p - str + x); \
    } while (0)

size_t strlen(const char *str)
{
    const char *p;
    const unsigned long *lp;
    long va, vb;

    /* Before trying the hard (unaligned byte-by-byte access) way
     * to figure out whether there is a nul character, try to see
     * if there is a nul character is within this accessible word
     * first.
     * p and (p & ~LONGPTR_MASK) must be equally accessible since
     * they always fall in the same memory page, as long as page
     * boundaries is integral multiple of word size.
     */
    lp = (const unsigned long *)((uintptr_t)str & ~LONGPTR_MASK);
    va = (*lp - mask01);
    vb = ((~*lp) & mask80);
    lp++;
    if (va & vb)
        /* Check if we have \0 in the first part */
        for (p = str; p < (const char *)lp; p++)
            if (*p == '\0')
                return (p - str);

    /* Scan the rest of the string using word sized operation */
    for (; ; lp++) {
        va = (*lp - mask01);
        vb = ((~*lp) & mask80);
        if (va & vb) {
            p = (const char *)(lp);
            testbyte(0);
            testbyte(1);
            testbyte(2);
            testbyte(3);
            testbyte(4);
            testbyte(5);
            testbyte(6);
            testbyte(7);
        }
    }

    /* NOTREACHED */
    return (0);
}
implementazione in glibc
/* Return the length of the null-terminated string STR.  Scan for
   the null terminator quickly by testing four bytes at a time. */
size_t strlen(const char *str)
{
    const char *char_ptr;
    const unsigned long int *longword_ptr;
    unsigned long int longword, himagic, lomagic;

    /* Handle the first few characters by reading one character at a time.
       Do this until CHAR_PTR is aligned on a longword boundary. */
    for (char_ptr = str; ((unsigned long int)char_ptr & (sizeof(longword) - 1)) != 0; ++char_ptr)
        if (*char_ptr == '\0')
            return char_ptr - str;

    /* All these elucidatory comments refer to 4-byte longwords,
       but the theory applies equally well to 8-byte longwords. */
    longword_ptr = (unsigned long int *) char_ptr;

    /* Bits 31, 24, 16, and 8 of this number are zero.  Call these bits
    the "holes."  Note that there is a hole just to the left of
    each byte, with an extra at the end:
        bits:  01111110 11111110 11111110 11111111
        bytes: AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDD
    The 1-bits make sure that carries propagate to the next 0-bit.
    The 0-bits provide holes for carries to fall into. */
    himagic = 0x80808080L;
    lomagic = 0x01010101L;
    if (sizeof (longword) > 4) {
        /* 64-bit version of the magic. */
        /* Do the shift in two steps to avoid a warning if long has 32 bits. */
        himagic = ((himagic << 16) << 16) | himagic;
        lomagic = ((lomagic << 16) << 16) | lomagic;
    }
    if (sizeof (longword) > 8)
        abort();

    /* Instead of the traditional loop which tests each character,
       we will test a longword at a time.  The tricky part is testing
       if *any of the four* bytes in the longword in question are zero. */
    for (;;) {
        longword = *longword_ptr++;
        if (((longword - lomagic) & ~longword & himagic) != 0) {
            /* Which of the bytes was the zero?  If none of them were, it was
               a misfire; continue the search. */
            const char *cp = (const char *) (longword_ptr - 1);
            if (cp[0] == 0)
                return cp - str;
            if (cp[1] == 0)
                return cp - str + 1;
            if (cp[2] == 0)
                return cp - str + 2;
            if (cp[3] == 0)
                return cp - str + 3;
            if (sizeof (longword) > 4) {
                if (cp[4] == 0)
                    return cp - str + 4;
                if (cp[5] == 0)
                    return cp - str + 5;
                if (cp[6] == 0)
                    return cp - str + 6;
                if (cp[7] == 0)
                    return cp - str + 7;
            }
        }
    }
}
implementazione in musl libc
#define ALIGN (sizeof(size_t))
#define ONES ((size_t)-1/UCHAR_MAX)
#define HIGHS (ONES * (UCHAR_MAX/2+1))
#define HASZERO(x) ((x)-ONES & ~(x) & HIGHS)

size_t strlen(const char *s)
{
    const char *a = s;
    const size_t *w;

    for (; (uintptr_t)s % ALIGN; s++)
        if (!*s)
            return s-a;

    for (w = (const void *)s; !HASZERO(*w); w++)
        ;

    for (s = (const void *)w; *s; s++)
        ;

    return s-a;
}
Come avrete notato sono tutte abbastanza più complicate della versione K&R, perché, invece di effettuare un semplice test a 0 un byte alla volta, effettuano il test su 4 bytes alla volta (usando un algoritmo tipo quello descritto in ZeroInWord): evidentemente questo sistema moltiplica la velocità di ricerca del fine stringa, permettendo un notevole risparmio di tempo per stringhe molto grandi. I codici FreeBSD e glibc sono ben commentati (ho lasciato quasi tutti i commenti originali) e, a prima vista, si nota che la versione FreeBSD è un po' più compatta della versione glibc. La vera sorpresa è la versione della musl libc che è veramente geniale: non è molto più lunga della implementazione del K&R! Ricordatevi della musl (che ho gia citato qui a proposito dei C11 Threads): è veramente una notevole alternativa alla glibc per i sistemi Linux embedded, visto che compattezza e efficienza sono i suoi segni distintivi.

Per oggi può bastare, nella seconda parte del post vi proporrò un programma che ho scritto per eseguire un benchmark di un po' di implementazioni della strlen(): ci sarà quella di default del sistema (Linux, nel mio caso, e uno si aspetterebbe che usa la versione glibc, ma... sorpresa in arrivo!) e poi ci saranno, ovviamente, le quattro di questo post (K&R, FreeBSD, glibc e musl). Vi anticipo che i risultati del test sono sorprendenti e, per spiegarli, vi proporrò una sesta (ancora più sorprendente) versione della strlen() che dovrebbe risolvere il dubbio su cosa usa in realtà Linux quando chiamiamo la strlen(). E, anche se so che non vedete l'ora di sapere quale è la sesta versione, vi raccomando (come al solito) di non trattenere il respiro nell'attesa...

Ciao e al prossimo post!

venerdì 22 giugno 2018

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!
A Virgil Starkwell bastava una cravatta... a noi invece basta un bel makefile universale. Ricordate quel vecchio post in cui ve ne ho proposto uno? Non ve lo ricordate? Subito a rileggerlo e poi tornate qua.
...presto avremo un makefile...
Rieccoci (e, piccola premessa: alcuni punti di questo post sono parzialmente copiati dal mio vecchio post... posso copiare me stesso, no?). Se avete riletto il post sapete già che questo sarà un post veloce, e non propriamente sul C: ho pensato che era ora di scrivere e proporvi una versione estesa e perfezionata del vecchio makefile universale: rispetto all'originale questo usa maggiormente le variabili, così il codice è più pulito e leggibile (lo stile, prima di tutto!) e aggiunge una funzionalità molto interessante: la creazione di una shared library (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/share/pluto/lib
2. modificare (come vedremo tra poco) il makefile per generare la libreria 
   (che chiameremo "libmyutils.so") e copiarla in "/usr/share/pluto/lib".
3. aggiungere in "/etc/ld.so.conf.d" un nuovo file "libmyutils.conf" che 
   contiene le seguenti due linee (la prima è solo un commento): 
       # libmyutils.so default configuration
       /usr/share/pluto/lib
4. rendere disponibile la nuova shared-lib eseguendo: 
       sudo ldconfig
Proseguiamo: supponiamo di usare lo stesso progetto dell'altra volta (si chiamava pluto). I nostri file sono organizzati in una maniera canonica, questa volta in quattro directory (l'altra volta erano tre): pluto, lib, libmyutils e include. La nuova directory è libmyutils, e contiene i sorgenti della shared-lib che vogliamo creare. Nella directory pluto troviamo il main() e il makefile, nella directory lib troviamo gli altri sorgenti dell'applicazione pluto e, infine, nella directory include troviamo gli header-files comuni a main(), lib e libmyutils. Vediamo il nuovo makefile:
# variabili
CC = gcc
CPPFLAGS = -I../include -g -Wall -Wshadow -pedantic -std=c11
CPPFLAGS_UTI = -fpic -I../include -g -Wall -Wshadow -pedantic -std=c11
LDFLAGS = -lmyutils -std=c11
LDFLAGS_UTI = -shared -fpic -lcurl -std=c11

# sorgenti, oggetti e dipendenze
SRCS = $(wildcard *.c)
SRCS_LIB = $(wildcard ../lib/*.c)
SRCS_LIB_UTI = $(wildcard ../libmyutils/*.c)
OBJS = $(SRCS:.c=.o)
OBJS_LIB = $(SRCS_LIB:.c=.o)
OBJS_LIB_UTI = $(SRCS_LIB_UTI:.c=.ou)
DEPS = $(SRCS:.c=.d)
DEPS_LIB = $(SRCS_LIB:.c=.d)
DEPS_LIB_UTI = $(SRCS_LIB_UTI:.c=.d)

# tutti i target
all: libmyutils pluto

# creazione del target file eseguibile
pluto: $(OBJS) $(OBJS_LIB)
 $(CC) $^ -o $@ $(LDFLAGS) -L/usr/share/pluto/lib

# creazione della shared-lib libmyutils.so
libmyutils: $(OBJS_LIB_UTI)
 $(CC) $^ -o libmyutils.so $(LDFLAGS_UTI)
 mv libmyutils.so /usr/share/pluto/lib

# creazione degli object file (per la applicazione e la shared-lib)
%.o: %.c
 $(CC) -MMD -MP $(CPPFLAGS) -c $< -o $@

%.ou: %.c
 $(CC) -MMD -MP $(CPPFLAGS_UTI) -c $< -o $@

# direttive phony
.PHONY: clean

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

# creazione dipendenze
-include $(DEPS) $(DEPS_LIB) $(DEPS_LIB_UTI)
Come vedete 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ù?

Aggiungo qualche piccolo dettaglio sui blocchi (commentati) che compongono il makefile universale:

# variabili
Qua si mettono le variabili che vengono usate nel resto del makefile. Notare che in CPPFLAGS e LDFLAGS sono contenute tutte le opzioni di compilazione e link necessarie nelle fasi di creazione della applicazione, mentre che in CPPFLAGS_UTI e LDFLAGS_UTI sono contenute quelle relative alla shared-lib). Non mi dilungherò sul significato delle singole opzioni: magari potrebbero essere l'argomento di un prossimo post... comunque le opzioni che ho usato sono decisamente "universali". Se si usa qualche libreria esterna si può aggiungere qui: nell'esempio (ricordarsi: è solo un esempio!) ho scritto che pluto usa libmyutils (con il comando -lmyutils in LDFLAGS) e ho scritto che libmyutils usa la libreria open-source libcurl (con il comando -lcurl in LDFLAGS_UTI). Notare 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.

# sorgenti, oggetti e dipendenze
Qua ci sono le direttive che usa internamente il programma make per decidere come e dove cercare sorgenti, oggetti e dipendenze.

# tutti i target
Qua ci sono gli obiettivi di creazione: nel nostro caso la libreria libmyutils e l'applicazione pluto: il comando make "da solo" 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 file eseguibile
Qua si mette il comando per linkare i file oggetto creati e produrre il file eseguibile finale. Notare che con la direttiva -L/usr/share/pluto/lib 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 della shared-lib libmyutils.so
Qua ci sono le istruzioni per la creazione della shared-lib libmyutils.so e per spostarla (col comando "mv") nella directory destinazione.

# creazione degli object file (per la applicazione e la shared-lib)
Qua si mette il comando per compilare ogni sorgente e creare il file oggetto corrispondente, attivando (attraverso le variabili definite all'inizio) tutte le opzioni del compilatore che ci servono.

# 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.

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!