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ì 13 marzo 2019

1997: Fuga da Bitwise
Come funzionano le operazioni di Bitwise in C

(...una premessa: questo post è un remake di un mio vecchio post (anzi, di due). L'ho riadattato e "modernizzato" per pubblicarlo su quell'altro bel blog collettivo dove scrivo. Visto che le modifiche sono molte e (forse) interessanti, lo ripubblico anche qui. Questo potrebbe ripetersi in futuro...)
Bob Hauk: Mi ucciderai ora, Jena?
Jena (Snake) Plissken: Sono troppo stanco... Forse più tardi...
Nel capolavoro 1997: Fuga da New York il grande John Carpenter ci mostrava come scappare da una tristissima New York trasformata in prigione federale. Ecco, l’argomento di questo articolo è uno di quegli argomenti che inducono alla fuga molti valorosi Programmatori C/C++… “ehm… oggi non ho tempo, puoi occupartene tu delle operazioni di bitwise? Poi domani mi farai vedere…
...a me Bitwise non me l'ha mai detto nessuno...
Cominciamo con un piccolo gioco: alzi la mano chi ha scritto recentemente codice che usa le operazioni bit a bit. Oppure, alzi la mano, senza prima andare a rileggersi un manuale del C/C++, chi di voi sa usare e/o descrivere perfettamente le operazioni bit a bit. Uhm… vedo poche mani alzate. Il fatto è che le bitwise operations sono una di quelle parti del C/C++ un po’ misconosciute, di uso dubbio e infrequente, insomma una di quelle parti di cui, per mancanza di pratica ci si scorda.

E, oltretutto, devo dire che sulle operazioni di bitwise il fantastico K&R non è particolarmente chiaro e dettagliato, le tratta (come sono) come un argomento di nicchia, e non ti coinvolge con decine di divertenti esempi che ti aiutano a memorizzare definitivamente l’argomento, anzi, per quel che ricordo dell’ultima volta che lo lessi, sono un paio di pagine che scivolano via e che hai già dimenticato quando passi al prossimo capitolo. Beh anche la bibbia K&R (che io considero sacra) ha alcuni punti non proprio coinvolgenti.

Rinfreschiamo: gli operatori di bitwise (che operano sui singoli bit) sono:
"&"  AND
"|"  OR
"^"  XOR
"~"  NOT (complemento a 1)
"<<" SHIFT a sinistra
">>" SHIFT a destra  
     
N.B.:
- il NOT e' un operatore unario: opera su un solo argomento indicato sulla destra.
- gli shift sono operatori unari: operano su un solo argomento indicato sulla sinistra.
Adesso, senza dilungarci in noiosi sproloqui, passiamo a una piccola tabella e ad alcuni semplici esempi pratici. Ecco la tabella:
"&": il risultato è 1 se i due operandi valgono 1. Altrimenti 0.
"|": il risultato è 0 se i due operandi valgono 0. Altrimenti 1.
"^": il risultato è 1 se i due operandi sono diversi. Altrimenti 0.
"~": il risultato è 1 se l'operando vale 0. Se l'operando vale 1 il risultato è 0.
"<< n": il risultato è l'operando con tutti i bit spostati a sinistra di n posizioni. 
">> n": il risultato è l'operando con tutti i bit spostati a destra di n posizioni.
Ed ecco i semplici esempi pratici:
AND
int a = 74;       // 0 1 0 0 1 0 1 0
int b = 174;      // 1 0 1 0 1 1 1 0
int c = a & b;    // 0 0 0 0 1 0 1 0 risultato c=10

OR
int a = 74;       // 0 1 0 0 1 0 1 0
int b = 174;      // 1 0 1 0 1 1 1 0
int c = a | b;    // 1 1 1 0 1 1 1 0 risultato c=238

XOR
int a = 74;       // 0 1 0 0 1 0 1 0
int b = 174;      // 1 0 1 0 1 1 1 0
int c = a ^ b;    // 1 1 1 0 0 1 0 0 risultato c=228

NOT
int a = 74;       // 0 1 0 0 1 0 1 0
int b = ~a;       // 1 0 1 1 0 1 0 1 risultato b=181

SHIFT a sinistra
int a = 74;       // 0 1 0 0 1 0 1 0
int b = a << 2;   // 0 0 1 0 1 0 0 0 risultato b=296

