Il linguaggio C, attraverso l'aritmetica dei puntatori, permette agli sviluppatori di passare da un tipo di dato ad un altro in maniera incontrollata. Solitamente solo gli sviluppatori virtuosi si applicano nella "artimetica dei puntatori". L'aritmetica dei puntatori puo' poi essere usata per fare una sorta di "introspezione", consentendo ad uno sviluppatore di estrarre da un puntatore ad un dato complesso (es. struct) un riferimento ad un singolo dato e viceversa. Per venire incontro agli sviluppatori, molti compilatori supportano delle istruzioni speciali, come ad esempio __builtin_offsetof di GCC, e la libreria C fornisce dei wrapper per queste funzioni. Uno di questi wrapper, molto interessante, e' appunto offsetof, che consente dato un tipo di struttura e il nome di un campo, di trovare l'offset del campo all'interno della struttura. Ad esempio, considerando la struttura seguente:
struct three_chars {
char a;
char b;
char c;
}
e volendo recuperare l'offset del campo "b" si puo' scrivere una cosa come segue:
offset = offsetof( struct three_chars, b );
che ritornera' il numero di bytes da sommare ad un puntatore alla base della struttura per ottenere l'indirizzo di memoria ove risiede "b". La macro offsetof e' indubbiamente comoda, ma risultati analoghi possono essere ricavati anche con l'analisi delle strutture e l'aritmetica dei puntatori; tuttavia lo sforzo richiesto e' maggiore.
Per meglio comprendere come si possa usare l'approccio con e senza offsetof ho creato alcuni semplici programmi che illustrano il funzionamento dell'aritmetica dei puntatori. L'idea alla base di questi programmi e' quella di avere l'annidamento di alcune strutture, un header e un pacchetto. Esistono due versioni di pacchetto: una con header all'inizio e una con header alla fine. Lo scopo delle due implementazione e' di mostrare come cambi l'aritmetica dei puntatori nei due casi. Le strutture dati sono molto semplici e definite come segue:
Per meglio comprendere come si possa usare l'approccio con e senza offsetof ho creato alcuni semplici programmi che illustrano il funzionamento dell'aritmetica dei puntatori. L'idea alla base di questi programmi e' quella di avere l'annidamento di alcune strutture, un header e un pacchetto. Esistono due versioni di pacchetto: una con header all'inizio e una con header alla fine. Lo scopo delle due implementazione e' di mostrare come cambi l'aritmetica dei puntatori nei due casi. Le strutture dati sono molto semplici e definite come segue:
struct header {
/**
* An header name, just to print something out.
*/
char* h_name;
/**
* A version, just to simulate some real header data.
*/
int h_version;
};
struct packet_with_header_at_top {
/**
* The packet header.
*/
struct header p_header;
/**
* A name for this packet, just to print out something.
*/
char* p_name;
/**
* An integer to simulate some real packet data.
*/
int p_data;
};
struct packet_with_header_at_bottom {
/**
* A name for this packet,
* just to print out something.
*/
char* p_name;
/**
* An integer to simulate some real packet data.
*/
int p_data;
/**
* The packet header.
*/
struct header p_header;
};
Si supponga di inizializzare strutture e relativi puntatori come segue:
// create an header
struct header my_header = {
.h_name = "HEADER",
.h_version = 1
};
struct packet_with_header_at_top my_packet_top = {
.p_header = my_header,
.p_name = "PACKET WITH HEADER AT TOP",
.p_data = 99
};
struct packet_with_header_at_bottom my_packet_bottom = {
.p_header = my_header,
.p_name = "PACKET WITH HEADER AT BOTTOM",
.p_data = 999
};
struct packet_with_header_at_top* top_pointer
= &my_packet_top;
struct packet_with_header_at_bottom* bottom_pointer
= &my_packet_bottom;
struct header* header_pointer
= &my_header;
Si consideri prima il caso privo di supporto offsetof: avendo inizializzato le due strutture di pacchetto e avendo i relativi puntatori, per estrarre l'header dai pacchetti occorre considerare il layout delle rispettive strutture pacchetto.
Nel caso di packet_with_header_at_top, essendo l'header il primo campo utile della struttura pacchetto, un puntatore al pacchetto e' automaticamente anche un puntatore all'header, e quindi non occorre applicare nessuna matematica dei puntatori:
Nel caso di packet_with_header_at_top, essendo l'header il primo campo utile della struttura pacchetto, un puntatore al pacchetto e' automaticamente anche un puntatore all'header, e quindi non occorre applicare nessuna matematica dei puntatori:
// cast the pointer to the packet to a pointer to the header
header_pointer = top_pointer;
printf( "\nHeader name and data extracted from the packet: %s - %d",
header_pointer->h_name,
header_pointer->h_version );
// cast back from header to packet
top_pointer = ( struct packet_with_header_at_top* ) header_pointer;
printf( "\n Name of the packet with header at top: %s",
top_pointer->p_name );
Nel caso di packet_with_header_at_bottom il puntatore alla struttura punta alla "base" della struttura, mentre l'header si trova ad un certo spiazzamento da questo. Occorre quindi calcolare manualmente l'offset considerando le dimensioni dei campi che precedono p_header nella struttura (e allineare l'offset stesso al byte):
// compute the supposed offset depending on the fields that are before
// the p_header
offset = sizeof( int ) + sizeof( char* );
// align the offset to 1 byte
offset = ( offset % sizeof( char* ) == 0 ? offset
: ceil( ( (double) offset / (double) sizeof( char* ) ) )
* sizeof( char* ) );
// move the pointer to the offset
header_pointer = ( (char*) bottom_pointer ) + offset;
printf( "\nHeader name and data extracted from the packet: %s - %d",
header_pointer->h_name,
header_pointer->h_version );
// cast back from header to packet using again the offset
bottom_pointer = ( struct packet_with_header_at_bottom* )
( (char*) header_pointer - offset );
printf( "\n Name of the packet with header at top: %s",
bottom_pointer->p_name );
Avendo quindi conoscenza del layout della struttura e' quindi possibile ottenere puntatori ai membri interni e, da questi, alla struttura stessa.
Tuttavia questo approccio non e' pienamente portabile, poiche' appunto prevede il calcolo manuale basato sul layout della struttura container.
E' qui che entra in gioco offsetof, che non richiede una conoscenza puntuale della struttura container; di seguito il caso relativo a packet_with_header_at_bottom (l'altro caso e' analogo):
Tuttavia questo approccio non e' pienamente portabile, poiche' appunto prevede il calcolo manuale basato sul layout della struttura container.
E' qui che entra in gioco offsetof, che non richiede una conoscenza puntuale della struttura container; di seguito il caso relativo a packet_with_header_at_bottom (l'altro caso e' analogo):
offset = offsetof( struct packet_with_header_at_bottom, p_header );
printf( "\nOffset computed is %d", offset );
// move the pointer to the offset
header_pointer = ( (char*) bottom_pointer )
+ offset;
printf( "\nHeader name and data extracted from the packet: %s - %d",
header_pointer->h_name,
header_pointer->h_version );
// cast back from header to packet using again the offset
bottom_pointer = ( struct packet_with_header_at_bottom* )
( (char*) header_pointer - offset );
printf( "\n Name of the packet with header at top: %s",
bottom_pointer->p_name );
Il funzionamento e' identico al caso manuale, ma qui non compare da nessuna parte l'elenco dei campi che precedono p_header, e quindi lo sviluppatore non e' costretto a ragionare sul layout della struttura container.
E' possibile implementare una sorta di "offsetof del poveraccio" usando ancora l'aritmetica dei puntatori e definendo una macro come la seguente:
E' possibile implementare una sorta di "offsetof del poveraccio" usando ancora l'aritmetica dei puntatori e definendo una macro come la seguente:
#define custom_offset_of( struct_pointer, struct_member ) \
( (size_t)
( (char*) &( (struct_pointer*) NULL )->struct_member
- (char*) NULL ) )
L'idea della macro e' abbastanza semplice e puo' essere riassunta nei seguenti passi:
1) si converte il valore NULL ad un puntatore alla struttura del tipo specificato, ossia si considera di avere una struttura che ha indirizzo di base 0: (struct_pointer*) NULL )
2) si chiede di accedere al campo specificato mediante operatore freccia, e si prende l'indirizzo del campo mediante operatore &. In altre parole si calcola dove risiede l'indirizzo di memoria del campo se la struct avesse indirizzo di base 0: &( (struct_pointer*) NULL )->struct_member
3) si sottrae dall'indirizzo di memoria del membro il valore di un puntatore a NULL, e quindi si ricava di quanti byte ci si deve spostare per arrivare al campo:
1) si converte il valore NULL ad un puntatore alla struttura del tipo specificato, ossia si considera di avere una struttura che ha indirizzo di base 0: (struct_pointer*) NULL )
2) si chiede di accedere al campo specificato mediante operatore freccia, e si prende l'indirizzo del campo mediante operatore &. In altre parole si calcola dove risiede l'indirizzo di memoria del campo se la struct avesse indirizzo di base 0: &( (struct_pointer*) NULL )->struct_member
3) si sottrae dall'indirizzo di memoria del membro il valore di un puntatore a NULL, e quindi si ricava di quanti byte ci si deve spostare per arrivare al campo:
&( (struct_pointer*) NULL )->struct_member - (char*) NULL )
4) si restituisce il valore trovato come size_t.
Quanto appena visto viene usato in modo pesante all'interno delle strutture dati del kernel, come ad esempio le liste (semplici e doppie).
Nessun commento:
Posta un commento