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.

mercoledì 25 settembre 2024

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

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

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

Questo è un post veloce. E non è neanche propriamente un post sul C. Il consiglio è di prendere l'informazione, scappare e conservarla gelosamente per il futuro, perché potrebbe tornare molto utile. E non fatevi prendere, se no potreste fare la fine di Virgil Starkwell, il protagonista del divertentissimo e bel mockumentary  Prendi i soldi e scappa del Maestro Woody Allen. Oggi parleremo di makefile!

...faccia da "ma ho solo rubato un makefile!"...

Allora, supponiamo che dobbiamo fare un progetto (che chiameremo, per esempio, pluto) e, per vari motivi, non vogliamo (siamo della vecchia scuola) o non possiamo (non ce n'è uno adatto) usare un IDE. Quindi organizziamo (a mano) i nostri file in una maniera canonica, in tre directory: pluto, lib e include. Ovviamente scriveremo il codice in C e piazziamo i file in maniera logica (evidentemente il file con il main va nella directory pluto). I file sono tanti e ogni volta che ricompiliamo non vogliamo riscrivere il tutto il comando e, soprattutto, vogliamo ricompilare solo quello che serve (solo i sorgenti modificati) soddisfacendo automaticamente anche le dipendenze dagli header  (ovvero: ricompilare solo i sorgenti che dipendono da un header modificato)... Ma ci serve un makefile!

Ok, tutti voi sapete già cosa è un makefile, ma... sapete scriverne uno veramente semplice e, allo stesso tempo, super-funzionale, generico e universale? Se la risposta è NO questo è l'articolo che fa per voi (e se la risposta è SI... Ciao e al prossimo post!).

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

# variabili
SRCS = $(wildcard *.c)
SRCS_LIB = $(wildcard ../lib/*.c)
OBJS = $(SRCS:.c=.o)
OBJS_LIB = $(SRCS_LIB:.c=.o)
DEPS = $(SRCS:.c=.d)
DEPS_LIB = $(SRCS_LIB:.c=.d)

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

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

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

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

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

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

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

# direttive phony
.PHONY: clean

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

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

Come vedete il makefile presentato è veramente semplice. Però è anche molto completo: fa tutto quello che serve, compresa la generazione dei file di dipendenza dagli header, e possiamo usarlo per qualsiasi progetto, indipendentemente dal numero di file (le directory pluto lib e include potrebbero essere vuote oppure contenere centinaia di file). Possiamo aggiungere e togliere sorgenti e header  e ricompilare senza modificare una sola linea del makefile, perché lui si adatta automaticamente a quello che trova nelle tre directory del progetto: cosa vogliamo di più?

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

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

Che ne dite? L'obbiettivo non era di spiegare cosa è un makefile e come si scrive (uff, c'è in rete una documentazione enorme sull'argomento). E neppure era di spiegare i segreti della sintassi (che permette anche soluzioni complesse). L'obbiettivo era di fornire un makefile basico e completo allo stesso tempo, un makefile universale per (quasi) qualsiasi progetto. Io direi che l'obbiettivo è compiuto... poi, se dobbiamo fare progetti complessi e portabili, con auto-installatori, ecc. magari ci troveremo più comodi usando un IDE di buona qualità oppure usando a mano strumenti come Autotools CMake... ma vi assicuro che il metodo rapido e vecchia-scuola che ho descritto è usabile sempre e senza limitazioni (io l'ho usato in progetti di produzione, giuro!). Sono soddisfazioni...

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

Ciao e al prossimo post!

martedì 27 agosto 2024

No Comment
come e perché scrivere i commenti nel C

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

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

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

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

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

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

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

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

Caso 1: codice Absolute No Comment

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

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

Caso 2: codice auto-commentante

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

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

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

p_deviceXYZ->message_to_send[0] = 0;
}

return number_sent;
}

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

Caso 3: codice chiaro e commentato

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

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

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

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

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

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

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

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

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

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

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

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

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

Ciao e al prossimo post! 

giovedì 11 luglio 2024

Furiosa Go
come spedire una struttura in Go - pt.2

Smeg: Chi sono?
Dementus: Qualcuno competente ed eccessivamente risentito.
Smeg: Cosa pensi che vogliano?
Dementus: Me, senza il mio equipaggio.

Ed eccoci di nuovo sul pezzo. E, visto che si tratta della seconda parte dell'articolo Fury Go, non posso non agganciarmi allo splendido prequel di Mad Max: Fury Road, e cioè a Furiosa: A Mad Max Saga diretto sempre dal Maestro George Miller. E, in effetti, anche questo articolo è un prequel: nell'altro avevo descritto come spedire una struttura in Go via IPC socket (UNIX domain socket), e il Software l'avevo scritto partendo da una mia versione "base" che inviava solo dei semplici testi. Ed ecco, in questa seconda parte vi mostrerò, come prequel, la versione base, dimostrandovi, come promesso, che le prestazioni buone ma non eccellenti del benchmark erano dovute (spoiler) solo alla codifica della struttura, perché con i soli testi il Go va come un treno!

...adesso vi faccio vedere cosa sa fare il Go...

Ok, e allora andiamo con il prequel, descrivendo la versione base che ho scritto prima di quella che usa le strutture. Siamo pronti? Si? E allora partiamo direttamente con il codice!

// reader.go - main processo figlio: è un reader (un server) su IPC socket
package main

import (
"bufio"
"fmt"
"net"
"os"
"time"
)

// funzione main
func main() {

// start ascolto sul file di scambio "myipcs" (con UNIX domain socket)
fmt.Printf("processo %d partito (reader)\n", os.Getpid())
addr := net.UnixAddr{Name: "./myipcs", Net: "unix"}
lner, err := net.ListenUnix("unix", &addr)
if err != nil {
// errore listen
fmt.Println(err)
return
}

// prenoto la chiusura del listener e rimuovo (eventualmente) il file di scambio
defer lner.Close()
defer os.Remove("./myipcs")

// accetta connessioni da un writer entrante
conn, err := lner.AcceptUnix()
if err != nil {
// errore accept
fmt.Println(err)
return
}

// set time di partenza per calcolare il tempo impiegato
start := time.Now()

// loop di lettura messaggi dal writer
n_msg := 0
connrdr := bufio.NewReader(conn) // reader sulla connessione
for {
// leggo con il conn reader
client_msg, err := connrdr.ReadString('\n')
if err != nil {
// errore di lettura
fmt.Println(err)
return
}

// test numero messaggi per forzare l'uscita
n_msg++
if n_msg == 2000000 {
// il processo chiude la connessione ed esce per numero raggiunto
fmt.Printf("reader: ultimo messaggio ricevuto: %s", client_msg)
fmt.Printf("reader: processo %d terminato (messaggi=%d tempo totale:%s)\n",
os.Getpid(), n_msg,
time.Since(start).Truncate(time.Millisecond).String())
conn.Close()
return
}
}
}
// writer.go - main processo figlio: è un writer (un client) su IPC socket
package main

import (
"fmt"
"net"
"os"
"time"
)

// funzione main
func main() {

// mi assicuro che il writer parta dopo il reader
fmt.Printf("processo %d partito (writer)\n", os.Getpid())
time.Sleep(100 * time.Millisecond)

// connessione al server remoto sul file di scambio "myipcs"
addr := net.UnixAddr{Name: "./myipcs", Net: "unix"}
conn, err := net.DialUnix("unix", nil, &addr)
if err != nil {
// errore dial
fmt.Println(err)
return
}

// loop di scrittura messaggi per il reader
var my_text string
index := 0
for {
// test index per forzare l'uscita
if index == 2000000 {
// il processo chiude la connessione ed esce per indice raggiunto
fmt.Printf("writer: processo %d terminato (text=%s messaggi=%d)\n",
os.Getpid(), my_text, index)
conn.Close()
return
}

// compongo il messaggio e lo invio
index++
my_text = fmt.Sprintf("un-messaggio-di-test:%d\n", index)

// invio il messaggio al server remoto
_, err = conn.Write([]byte(my_text))
if err != nil {
fmt.Println("errore di invio: ", err)
return
}
}
}

Come avrete notato dalla descrizione nella prima linea (e anche dal codice, spero!) ho usato anche questa volta gli IPC socket. Poi ho anche scritto la versione con i Network Socket, ma non vi mostrerò il codice perché è quasi identico. Effettivamente, per la magia del Go, il codice è semplicissimo rispetto alla analoga versione in C vista qui (in questo caso era la versione "fast"), ed è anche quasi identico alla versione con le strutture dello scorso articolo. Come sempre il codice è stra-commentato, e credo che possa essere facilmente compreso anche da chi non conosce il Go, però mi interessa, a questo punto, aggiungere qualche dettaglio per far notare le (poche) differenze rispetto a quello dello scorso articolo.

Cominciamo, allora, con il writer: in entrambe versioni si crea, inizialmente, un oggetto "connessione" conn:

// connessione al server remoto sul file di scambio "myipcs"
addr := net.UnixAddr{Name: "./myipcs", Net: "unix"}
conn, err := net.DialUnix("unix", nil, &addr)

Poi, nella versione con le strutture, si invia il messaggio attraverso il codec encoding/gob passandogli la connessione:

// set encoder e spedizione dall'encoder
encoder := gob.NewEncoder(conn)
err = encoder.Encode(message)

Invece, nella versione base si scrive, direttamente, con l'oggetto connessione creato all'inizio:

// invio il messaggio al server remoto
_, err = conn.Write([]byte(my_text))

E ora passiamo al reader: anche qui, in entrambe versioni, si crea un oggetto "connessione" conn:

// accetta connessioni da un writer entrante
conn, err := lner.AcceptUnix()

Poi, nella versione con le strutture, si riceve il messaggio attraverso il codec passandogli la connessione:

// set decoder e ricezione dal decoder
decoder := gob.NewDecoder(conn)
decoder.Decode(&message)

Invece, nella versione base, si legge con un oggetto "Reader" (della libreria bufio) creato sulla connessione vista sopra:

connrdr := bufio.NewReader(conn) // reader sulla connessione
for {
// leggo con il conn reader
client_msg, err := connrdr.ReadString('\n')

Come avrete notato le differenze sono poche ma significative. Notare anche che nella versione con le strutture il testo del messaggio è una stringa dentro la struttura Message, mentre nella versione base il messaggio è direttamente una stringa terminata con un "newline": questo è un particolare importante, perché in fase di ricezione con la funzione ReadString appena vista è necessario specificare qual'è il terminatore di stringa.

E vabbé, so che siete curiosi, è ora di passare ai risultati! Di seguito i risultati dei benchmark delle "versioni base" che inviano solo testi in Go:

aldo@Linux $ cd ../go-ipcsocketbase/
aldo@Linux $ ./processes
sono il padre (15381): attendo la terminazione dei figli
sono il figlio 1 (15382): eseguo il nuovo processo
sono il figlio 2 (15383): eseguo il nuovo processo
processo 15382 partito (reader)
processo 15383 partito (writer)
writer: processo 15383 terminato (text=un-messaggio-di-test:2000000
messaggi=2000000)
reader: ultimo messaggio ricevuto: un-messaggio-di-test:2000000
reader: processo 15382 terminato (messaggi=2000000 tempo totale:3.058s)
sono il padre (15381): figlio 15382 terminato (0)
sono il padre (15381): figlio 15383 terminato (0)
./processes: processi terminati
aldo@Linux $ cd ../go-socketbase/
aldo@Linux $ ./processes
sono il padre (15408): attendo la terminazione dei figli
sono il figlio 1 (15409): eseguo il nuovo processo
sono il figlio 2 (15410): eseguo il nuovo processo
processo 15410 partito (writer)
processo 15409 partito (reader)
writer: processo 15410 terminato (text=un-messaggio-di-test:2000000
messaggi=2000000)
reader: ultimo messaggio ricevuto: un-messaggio-di-test:2000000
reader: processo 15409 terminato (messaggi=2000000 tempo totale:8.46s)
sono il padre (15408): figlio 15409 terminato (0)
sono il padre (15408): figlio 15410 terminato (0)
./processes: processi terminati

A questo punto i risultati parlano senza temi di smentite: i tempi realizzati sono decisamente migliori di quelli mostrati per le versioni che spedivano strutture complesse, quindi è evidente che la maggior parte del tempo di CPU se la mangiava la libreria specializzata encoding/gob, che funziona bene però, a quanto pare, non è un fulmine. E, grazie ai test appena mostrati sopra si può affermare (come anticipato nella prima parte dell'articolo) che il Go è un linguaggio notevolmente veloce, alla faccia di chi pensa il contrario... Notare che la versione IPC con i suoi 3.058s è addirittura veloce come la versione in C (che impiegava 3.309s ma con messaggi leggermente più lunghi a causa della presenza dell'indice)! Anche in questo caso (come già nello scorso articolo) la versione con i Network socket è un po' più lenta (8.46s) ma è, comunque, sufficientemente veloce.

Ok, credo che, per il momento si può chiudere la parentesi Go sulla comunicazione tra processi: credo che i risultati siano stati interessanti, specialmente per il fatto di avere confrontato codici analoghi per C e Go. Non so di cosa parlerò` prossimamente: in questo momento ho in mente solo le prossime (meritate) vacanze. Ci sentiremo più avanti, ben rilassati e pronti per nuove avventure in C (o in Go...)!

Ciao, e al prossimo post!