SHIFT a destra
int a = 74;       // 0 1 0 0 1 0 1 0
int b = a >> 2;   // 0 0 0 1 0 0 1 0 risultato b=18
Notare che nelle operazioni di shift i bit nuovi che entrano a destra (nello shift a sinistra) valgono 0, e i bit nuovi che entrano a sinistra (nello shift a destra) valgono 0.
Notare anche che lo shift a destra equivale a una divisione per multipli di 2 (>>1  è una divisione per 2,  >>2  è una divisione per 4, ecc.), mentre lo shift a sinistra equivale a una moltiplicazione per multipli di 2 (<<1  è una moltiplicazione per 2, <<2  è una moltiplicazione per 4, ecc.). Queste operazioni di moltiplicazione e divisione sono molto veloci, e si potrebbe essere tentati a usarle per velocizzare il codice: beh, prima di farlo rileggetevi (o leggetevi) questo.

E aggiungo un avvertimento: in base alla dimensione del tipo del operando e alla presenza o meno del bit di segno, le moltiplicazioni e divisioni con shift possono dare risultati inaspettati. Anzi, andiamoci subito alle dolenti note,  togliamoci il pensiero: vediamo un esempio:
#include <stdio.h>

void main()
{
    // 1) SHIFT a sinistra con unsigned int
    unsigned int a, b;
    a = 74;           // 0 0 1 0 0 1 0 1 0
    b = a << 2;       // 1 0 0 1 0 1 0 0 0 risultato b=296
    printf("var = %d; var << 2 = %d\n", a, b);

    // 2) SHIFT a sinistra con unsigned char
    unsigned char c, d;
    c = 74;           // 0 1 0 0 1 0 1 0
    d = c << 2;       // 0 0 1 0 1 0 0 0 risultato d=40
    printf("var = %d; var << 2 = %d\n", c, d);

    // 3) SHIFT a destra con signed char (o int)
    char e, f;
    e = -4;           // 1 1 1 1 1 1 0 0
    f = e >> 2;       // 1 1 1 1 1 1 1 1 risultato f=-1
    printf("var = %d: var >> 2 = %d\n", e, f);
    // N.B.: senza estensione di segno sarebbe:
    // e = -4;        // 1 1 1 1 1 1 0 0
    // f = e >> 2;    // 0 0 1 1 1 1 1 1 risultato f=63
}
Il caso 1 è, evidentemente il caso funzionante: un int è molto più grande degli 8 bit usati per rappresentare il numero di partenza (74), per cui lo shift non perde nessun 1 sulla sinistra (è questo il possibile problema) e il risultato è corretto (74×4=296). Se usiamo però (caso 2) un char (8 bit) durante lo shift perdiamo un 1 e il risultato va in overflow (74×4=40 ??). Quindi attenzione!

Il caso 3, poi, è ancora più subdolo: facendo operazioni con segno (nei casi 1 e 2 ho usato variabili unsigned proprio in preparazione al punto 3) e usando valori negativi, possono succedere cose strane: per la rappresentazione stessa dei numeri negativi in binario (complemento a 2) il bit più a sinistra (MSB) è il bit di segno, e, in questo caso l’operazione di shift è machine-dependent: in base al tipo di CPU possiamo disporre o no dell’estensione di segno (di default, ad esempio, su macchine Intel), per cui lo shift dell’esempio può dare il risultato aspettato (-4/4=-1) o un risultato completamente diverso (-4/4=63 ??). Di nuovo: attenzione!

E adesso è il momento di mostrare alcuni esempi pratici di uso di quanto esposto, che altrimenti, sarebbe know-how  fine a se stesso: vediamo come leggere lo stato dei singoli bit di una word usando una maschera:
#include <stdio.h>

void main()
{
    // uso di una maschera per leggere i bit di una word
    unsigned char mask = 1;     // 0 0 0 0 0 0 0 1
    unsigned char word = 74;    // 0 1 0 0 1 0 1 0

    // loop di lettura
    int i;
    for (i = 0; i < 8; i++)
        printf("il bit %d della word è %s\n", i, (word & mask<<i) ? "ON" : "OFF");
}
semplice no? E la stessa operazione si può fare con una macro:
#include <stdio.h>

#define INPUT(w, i)    (w & 0x01<<i)

