Macro static assert in C99 compatibile con MISRA-C
…e un piccolo extra!
⚠️ Se sai già di cosa sto parlando e vuoi solo la macro, puoi saltare direttamente alle conclusioni.
Spesso, specialmente nella programmazione embedded, ci sono dei controlli che è meglio eseguire a tempo di compilazione piuttosto che a runtime. Supponiamo di avere del codice (come ad esempio una libreria) che contiene alcune funzionalità dipendenti dal fatto che un unsigned int della piattaforma sia esattamente lungo 4 byte, o che un long sia esattamente lungo 8 byte, o che il tipo char sia con segno (signed), e così via. Potrebbero esserci molti commenti scritti nel codice riguardo a questi vincoli, ma alla fine, chi impedisce a te (o a qualcun altro) di violare queste assunzioni?
Ad esempio, supponiamo di avere una struct che deve avere esattamente una certa dimensione specifica per una libreria o applicazione:
Esempio 1:
typedef struct // __attribute__((packed))
{
short a;
char b;
} PackedOrNot_t;
/*
* Abbiamo bisogno dell'attributo packed?
* Se sì, come lo posso imporre?
*/
In questo caso, la dimensione sizeof della struct può essere di 3 o 4 byte, a seconda che si usi l’attributo packed o meno. Come possiamo assicurarci che un errore del genere non passi mai inosservato? E se avessimo altre persone che lavorano con noi allo stesso progetto, o se dovessimo consegnare interamente il progetto/libreria a qualcun altro?
Oppure un altro esempio: supponiamo di avere una struct che è la rappresentazione 1:1 di alcuni dati specifici del mondo reale. Diciamo che deve essere inviata tramite un’interfaccia che richiede una dimensione multipla di 8 byte (es. tramite CAN). Come puoi essere programmaticamente sicuro che l’aggiunta di elementi alla nostra struct mantenga la sua dimensione un multiplo esatto di 8 byte? Potremmo aggiungere dei commenti che spiegano come riempire (padding) la struttura con byte riservati extra, ma siamo sempre a un passo da una potenziale fonte di fastidiosi bug a runtime.
Esempio 2:
typedef struct
{
char someData;
char someMoreData;
...
/* Supponiamo che la definizione di questa struttura sia molto,
* lunga, possibilmente anche con altre struct al suo interno.
*/
} EightBytesMultiple_t;
/*
* ..siamo sicuri che sarà sempre lunga un multiplo di 8-bytes?
*/
Un ultimo esempio: prima del C99, il comportamento della divisione tra interi dipendeva dalla specifica implementazione (l’arrotondamento poteva andare verso lo zero o verso l’infinito negativo). Dal C99 in poi, la divisione tra interi è ben definita in quanto esegue un troncamento verso lo zero. Se parte della logica del codice dipende dal fatto che questo comportamento sia ben definito, come possiamo imporlo al tempo di compilazione?
Esempio 3:
...
// Prima del C99 (ad esempio in ANSI-C) il risultato poteva
// essere -2 o -3 a seconda dell'implementazione!
int result = -10 / 4;
Fortunatamente, il C11 include il predicato _Static_assert, come si può vedere qui. Tuttavia, a volte siamo limitati all’uso di standard C più vecchi, come il C99, specialmente per l’utilizzo embedded/automotive. Esiste quindi un modo per aggirare questo problema nelle vecchie versioni del C?
Nessun preprocessore è stato maltrattato per la stesura di questo articolo
In realtà sì, possiamo aggirare l’ostacolo sfruttando il preprocessore/compilatore di clang o gcc. Quello che potremmo pensare di fare è una cosa del genere:
typedef char __something[(expression) ? (+1) : (-1)];
Qui, il compilatore valuterà qualsiasi espressione che può essere risolta staticamente all’interno delle parentesi e sostituirà +1 se l’espressione viene valutata come true o -1 in caso contrario. Poiché gli array non possono essere dichiarati con una dimensione negativa, otterremo un errore di compilazione nel caso di risultato negativo dell’espressione; o in altre parole, un fallimento dell’asserzione. Benissimo! Approfondiamo questo concetto; facciamo in modo che una macro faccia tutto il lavoro per noi:
#define STATIC_ASSERT(expr) typedef char __something[(expr) ? (+1) : (-1)];
Il primo problema evidente è che stiamo applicando un typedef a un nuovo tipo di variabile sempre con lo stesso nome (ci torneremo dopo…); inoltre, il segnaposto __something che utilizziamo potrebbe essere già utilizzato per qualcos’altro. Ok, risolveremo questi due potenziali problemi usando la concatenazione delle macro e un piccolo aiuto da __COUNTER__:
#define GLUE(a, b) a##b
#define EVAL(expr, name) typedef char GLUE(__assert_test_t_, name)[(expr) ? (+1) : (-1)]
#define STATIC_ASSERT(expr) EVAL(expr, __COUNTER__)
Qui __COUNTER__ è una speciale macro estensione del compilatore che viene incrementata ogni volta che viene utilizzata. Così, ad esempio, la pre-elaborazione di:
...
STATIC_ASSERT(sizeof(int) == 4)
STATIC_ASSERT(sizeof(long) == 8)
Verrà espansa come:
...
typedef char __assert_test_t_0[(sizeof(int) == 4) ? (+1) : (-1)];
typedef char __assert_test_t_1[(sizeof(long) == 8) ? (+1) : (-1)];
Queste due asserzioni falliranno correttamente (dando un errore di compilazione) ogni volta che sarà necessario. Quindi abbiamo finito, giusto?
MISRA entra in scena e rovina il divertimento
Ricordi quando ho detto che due typedef non possono avere lo stesso nome? In realtà al compilatore non importa; ma a MISRA-C sì. Quindi, cos’è? È un insieme di linee guida per lo sviluppo software nel linguaggio di programmazione C, con lo scopo di facilitare la sicurezza (safety e security), la portabilità e l’affidabilità del codice nel contesto dei sistemi embedded. È stato originariamente scritto per l’industria del software embedded in ambito automotive, ma oggigiorno è usato anche in altri settori, come la difesa, le telecomunicazioni, i dispositivi medici, ecc. Se hai mai lavorato come sviluppatore C embedded in uno di questi campi, sai di cosa sto parlando.
La macro, nella sua forma originale, ridefinisce un typedef più volte, violando:
- Regola MISRA-C:2012 5.6: Il nome di un typedef deve essere un identificatore univoco.
La seconda versione con __COUNTER__ risolve questo vincolo. Ma abbiamo una nuova violazione:
- Regola MISRA-C:2012 2.3: Un progetto non dovrebbe contenere dichiarazioni di tipi inutilizzati.
Questo perché per ogni STATIC_ASSERT(...) stiamo definendo un nuovo alias di tipo con il nome __assert_test_t_n, ma di fatto non lo stiamo mai usando nel codice.
Possiamo aggirare anche questo? Certamente! Dobbiamo solo usare effettivamente il tipo a cui abbiamo dato l’alias. Guarda qua:
#define EVAL(expr, name) \
typedef char GLUE(__assert_test_t_, name)[(expr) ? (+1) : (-1)]; \
static GLUE(__assert_test_t_, name) GLUE(__assert_test_var_, name)
#define STATIC_ASSERT(expr) EVAL(expr, __COUNTER__)
Quello che stiamo facendo è essenzialmente dichiarare una variabile static chiamata __assert_test_var_n per ogni dichiarazione typedef di __assert_test_t_n. Questo garantisce la conformità a quest’ultima regola.
Tuttavia, se controlliamo la nostra base di codice usando le regole MISRA-C, ci sono altissime probabilità che si utilizzino anche tutti i flag di warning a nostra disposizione (come -Wall e -Wextra). In questo caso, otterremo una serie di avvertimenti che ci segnalano le nostre dichiarazioni di variabili inutilizzate; usando gcc -Wall -Wextra:
example.c:6:46: warning: ‘__assert_test_var_0’ defined but not used [-Wunused-variable]
6 | static GLUE(__assert_test_t_, name) GLUE(__assert_test_var_, name)
| ^~~~~~~~~~~~~~~~~~
example.c:3:20: note: in definition of macro ‘GLUE’
3 | #define GLUE(a, b) a##b
| ^
example.c:7:29: note: in expansion of macro ‘EVAL’
7 | #define STATIC_ASSERT(expr) EVAL(expr, __COUNTER__)
| ^~~~
example.c:18:1: note: in expansion of macro ‘STATIC_ASSERT’
18 | STATIC_ASSERT(sizeof(int) == 4);
| ^~~~~~~~~~~~~
E usando clang -Wall -Wextra:
example.c:18:1: warning: unused variable '__assert_test_var_0' [-Wunused-variable]
STATIC_ASSERT(sizeof(int) == 4);
^
example.c:7:29: note: expanded from macro 'STATIC_ASSERT'
#define STATIC_ASSERT(expr) EVAL(expr, __COUNTER__)
^
example.c:6:41: note: expanded from macro 'EVAL'
static GLUE(__assert_test_t_, name) GLUE(__assert_test_var_, name)
^
example.c:3:20: note: expanded from macro 'GLUE'
#define GLUE(a, b) a##b
^
<scratch space>:49:1: note: expanded from here
__assert_test_var_0
^
1 warning generated.
Questo può però essere risolto rapidamente usando l’estensione del compilatore __attribute__((unused)) nella nostra macro. Aggiungendo questo:
#define EVAL(expr, name) \
typedef char GLUE(__assert_test_t_, name)[(expr) ? (+1) : (-1)]; \
static GLUE(__assert_test_t_, name) GLUE(__assert_test_var_, name) __attribute__((unused))
#define STATIC_ASSERT(expr) EVAL(expr, __COUNTER__)
Otteniamo zero warning sia con gcc -Wall -Wextra che con clang -Wall -Wextra. Usando quest’ultimo, se compiliamo con -g per includere anche i simboli di debug, possiamo anche vedere tramite objdump che i simboli alias di tipo dell’asserzione e le definizioni delle variabili statiche non vengono nemmeno inclusi nell’oggetto binario finale, e quindi non occupano un singolo bit di spazio extra (testato con gcc 11.4.0 e clang 14.0.0).
A proposito di quel MISRA…
A dire il vero, potresti aver notato che questa implementazione della static assertion viola palesemente una delle regole MISRA:
- Regola MISRA-C:2012 20.10: Gli operatori del preprocessore # e ## non dovrebbero essere usati.
Tuttavia, vorrei sottolineare che questa regola è solo consultiva (advisory) e la sua analisi è decidibile (decidable). Inoltre, la motivazione alla base di questa regola sostiene che:
Motivazione della Regola 20.10:
L’ordine di valutazione associato a operatori del preprocessore # multipli, ## multipli o un misto di # e ## non è specificato. In alcuni casi non è quindi possibile prevedere il risultato dell’espansione della macro. L’uso dell’operatore ## può produrre un codice oscuro.
Nel nostro caso, però, l’ordine di valutazione è chiaramente ben definito e non lascia spazio a interpretazioni; perciò penso che sia una valida eccezione alla regola, e che il vantaggio di avere un predicato di static assertion in C99 ne valga assolutamente la pena.
Conclusioni
Ecco la macro nella sua forma finale:
#define GLUE(a, b) a##b
#define EVAL(expr, name) \
typedef char GLUE(__assert_test_t_, name)[(expr) ? (+1) : (-1)]; \
static GLUE(__assert_test_t_, name) GLUE(__assert_test_var_, name) __attribute__((unused))
#define STATIC_ASSERT(expr) EVAL(expr, __COUNTER__)
Con questo predicato otterremmo un errore di compilazione ogni volta che l’espressione fallisce; i problemi descritti in precedenza verrebbero gestiti come segue:
- Es. 0 (controlli sui tipi specifici della piattaforma):
// I tipi della piattaforma hanno la dimensione richiesta?
STATIC_ASSERT(sizeof(int) == 4);
STATIC_ASSERT(sizeof(long long) == 8);
...
// Il tipo "char" è provvisto di segno (signed)?
#include <limits.h>
STATIC_ASSERT(CHAR_MIN < 0);
- Es. 1:
// Hai dimenticato di applicare il pack alla tua struct?
STATIC_ASSERT(sizeof(PackedOrNot_t) == 3);
- Es. 2:
// Hai dimenticato dei byte extra di padding nella definizione della tua struct?
STATIC_ASSERT(sizeof(EightBytesMultiple_t) % 8 == 0);
- Es. 3:
// La divisione tra interi con valori negativi arrotonda correttamente verso lo zero?
STATIC_ASSERT(-10 / 4 == -2);
Un piccolo extra
Volevo solo aggiungere una piccola chicca extra: supponiamo che tu abbia una definizione di struct molto lunga e nidificata nel tuo codice:
typedef struct
{
int a;
short b;
unsigned long c[10];
SomeOtherStruct_t d;
...
// Immaginiamo che questa definizione sia davvero
// molto lunga e nidificata.
} VeryLongStruct_t;
Cosa succede se avessi bisogno di ottenere il suo sizeof senza fare tutti i calcoli a mano, o senza entrare in modalità debug? Questa macro torna utile:
#define SIZEOF_WARN(type) char (*__var)[sizeof(type)] = 1;
Una volta compilata, questa macro genererà un warning del compilatore, e nell’output del warning sarà presente il sizeof corretto. Esempio d’uso:
SIZEOF_WARN(VeryLongStruct_t)
Compila con i soliti flag -Wall -Wextra ed ecco fatto:
example.c:9:57: warning: initialization of ‘char (*)[246]’ from ‘int’ makes pointer from integer without a cast [-Wint-conversion]
9 | #define SIZEOF_WARN(type) char (*__var)[sizeof(type)] = 1;
| ^
example.c:20:1: note: in expansion of macro ‘SIZEOF_WARN’
20 | SIZEOF_WARN(VeryLongStruct_t)
| ^~~~~~~~~~~
La tua VeryLongStruct_t ha un valore sizeof di 246.