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

venerdì 18 maggio 2018

Il grande lighttpd
come scrivere un modulo lighttpd in C - pt.4

Maude: E di cosa ti occupi nel tempo libero?
Drugo: Mah, le solite cose: Bowling, un giro in macchina, un trip d'acido quando capita...
Sottinteso che per scrivere del buon Software non c'è bisogno di imitare le abitudini del mitico Drugo del Grande Lebowski, ritorniamo sull'argomento lighttpd, perchè nel terzo post della serie (qui, qui e ancora qui) avevo promesso di ampliare il discorso. Ebbene è venuto il momento, le promesse si mantengono!
...Mah, le solite cose: Bowling, un giro in macchina, un modulo lighttpd quando capita...
Bene, ne "Il grande lighttpd - pt.3" (che, avete appena riletto, immagino...) avevamo scritto un bel modulo elementare per lighttpd. Faceva poco (era un classico "Helloworld": scriveva solo una presentazione nel browser) però era un buon inizio per entrare nel mondo dei moduli di questo notevole Web Server. Ecco, adesso riprenderemo la base fatta a suo tempo e gli aggiungeremo una funzione che estrae i dati POST inclusi in una eventuale petizione HTTP di tipo POST che arriva al modulo. Perché ho scelto di aggiungere proprio questa funzionalità? Beh, ovviamente perché è abbastanza normale che un modulo tratti vari tipi di petizioni (GET, POST, ecc.), e poi, in particolare, ricordo che la prima volta che aggiunsi questa funzionalità in un modulo Apache che avevo scritto, notai che non erano disponibili molte indicazioni su questo argomento (e questo nonostante la enorme mole di documentazione su Apache disponibile rispetto a quella su lighttpd)... vi lascio immaginare, quindi, quanta documentazione e quanti esempi ho trovato per fare la stessa cosa per lighttpd: praticamente niente.

Bene, senza stare a ripetere tutto il codice e la maniera di generarlo, riscriveremo solo la funzione mod_helloworld_uri_handler() e aggiungeremo una nuova funzione getPost(). Premetto che, per integrare il codice che ho scritto, è necessario seguire la guida di installazione indicata in "Il grande lighttpd - pt.1" e ripetere tutti i 10 passi indicati in "Il grande lighttpd - pt.2", usando questa volta la versione 1.4.45 di lighttpd (occhio: se usate una versione più vecchia il codice che sto per mostrarvi non funziona). Fatto? Allora vai col codice!
URIHANDLER_FUNC(
    mod_helloworld_uri_handler)
{
    plugin_data *p = p_d;

    UNUSED(srv);

    // test modo (return se errore)
    if (con->mode != DIRECT)
        return HANDLER_GO_ON;

    // test uri path (return se errore)
    size_t s_len = buffer_string_length(con->uri.path);
    if (s_len == 0)
        return HANDLER_GO_ON;

    mod_helloworld_patch_connection(srv, con, p);

    // test handler (return se errore)
    if (con->uri.path->ptr && strstr(con->uri.path->ptr, "helloworld")) {
        // prepara il buffer per la risposta
        buffer *buf = buffer_init();

        // test metodo http
        if (strstr(con->request.request->ptr, "GET")) {
            // metodo GET: scrive risposta
            buffer_append_string(buf, "<big>Hello, world!</big>");
        }
        else if (strstr(con->request.request->ptr, "POST")) {
            // metodo POST: controlla la presenza di post-data
            size_t len = con->request.content_length;
            if (len) {
                // post-data presenti; li legge per scrivere la risposta
                char *data = malloc(len + 1);
                if (readPost(con, data, len)) {
                    // scrive la risposta
                    buffer_append_string(buf, data);
                }

                // libera la memoria
                free(data);
            }
            else {
                // errore: messaggio POST senza post-data
                buffer_append_string(buf, "mod_helloworld: errore: POST senza post-data");
            }
        }
        else {
            // errore: messaggio con metodo non trattato
            buffer_append_string(buf, "mod_helloworld errore: metodo non trattato");
        }

        // scrive il buffer e lo libera
        chunkqueue_append_buffer(con->write_queue, buf);
        buffer_free(buf);

        // spedisce l'header
        response_header_overwrite(
                srv, con, CONST_STR_LEN("Content-Type"), CONST_STR_LEN("text/html"));
        con->http_status = 200;
        con->file_finished = 1;

        // handling finito
        return HANDLER_FINISHED;
    }

    // handler non trovato
    return HANDLER_GO_ON;
}

static bool readPost(
    connection *con,                // dati connessione
    char       *data,               // buffer destinazione per post-data
    size_t     len)                 // lunghezza data (senza terminatore)
{
    // set valore di return di default
    int retval = false;

    // legge post-data (nello stream di chunks)
    size_t rpos = 0;
    chunkqueue *cq = con->read_queue;
    for (chunk *mychunk = cq->first; mychunk; mychunk = cq->first) {
        // calcola il size del buffer corrispondente al chunk di post-data corrente
        size_t n_tocopy = buffer_string_length(mychunk->mem) - mychunk->offset;

        // test se ci sono dati da copiare
        if (n_tocopy <= (len - rpos)) {
            // copia un chunk e set della posizione di copia del prossimo chunk
            memcpy(data + rpos, mychunk->mem->ptr + mychunk->offset, n_tocopy);
            rpos += n_tocopy;

            // é stato letto (almeno) un chunk di post-data: set retval=true
            retval = true;
        }
        else {
            // buffer overflow: forzo uscita dal loop
            break;
        }

        // segnalo come letto il chunk di post-data corrente
        chunkqueue_mark_written(cq, chunkqueue_length(cq));
    }

    // aggiungo il terminatore di stringa (il buffer é lungo len+1)
    data[len] = 0;

    // esco con retval
    return retval;
}
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.

L'originale funzione mod_helloworld_uri_handler() è ancora una sorta di funzione "Helloworld", ma ora distingue i tipi di petizione e, nel caso POST, chiama la funzione readPost() e scrive come risposta "Hello, world!" più i dati POST. Nel caso di petizioni GET si limita a scrivere "Hello, world!" e, con altri tipi di petizione o con petizioni POST senza dati, scrive un messaggio di un errore.

La funzione readPost() è semplice ma non immediata: vista la scarsezza quasi totale di esempi disponibili in rete ho dovuto, praticamente, fare reverse engineering sugli (ottimi) moduli integrati nella distribuzione (avere il codice sorgente originale a disposizione è sempre una cosa ottima!) in maniera di dedurre come fare quello che mi ero prefisso. Il risultato finale è (modestia a parte) buono, sia come aspetto (stile) sia come prestazioni (ho fatto dei test e funziona bene).

Ah, il test, dimenticavo! Testare con petizioni POST non è semplice come farlo con le GET, dove è sufficiente (come detto nel post precedente) scrivere la URI del modulo nella barra degli indirizzi di Firefox o Chrome (o qualsiasi altro browser meno IE, per favore...) e aspettare la risposta. Per testare il nostro nuovo modulo è meglio usare un bel plugin del browser (tipo Poster) per semplificare il lavoro.

Va bene, per il momento credo che possiamo fermare per un tempo l'argomento moduli lighttpd. Giuro che nel prossimo post parlerò d'altro, non vorrei che pensaste che il C si usa solo per i Web Servers...

Ciao e al prossimo post!