void main()
{
    // uso di una macro per leggere i bit di una word
    unsigned char i_word = 74;    // 0 1 0 0 1 0 1 0

    // loop di lettura
    int i;
    for (i = 0; i < 8; i++)
        printf("il bit %d della word è %s\n", i, INPUT(i_word, i) ? "ON" : "OFF");
}
E poi, visto che lo stile è sempre molto importante, vediamo un modo con una una buona estetica per leggere degli input di un dispositivo, per esempio i fine corsa di un sistema elettromeccanico che dobbiamo controllare col nostro amato C:
#include <stdio.h>

#define INPUT_FC1    (in_word & 0x01<<0)
#define INPUT_FC2    (in_word & 0x01<<1)

void main()
{
    // uso di una define per ogni bit da leggere di una word
    unsigned char in_word = 74;    // 0 1 0 0 1 0 1 0

    // lettura
    printf("il bit FC1 della word è %s\n", INPUT_FC1 ? "ON" : "OFF");
    printf("il bit FC2 della word è %s\n", INPUT_FC2 ? "ON" : "OFF");
}
Ecco, l’esempio appena mostrato indica una maniera, semplice ed elegante, per descrivere degli input (usando dei mnemonici auto-esplicativi) che può risultare utile per scrivere del Software di controllo di dispositivi Hardware, facile da leggere e da manutenere.

Facile da leggere e da manutenere: questa l’ho già sentita… ah si: è come dovrebbe essere tutto il S/W che scrive un Analista Programmatore (…ma questa è un’altra storia…).

Ciao e al prossimo post!

domenica 17 febbraio 2019

Variabili Globali? No, grazie!
come NON usare le variabili globali nel C

(...una premessa: questo post è un remake di un mio vecchio post. L'ho riadattato e "modernizzato" per pubblicarlo su quell'altro bel blog collettivo dove scrivo. Visto che le modifiche sono molte e (forse) interessanti, lo ripubblico anche qui. Questo potrebbe ripetersi in futuro...)
"L'unica sua dote era una specie di futile entusiasmo. In pratica sembrava che usasse l'archetto come una sega, straziando le corde al punto da condurre l'ascoltatore verso pericolosi stati di follia. Non aveva cognizione della natura dello strumento: provava a soffiarci dentro." (Mr. A.Torgman) [maestro di violoncello di Virgil Starkwell, intervistato].
In Prendi i soldi e scappa il maestro di violoncello di Virgil Starkwell ci descriveva come si sentivano gli sfortunati ascoltatori di Virgil (Woody Allen) durante le sue esibizioni. Ecco, io ho esattamente le stesse sensazioni quando leggo del codice che usa uno dei capisaldi della non-programmazione: le variabili globali. Intendiamoci: le variabili globali (come anche il goto, per esempio) fanno parte del linguaggio, quindi esistono: a volte è possibile e/o necessario usarle. Però, esattamente come il goto, è quasi sempre possibile farne a meno, con grandi benefici di stile (leggibilità e manutenibilità, soprattutto), e funzionalità (meno bugs): chiamalo poco.
...la variabile globale: lo strumento sbagliato nel posto sbagliato...
I punti critici sono moltissimi, ma, visto che non voglio scrivere né un poema, né un libro sull’argomento, ne ho isolati alcuni. Vediamo:

1) le variabili globali non sono thread-safe

Nella programmazione multithreading l'uso delle globali può generare problemi di malfunzionamento subdoli e difficili da trovare. Il caso classico sono le istruzioni di lettura/scrittura di una globale fuori da una zona critica, ossia (per esempio) senza la protezione di un mutex: in un grande progetto è sufficiente una (una sola!) dimenticanza, di questo tipo per creare tanti di quei mal di testa che ti passerà la voglia di usare le globali per il resto della tua vita di programmatore. Provare per credere. A questo punto uno potrebbe obbiettare: "ma io scrivo programmi normali, non multithreading". Va bene: premettendo (e chiarendo nel prossimo paragrafo) che un programma "normale" può essere considerato un programma multi-thread con un solo thread, ringrazio per l'obiezione, che mi serve giusto di spunto per illustrare il punto 2:

2) le variabili globali violano il principio di manutenzione/riutilizzazione del Software

Quando si scrive del codice professionalmente bisogna sempre pensare al lavoro in team, e quindi alle operazioni di manutenzione che potrebbero essere svolte da altri (e, a volte) dopo molto tempo: evidentemente un codice, vasto e pieno di globali, è difficile da manutenere come un codice No Comment (ricordate ?), perché la storia di una globale è poco comprensibile, potrebbe essere toccata in molti posti diversi da molte funzioni diverse, e, se per capire un pezzetto di codice bisogna aprire alcune decine di file... avete vinto un altro bel mal di testa! E non ne parliamo di riutilizzare del codice pieno di globali per un altro progetto: se non l'avete mai fatto provate almeno a immaginarvi la difficoltà. E non solo: torniamo al punto 1 (thread-safe): chi mi dice che del codice "normale" non lo debba riutilizzare (un giorno) in un progetto multithread ? Se il codice è thread-safe si può fare agevolmente, ma se ci sono delle globali di mezzo... beh, buon lavoro (e buona fortuna).

3) le variabili globali aumentano la difficoltà del debug e moltiplicano la probabilità di errori di programmazione

Per quanto riguarda il debug vi rimando al punto 2: se il valore di una variabile è difficile da seguire a livello di manutenzione lo sarà anche a livello di debug. E ci saranno anche più malfunzionamenti da debuggare (fantastico!), perché, oltre a tutti i possibili errori di codificazione ci aggiungiamo anche quelli di scope: provate questo codice:
int my_var = 0;   // globale!

// funzione che incrementa la globale
void incrementaMyVar()
{
    my_var += 5;
}

// funzione main
int main(int argc, char **argv)
{
    // faccio mille cose...
    // ...

    // incremento my_var
    incrementaMyVar();

    // faccio altre mille cose...
    // ...

    // definisco una "nuova" variabile my var e la uso
    int my_var = 2;     // ah, ah, ah: ridefinizione!
    // ...

    // faccio ancora mille cose...
    // ...

    // incremento e test my_var (oops! quale my_var?)
    incrementaMyVar();
    if (my_var == 2)
        formatMyHardDisk(); // uh, uh, uh: era quella sbagliata!
    // ...

    return 0;
}
Quello mostrato sopra era un problema di scope con ridefinizione locale (accidentale) di una globale. Il codice che segue è ancora più semplice, mostra una svista su un dettaglio importante:
int my_var = 0;    // globale!

// funzione lunghissima che fa un sacco di cose
void faccioMilleCose()
{
    // faccio mille cose...
    // ...

    // incremento my_var (sepolto tra mille istruzioni!)
    my_var += 5;

    // faccio altre mille cose...
    // ...
}

// funzione main
int main(int argc, char **argv)
{
    // faccio un po' di cose...
    // ...

    // chiamo faccioMilleCose()
    faccioMilleCose();  // oops! ho incrementato my_var senza accorgermene

    // faccio altre cose...
    // ...

    // test my_var
    if (my_var == 5)
        formatMyHardDisk(); // uh, uh, uh: ho sbagliato qualcosa?
    // ...

    return 0;
}
Bella storia, eh ?

4) le variabili globali violano il principio di incapsulamento delle variabili

Beh, questo non c’è nemmeno bisogno di spiegarlo, una globale è tutto meno che incapsulata… oops, ma questo è OOP, quindi esula un po’ l’argomento del post… ah, no: questo articolo vale sia per C che per C++ (ed anche altri linguaggi, direi…). Beh, visto che siamo in argomento OOP, e quindi C++, cito volentieri il grande M.Cline che nelle su C++FAQ dice (traduco, eh):
I nomi delle variabili globali dovrebbero iniziare con //. 
Ecco il modo ideale per dichiarare una variabile globale:  

// int xyz; <-la cosa che rende ideale questa globale è l'iniziale // 

Ecco il modo ideale per utilizzare una variabile globale:

void mycode() 
{ 
    ... 
    // fai_qualcosa_con(xyz); <-idem come sopra 
    ... 
} 

Ok, questo è un gioco. Una specie. La verità è che ci sono casi in cui le 
variabili globali sono meno peggio delle alternative - quando le globali 
sono il minore dei mali. Ma loro sono sempre malvagie. Quindi lavatevi le 
mani dopo averle usate. Due volte.
(Marshall Cline C++FAQ sezione 27.15)

Sante parole.

E, per restare in tema C++, aggiungo un piccolo appunto: una Duna, anche se ci attacchi un logo Ferrari resta, ahimè, una Duna (e non me ne vogliano i lettori che usano la Duna, eh!). Allo stesso modo se, per darti delle arie, chiami Singleton una Variabile Globale Sofisticata il risultato non cambia, puoi sofisticarla quanto vuoi (inizializzazione “lazy”, inizializzazione “eager”, inizializzazione “oggi-sono-indeciso-e-faccio-un-mix-di-lazy-e-eager”, ecc., ecc.) ma, sotto sotto, è sempre una schifezza di Variabile Globale (anche se un pelino più accettabile, devo ammetterlo).

(…e qua bisogna giocare d’anticipo:  so che la affermazione qui sopra avrà fatto saltare sulla sedia tutti gli adepti della religione di Design Patterns, si, quelli che la sera recitano le orazioni davanti alla foto della Gang of Four che tengono sul comodino. Ecco, ho il massimo rispetto per loro, per la Gang of Four e per il libro che ha, sicuramente, un grande valore tecnico (e chi sono io per negarlo?) ma non è da prendere tutto come oro colato…  E vi assicuro che molta gente ha scritto del buon Software anche prima dell’uscita di quel libro (ma come avranno fatto?). Ecco, quel libro è l’equivalente informatico de La corazzata Potëmkin del Cinema, bello fin che vuoi (è un capolavoro) ma ti induce facilmente valutazioni come quella che fece Fantozzi. Anzi, posso affermare che, ultimamente, considero che il vero grande valore aggiunto che ci fornisce il libro Design Patterns è quello dissuasivo: supponiamo che avete (o avrete) un/a figlio/a che vuole dedicarsi all’Informatica, ma voi pensate che studiando Giurisprudenza o Economia possa avere un futuro più luminoso. Ecco, fategli trovare (casualmente) una copia di Design Patterns sul letto dicendo “…Ho ritrovato in cantina questo mio vecchio libro… dagli una occhiata, perché presto sarà il tuo pane quotidiano”. È probabile che la mattina successiva, verso le 6 (meglio non aspettare) verrete svegliati da vostro figlio/a chiedendovi “Come hai detto che ci si iscrive a Giurisprudenza?”…) 

CONCLUSIONE

Che possiamo dire in conclusione? Sicuramente che, se pensate che tutto quanto detto qui sopra sia solo farina del mio sacco, potete provate a interrogare il nostro amico Google, chiedendo, ad esempio: “global variables are evil?“, e vedrete che valanga di risultati vi verrà fuori. Se vi può interessare una delle pagine più ben fatte la trovate qui.
 
A questo punto penso che quanto detto sia sufficiente. Evidentemente io sono un po’ prevenuto perché ho lavorato a lungo su software multithreading (e ho ancora un po’ di mal di testa…), però vi chiedo di fidarvi e di propagare il messaggio il più possibile. Si, lo so, dire a un programmatore inesperto (o a un programmatore stanco) “non usare le variabili globali!” è come dire a un bambino “non mangiare troppe caramelle!“: beh, anche se a malincuore, bisogna farlo: la salute prima di tutto.

Ciao e al prossimo post!

domenica 20 gennaio 2019

Io e l'Integer Packing
come funziona il pack e unpack di interi in C

Annie: Oh, vai dallo psichiatra?
Alvy: S-sì. Da quindici anni soli...
Annie: Quindici anni?!
Alvy: Sì... gli concedo un altro anno... poi vado a Lourdes.
Nel capolavoro Io e Annie il Maestro Woody Allen ci proponeva, attraverso una personalissima "commedia romantica nevrotica", l'analisi della relazione tra i protagonisti Alvy Singer e Annie Hall in tutte le sue fasi (l'inizio/la relazione/la fine), mettendo in mostra fatti e misfatti in cui tutti ci possiamo riconoscere. Allo stesso modo (ma non con la classe del Maestro, purtroppo) cercherò con questo articolo di ricreare e (spero) risolvere una situazione che tutti i programmatori che si sporcano le mani con la programmazione low-level hanno dovuto affrontare (...ebbene si, cari programmatori high-level: sotto le vostre splendide applicazioni gira sempre del Software low-level, quasi sempre scritto in C, non dimenticatevelo...).
...Alvy: il pack/unpack di interi... Annie: argomento interessantissimo (yawn)...
Supponiamo (ma proprio supponiamo) che abbiamo due interi da 16 bit che vogliamo, magicamente, trasformare in un unico intero da 32 bit. Oppure vogliamo fare l'operazione inversa (spezzare un int a 32 bit in due int a 16 bit). Oppure abbiamo quattro interi da 16 bit e vogliamo... blah, blah, bla. I casi appena descritti non sono fantascienza, anzi sono casi abbastanza normali: ad esempio chi maneggia giornalmente i registri di periferiche Modbus sicuramente si è trovato a dover fare operazioni di questo tipo. Ecco, chi usa queste operazioni frequentemente può anche saltare il resto dell'articolo (e magari usare il tempo risparmiato per guardarsi un buon film e alimentare così la propria mente).

Ma, visto che mi è capitato di vedere facce terrorizzate di fronte a problemoni come questo (specialmente tra i colleghi non avvezzi alla programmazione low-level), cercherò di proporre alcune soluzioni, dopodiché si noterà che il tutto è di una semplicità disarmante. Si può, ovviamente, risolvere in 33 (o 33000) modi diversi, io ho pensato di mostrare i 3 modi che mi sembrano più semplici, lineari ed efficienti. Gli esempi che seguono sono di pack e unpack di interi a 32 bit (2 registri da 16bit in un intero da 32 bit e viceversa), ma adattare gli esempi ai 64 bit (4 registri da 16bit in un intero da 64 bit e viceversa) è veramente un gioco da ragazzi, e quindi non ve lo mostro nemmeno. Vai col codice dell'esempio 1!:

1) pack32 e unpack32 con bitwise

// pack32bitwise - copia un array di 2 uint16_t dentro una variabile uint32_t
uint32_t pack32bitwise(
    const uint16_t *src)    // array sorgente per il pack
{
    // ottiene il valore destinazione dal valore sorgente
    return ((uint32_t)src[1]) << 16 | src[0];
}

// unpack32bitwise - copia una variabile uint32_t in un array di 2 uint16_t
uint16_t *unpack32bitwise(
    uint16_t       *dest,   // array destinazione
    const uint32_t src)     // valore sorgente per l'unpack
{
    // ottiene il valore destinazione dal valore sorgente
    dest[1] = (uint16_t)((src & 0xFFFF0000) >> 16);
    dest[0] = (uint16_t)( src & 0x0000FFFF);

    return dest;
}
Questo è il modo più semplice e (probabilmente) più efficiente per fare il pack/unpack (io di solito uso questo). Si effettuano delle semplici operazioni di bitwise, che spostano o copiano i valori di entrata sull'uscita. Ovviamente la funzione pack() si aspetta un array come argomento e restituisce il valore intero corrispondente. Specularmente la funzione unpack() si aspetta un argomento con un valore e un argomento destinazione che sarà un array. Le operazioni di bitwise sono semplicissime: usando lo shift a 16 (visto che usiamo interi a 16 bit) possiamo copiare/spostare/mascherare opportunamente per ottenere il risultato voluto. Ed ora siamo pronti per l'esempio 2. vai col codice!:

2) pack e unpack con union

// pack32union - copia un array di 2 uint16_t dentro una variabile uint32_t
uint32_t pack32union(
    const uint16_t *src)    // array sorgente per il pack
{
    union {
        uint16_t src[2];
        uint32_t dest;
    } u;

    // ottiene il valore destinazione dal valore sorgente
    u.src[0] = src[0];
    u.src[1] = src[1];

    return u.dest;
}

// unpack32union - copia una variabile uint32_t in un array di 2 uint16_t
uint16_t *unpack32union(
    uint16_t       *dest,   // array destinazione
    const uint32_t src)     // valore sorgente per l'unpack
{
    union {
        uint16_t dest[2];
        uint32_t src;
    } u;

    // ottiene il valore destinazione dal valore sorgente
    u.src = src;
    dest[0] = u.dest[0];
    dest[1] = u.dest[1];

    return dest;
}
Questa maniera per eseguire il pack/unpack è quasi un trucco: si sfrutta una particolarità del linguaggio (il funzionamento delle union) in maniera impropria (non è che le union servono esattamente a quello)... ma visto che funziona perché non farlo? Quindi basta creare una union della forma opportuna e, assegnato a un membro della union il valore in ingresso, troveremo nell'altro membro della union il valore "rimappato" di uscita, esattamente quello che ci serve, no? Per chi in questo momento ha un vuoto di memoria su come funzionano le union basta ricordarsi di questo: la dimensione di una union è quella di un membro solo (quello che occupa più memoria) e, nel nostro caso, visto che i due membri occupano lo stesso spazio ma sono di tipo diverso ci viene offerto un immediato recasting implicito. Semplicissimo! Ed ora passiamo all'ultimo esempio. vai col codice!:

3) pack e unpack con memcpy()

// pack32memcpy - copia un array di 2 uint16_t dentro una variabile uint32_t
uint32_t pack32memcpy(
    const uint16_t *src)    // array sorgente per il pack
{
    // copia il valore sorgente nella destinazione
    uint32_t dest;
    memcpy(&dest, src, sizeof(dest));

    return dest;
}

// unpack32memcpy - copia una variabile uint32_t in un array di 2 uint16_t
uint16_t *unpack32memcpy(
    uint16_t       *dest,   // array destinazione
    const uint32_t src)     // valore sorgente per l'unpack
{
    // copia il valore sorgente nella destinazione
    uint32_t mysrc = src;
    memcpy(dest, &mysrc, sizeof(mysrc));

    return dest;
}
Visto che le operazioni di pack/unpack sono, bene o male, operazioni di copia/spostamento di memoria, non poteva mancare un esempio con la memcpy(), che è l'oggetto privilegiato e preferito quando si parla di operazioni di questo tipo. Mi risulta che questo metodo è molto usato, e l'ho usato anch'io ma, ultimamente, uso più spesso il metodo 1 (anche perché col metodo 1 è facilissimo, nel caso risulti necessario, invertire l'ordine dei registri. Non è usuale ma a volte capita). Il codice è così semplice che non c'è quasi nulla da spiegare: sfruttando il fatto che la memcpy() usa argomenti di tipo void* è perfettamente usabile per una operazione come la nostra, che consiste, a tutti gli effetti, nella copia di un blocco di memoria di un tipo in uno di un tipo diverso. A questo punto cosa ci manca? ma un bell'esempio d'uso, chiaro! Eccolo:
#include <stdio.h>
#include <stdint.h>
#include <string.h>

// prototipi locali
uint32_t pack32bitwise(const uint16_t *src);
uint16_t *unpack32bitwise(uint16_t *dest, const uint32_t src);
uint32_t pack32union(const uint16_t *src);
uint16_t *unpack32union(uint16_t *dest, const uint32_t src);
uint32_t pack32memcpy(const uint16_t *src);
uint16_t *unpack32memcpy(uint16_t *dest, const uint32_t src);

// funzione main
int main(int argc, char *argv[])
{
    // set variabili per test pack32()
    uint16_t src1[2];
    uint32_t dest1;
    src1[0] = 10;
    src1[1] = 20;

    // test pack32()
    dest1 = pack32bitwise(src1);;
    printf("con bitwise: src1={%d,%d} dest1=%d\n", src1[0], src1[1], dest1);
    dest1 = pack32union(src1);;
    printf("con union:   src1={%d,%d} dest1=%d\n", src1[0], src1[1], dest1);
    dest1 = pack32memcpy(src1);;
    printf("con memcpy:  src1={%d,%d} dest1=%d\n", src1[0], src1[1], dest1);

    // set variabili per test unpack32()
    uint32_t src2 = 1310730;
    uint16_t dest2[2];

    // test unpack32()
    unpack32bitwise(dest2, src2);
    printf("con bitwise; src=%d dest={%d,%d}\n", src2, dest2[0], dest2[1]);
    unpack32union(dest2, src2);
    printf("con union:   src=%d dest={%d,%d}\n", src2, dest2[0], dest2[1]);
    unpack32memcpy(dest2, src2);
    printf("con memcpy:  src=%d dest={%d,%d}\n", src2, dest2[0], dest2[1]);

    return 0;
}
Ecco, con questo piccolo main() di esempio potrete verificare che le varie versioni di pack e unpack offrono sempre gli stessi risultati (e ci mancherebbe solo che non fosse così!). E con questo ho detto tutto, e vi lascio, come compiti delle vacanze (vacanze? quali vacanze?) la trasformazione di tutti gli esempi a 32 bit in esempi a 64 bit: come ho già detto più sopra, è un gioco da ragazzi!

Ciao e al prossimo post!