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ì 27 aprile 2022

Riders of Strptime
come si processano data e ora con la strptime in C - pt.2

Membro del consiglio: Lascia che ti fermi qui, Otto.
Otto: Si.
Membro del consiglio: Quanto tempo lei e il suo team avete passato su questo algoritmo?
Otto: Oh ragazzi, è difficile da dire esattamente... Quarantasei settimane. Ma l'abbiamo fatto principalmente di notte.
Membro del consiglio: Quindi abbiamo speso un anno e una fortuna per un algoritmo che può capire che i poveri guidano Kia e i ricchi Mercedes?

Ebbene si, sono un perfezionista. È un difetto? È un pregio? Boh, non lo so. Anche Otto e Lars, i due coprotagonisti del bellissimo Riders of Justice sono dei perfezionisti, tanto da dedicare ben quarantasei settimane, alla scrittura di un algoritmo in grado di scoprire che i poveri guidano Kia e i ricchi guidano Mercedes (e li hanno licenziati: non c'è giustizia in questo mondo). Io, ad esempio, ho sempre tentato scrivere codice buono, a costo di sforare le tempistiche di realizzazione, piuttosto che scrivere in fretta codice che "basta che stia in piedi". E se c'è qualche altro perfezionista tra i lettori (è un difetto frequente nei programmatori) mi capirà. Bravi Otto e Lars, io non vi avrei mai licenziato.

...siamo qui per difendere i programmatori-perfezionisti. Qualcosa in contrario?... 

E cosa centra il discorso iniziale con questo articolo? Centra, centra... il problema è che pochi mesi fa ho scritto un post intitolato Strptime: No Way Home in cui proponevo una (spero) interessante versione "custom" della strptime(3), che è una ottima funzione della libc, ma ha qualche criticità d'uso e di funzionamento. In particolare mi ero soffermato su un problema reale che ho affrontato in un progetto, un problema sulla gestione di data/ora (da qui in avanti datetime) in formato ISO 8601 "ma con i millisecondi", e proprio i millisecondi non piacciono molto alla strptime(3). Ora non mi sembra il caso di ripetere tutta la presentazione e gli esempi del vecchio articolo (basta rileggerlo, no?), credo che basti dire che è tutt'ora valido nei primi tre quarti (descrizione del tema, esempi vari e codice esplicativo) ma nell'ultimo quarto... c'e una funzione che avevo scritto ad-hoc, la myStrptime(), che già` al tempo della pubblicazione non mi  convinceva più di tanto (non mi dava una buona impressione neanche "esteticamente", sapete com'è: sesto senso del programmatore).

(...e, tra l'altro, la myStrptime() contiene anche un piccolo bug, non grave direi, ma pur sempre un bug. Chissà se qualcuno se n'è accorto...)

Che fare allora? Correggere o modificare l'articolo originale per migliorarlo facendo finta di niente? Oppure pubblicare un remake? (un remake dopo tre mesi? Nel cinema di solito si aspetta un po' di più...). Ok, alla fine ho optato per fare una "seconda parte" (questa che state leggendo) in cui vi invito a rileggere l'articolo "prima parte", ricordandovi, però, di sorvolare sulla funzione finale myStrptime() prendendo per buona quella nuova che vi mostrerò tra poco.

Ok, si può fare!

Allora, riepiloghiamo: la strptime(3) funziona bene ed è ben adattabile a moltissimi tipi di datetime. Noi vogliamo farne una versione specializzata, che tratta qualche tipo in meno ma accetta i millisecondi, e già che ci siamo, approfittiamo per renderla un po' più user-friendly (cambiando la sintassi d'uso: si perde in genericità ma si guadagna in facilità di sviluppo). E per quanto riguarda i tipi accettati bisogna coprire più casi  rispetto alla myStrptime() originale: la famiglia di datetime accettata, sarà la seguente:

DATETIME UTC
------------
2022-04-23T09:30:01Z
2022-04-23T09:30:01.278Z
2022-04-23T11:30:01+0200
2022-04-23T11:30:01+02.00
2022-04-23T11:30:01.278+0200
2022-04-23T11:30:01.278+02.00
20220423T09:30:01Z
20220423T09:30:01.278Z
20220423T11:30:01+0200
20220423T11:30:01+02.00
20220423T11:30:01.278+0200
20220423T11:30:01.278+02.00
2022-04-23T093001Z
2022-04-23T093001.278Z
2022-04-23T113001+0200
2022-04-23T113001+02.00
2022-04-23T113001.278+0200
2022-04-23T113001.278+02.00
20220423T093001Z
20220423T093001.278Z
20220423T113001+0200
20220423T113001+02.00
20220423T113001.278+0200
20220423T113001.278+02.00
DATETIME NON UTC
----------------
2022-04-23T11:30:01
2022-04-23T11:30:01.278
20220423T11:30:01
20220423T11:30:01.278
2022-04-23T113001
2022-04-23T113001.278
20220423T113001
20220423T113001.278

Se qualcuno ha familiarità con il formato ISO 8601 noterà che l'obiettivo (ambizioso, direi) è coprire tutti i formati previsti del datetime UTC, il che non è poco! (nella versione del vecchio articolo si coprivano solo i primi 6 casi). E tratteremo anche i millisecondi! E, ciliegina sulla torta, copriremo anche alcuni formati non UTC. Sono ben 32 formati diversi, e scusate se è poco!

Allora, bando alle ciance: la nuova funzione l'ho ribattezzata isoStrptime() ed è questa (con relativo main() di esempio):

#define _XOPEN_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

// prototipi locali
int isoStrptime(const char *s, struct tm *tm);

// main() - funzione main
int main(void)
{
char buf[256];
char datetime[32];
struct tm tm;

strcpy(datetime, "2022-04-23T09:30:01Z");
if (isoStrptime(datetime, &tm) != -1) {
strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm);
printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf);
}
else
printf("ERROR!\n");

strcpy(datetime, "20220423T113001.278+0200");
if (isoStrptime(datetime, &tm) != -1) {
strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm);
printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf);
}
else
printf("ERROR!\n");

exit(EXIT_SUCCESS);
}

// isoStrptime() - funzione wrapper per strptime(3)
int isoStrptime(
const char *s, // datetime sorgente
struct tm *tm) // struct tm destinazione
{
const char *format; // formato del datetime sorgente
size_t len; // lunghezza della parte data+ora+secondi

// analizzo la stringa sorgente
if (strchr(s, '-') && strchr(s, ':')) {
// appartiene al type/subtype: 2022-04-23T09:30:01Z
format = "%Y-%m-%dT%H:%M:%S%z";
len = 19;
}
else if (strchr(s, '-') == NULL && strchr(s, ':')) {
// appartiene al type/subtype: 20220423T09:30:01Z
format = "%Y%m%dT%H:%M:%S%z";
len = 17;
}
else if (strchr(s, '-') && strchr(s, ':') == NULL) {
// appartiene al type/subtype: 2022-04-23T093001Z
format = "%Y-%m-%dT%H%M%S%z";
len = 17;
}
else if (strchr(s, '-') == NULL && strchr(s, ':') == NULL) {
// appartiene al type/subtype: 20220423T093001Z
format = "%Y%m%dT%H%M%S%z";
len = 15;
}

// controllo se la lunghezza della stringa sorgente è Ok
char part_one[32];
if (strlen(s) >= len && strlen(s) <= 29) {
// reset del tm prima di usarlo (questo è importante)
memset(tm, 0, sizeof(struct tm));

// estraggo la parte data+ora+secondi
memcpy(part_one, s, len);
part_one[len] = 0;

// eventualmente estraggo la seconda parte (il timezone)
char mys[32];
char part_two[32];
if (strlen(s) > strlen(part_one) && (strlen(s) - strlen(part_one)) != 4) {
// case con timezone
snprintf(part_two, sizeof(part_two), "%s", &s[strlen(part_one)]);

// ricompongo le due parti e applico strptime(3)
char *mytimezone;
if ((mytimezone = strchr(part_two, 'Z')) != NULL) {
// caso speciale con Z finale
snprintf(mys, sizeof(mys), "%s%s", part_one, mytimezone);
}
else if ( (mytimezone = strchr(part_two, '+')) != NULL ||
(mytimezone = strchr(part_two, '-')) != NULL ) {

// caso con timezone esplicito
snprintf(mys, sizeof(mys), "%s%s", part_one, mytimezone);
}

// uso strptime(3) per processare la nuova stringa
if (strptime(mys, format, tm) != NULL)
return 0; // strptime(3) Ok
}
else {
// caso senza timezone (tolgo il %z dal format)
char format_noz[32];
snprintf(format_noz, sizeof(format_noz), "%s", format);
format_noz[strlen(format) - 2] = 0;

// uso strptime(3) per processare la nuova stringa
if (strptime(part_one, format_noz, tm) != NULL)
return 0; // strptime(3) Ok
}
}

// lunghezza non Ok oppure errore della strptime(3): ritorno errore
return -1;
}

Che ve ne pare? Questa mi dà già una buona impressione estetica (e di solito mi fido di questa impressione), ed è, evidentemente, molto più facile da usare: solo 2 parametri invece di 4! E non c'è bisogno di passare il format (che viene auto-costruito internamente), basta passare solo la stringa del datetime e la struct tm destinazione. Mi piace, e non contiene neanche il (piccolo) bug della myStrptime() citato sopra. Nel main() ho messo solo 2 esempi di uso (per renderlo meno pesante da leggere), ma sulla falsariga dei 2 esempi potete aggiungere gli altri 30, e magari anche qualche caso di data erronea (io l'ho fatto e funzionano tutti e 32, e anche gli errori sono ben rilevati).

Ebbene si, ora mi sento meglio, il peso da "funzione venuta male" si è affievolito, finalmente. E quindi per oggi può bastare, così potrò dedicare le prossime 46 settimane a creare qualche geniale algoritmo...

Ciao, e al prossimo post! 

sabato 19 marzo 2022

Signal Handler
come scrivere un signal handler in C

Michele Apicella: ...il Mont Blanc si regge su un equilibrio delicato, non è come la Sacher Torte...
Mario: Cosa?
Michele Apicella: La Sacher Torte...
Mario: Cos'è?
Michele ApicellaCioè, lei praticamente non ha mai assaggiato la Sacher Torte?
Mario: No.
Michele Apicella: Vabbè, continuiamo così, facciamoci del male...

Signal Handler, un titolo secco e breve che mi fa venire in mente Bianca del grande Nanni Moretti (ogni scusa è buona per ricordarsi di un gran film). Anche qui un titolo secco per un film bellissimo, che ci presenta, con magistrali toni da commedia, argomenti tragici come solitudine, nevrosi e ossessioni... e il momento mitico citato in apertura ci ricorda che alcune cose date per scontate (la Sacher Torte) possono essere meno scontate di quello che sembra, come i Segnali di POSIX (e quindi di UNIX/Linux).

...Cioè, lei praticamente non ha mai scritto un Signal Handler?...

Allora, in questo articolo parleremo di segnali, che sono la forma più semplice e antica di IPC nei sistemi POSIX. Anzi, dovrei aggiungere che nel ciclo di articoli sulla POSIX IPC (qui, qui e qui le tre parti) non ne ho parlato volutamente: e perché? Beh, in quel ciclo si parlava di comunicazione tra processi a livello di trasferimento di dati (e quindi con messaggi, memoria condivisa, ecc.), con invio e ricezione delle più svariate informazioni. I segnali, invece, sono una forma di comunicazione sicura e potente, ma limitata a un set di informazioni ben precise e codificate. Ossia sono del tipo: "Ti invio il segnale n.9 e tu sai già cosa devi fare".

Come detto, i segnali sono la forma più antica di comunicazione tra processi: sono anteriori a POSIX (che si avviò nel 1988) e sono, addirittura, anteriori anche alla prima edizione del nome IPC, che venne usato per la prima volta nel 1983 per denominare il System V IPC di Unix SVR1 (Unix System V Release 1). Ebbene si, i segnali sono veramente antichi: sono apparsi per la prima volta in Unix V4 (Unix 4th Edition), nel 1973! Quanto tempo è passato...

Allora, come detto sopra, i segnali si limitano a "un set di informazioni ben precise e codificate", che si possono listare usando il comando "kill -l"  e sulla mia macchina Linux sono queste:

aldo@Linux $ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

Sono un po' più di quelli standard (che sono 28) descritti nello standard POSIX, infatti almeno gli ultimi 30 della lista mostrata hanno un uso molto limitato e speciale. Il comando kill(1), è disponibile nella shell di Linux, e usa internamente le system call che ci interessano in questa sede, quelle relative ai segnali.

Il meccanismo d'uso dei segnali che può essere implementato nelle applicazioni che scriviamo è relativamente semplice:

  1. Un processo può inviare a un altro processo uno dei segnali listati qua sopra usando la system call kill(2).
  2. Il processo ricevente eseguirà l'azione di default collegata al segnale ricevuto, a meno che non abbia previamente definito un signal handler.
  3. Un processo può definire un signal handler (ad esempio usando la system call signal(2)) per trattare opportunamente i segnali ricevuti

Punto. Cioè, in realtà l'argomento è un po' più complicato, ma sommariamente si può sintetizzare con i tre punti appena descritti.

1) Si comincia con signal(2)

E quindi, cosa è un signal handler? È, come dice il nome, una funzione che "maneggia" opportunamente i segnali. Come dite? Non è chiaro? Volete un semplice esempio? Eccolo!

#include<stdio.h>
#include<signal.h>
#include<unistd.h>

// prototipi locali
static void sigHandler(int signum);

// main() - funzione main
int main(void)
{
// WARNING! usiamo signal() solo per test!

// creo un signal handler con signal() per SIGKILL (sig n.9)
if (signal(SIGKILL, sigHandler) == SIG_ERR)
printf("non posso intercettare SIGKILL\n");

// creo un signal handler con signal() per SIGINT (sig n.2)
if (signal(SIGINT, sigHandler) == SIG_ERR)
printf("non posso intercettare SIGINT\n");

// un loop infinito per testare la applicazione
for (;;)
sleep(1);

return 0;
}

// sigHandler() - un semplice signal handler
static void sigHandler(int signum)
{
// WARNING! usiamo printf() solo per test!

// mostro l'arrivo di uno dei segnali intercettati
if (signum == SIGKILL)
printf("ricevuto un SIGKILL\n");
else if (signum == SIGINT)
printf("ricevuto un SIGINT\n");
}

Che ne dite? È un codice semplicissimo, di poche righe (e anche ben commentate).

Sorvolando sui due Warning inseriti nei commenti (ci torneremo dopo) è evidente che è relativamente semplice scrivere un signal handler: basta chiamare signal(2) indicando quale segnale vogliamo intercettare e con quale funzione vogliamo farlo. Poi, ovviamente, dobbiamo scrivere la funzione di handler, che può fare cose più o meno sofisticate, e in questo programma di test si limita a dire che ha intercettato il segnale. E vediamo cosa ci mostra il programma durante l'esecuzione:

aldo@Linux $ ./signal
non posso intercettare SIGKILL
^Cricevuto un SIGINT
ricevuto un SIGINT
^Cricevuto un SIGINT
Ucciso

Allora: ho eseguito il programma e subito ho ricevuto il messaggio di errore "non posso intercettare SIGKILL"  perché, sfortunatamente, alcuni segnali non si possono intercettare, e SIGKILL è proprio uno di questi (tenetelo presente quando scrivete un signal handler!). Dopodiché il programma non fa nulla (ha un loop infinito di sleep(3)) e si risveglia quando arriva un segnale intercettato, nel nostro caso SIGINT che è il "interrupt signal" e si può inviare semplicemente scrivendo "Ctrl + C" (righe 3 e 5 del test) oppure usando il comando shell kill(1) da un altro terminale (riga 4 del test). Quando il nostro programma riceve un segnale non intercettato esegue l'azione standard per quel tipo di segnale e, nel nostro caso, ho inviato un SIGKILL con "kill -9 pid-del-processo" , e il processo è stato ucciso (riga 6 del test. E questa è, tristemente, l'azione standard di SIGKILL).

La struttura del programma di test ci insegna che è possibile installare un signal handler che tratta più segnali alla volta, basta aggiungere altre chiamate a signal(2) nel main() e altri "else if"  nella funzione sigHandler(). E ogni segnale intercettato può eseguire diverse attività quando viene ricevuto, così si può personalizzare notevolmente il comportamento in varie situazioni di emergenza e non. E quale è un esempio molto tipico di uso un signal handler? Io direi la gestione dell'uscita controllata: ossia si può decidere che alla ricezione di un determinato segnale (tipicamente SIGTERM) il nostro programma esegua una serie di attività (tipo fermare dei thread, scrivere dei buffer su disco, liberare la memoria, ecc.) e poi uscire. Et voilà!

E ora veniamo alle dolenti note, quelle indicate nei due Warning del programma di test:

- WARNING! usiamo signal() solo per test!

Abbiamo visto che usare signal(2) è relativamente semplice, però, sfortunatamente, il suo uso è scoraggiato, e il motivo è ben spiegato nel manuale:

The behavior of signal() varies across UNIX versions, and has also var‐
ied historically across different versions of Linux. Avoid its use:
use sigaction(2) instead. See Portability below.

The effects of signal() in a multithreaded process are unspecified.

POSIX.1 solved the portability mess by specifying sigaction(2), which
provides explicit control of the semantics when a signal handler is in‐
voked; use that interface instead of signal().

da: SIGNAL(2) Linux Programmer's Manual

ossia, signal(2) è standard (è addirittura parte del ISO C/ANSI C/Standard C) però fornisce comportamenti imprevedibili nel multithreading e, soprattutto, ha problemi di portabilità (il che è un po' assurdo per una funzione standard del C!). POSIX ha risolto il problema deprecando signal(2) e inserendo la sigaction(2) che è, definitivamente, l'interfaccia raccomandata per tutte le nuove applicazioni (anche se, paradossalmente, questa nuova funzione è meno portabile in assoluto, perché non fa parte del Standard C ma solo di POSIX).

Comunque, è importante evidenziare che è assolutamente sconsigliato mischiare signal(2) e sigaction(2) nello stesso programma: lo standard POSIX stesso, per evitare problemi di questo tipo, raccomanda di implementare (a livello libreria) signal(2) usando la sigaction(2) stessa (e mi risulta che su Linux sia così). La sigaction(2) la vedremo tra poco.

- WARNING! usiamo printf() solo per test!

Questo è un problema più semplice da descrivere: la printf(3) è una funzione che non fa parte di una lista di funzioni che sono, per così dire, "signal-handler-safe" (in realtà il nome esatto è "async-signal-safe functions"). E la spiegazione è ben descritta qui:

When a signal occurs, the normal flow of control of a program is interrupted.
If a signal occurs that is being trapped by a signal handler, that handler is
invoked. When it is finished, execution continues at the point at which the
signal occurred. This arrangement can cause problems if the signal handler
invokes a library function that was being executed at the time of the signal.

da: C Rationale, 7.14.1.1 [C99 Rationale 2003]

2) Si prosegue con sigaction(2)

E ora credo che siamo pronti per esaminare una versione più accettabile del programma di test. Vai col codice!

#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>

#define SIGINT_MSG "ricevuto un SIGINT\n"

// prototipi locali
static void sigHandler(int signum);

/* un flag per mantenere attivo il loop pseudo-infinito.
volatile potrebbe essere necessario a seconda del sistema/implementazione
in uso. (vedere "C11 draft standard n1570: 5.1.2.3") */
volatile sig_atomic_t resta_attivo = 1;

// main() - funzione main
int main(int argc, char *argv[])
{
// preparo i dati per il signal handler
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sigHandler;
sa.sa_flags = SA_RESTART;

// creo un signal handler con signal() per SIGINT (sig n.2)
if (sigaction(SIGINT, &sa, NULL) == -1)
printf("non posso intercettare SIGINT\n");

// un loop pseudo-infinito per testare la applicazione
while (resta_attivo)
sleep(1);

// il loop pseudo-infinito è stato interrotto da un SIGINT
printf("loop teminato\n");
return 0;
}

// sigHandler() - un semplice signal handler
static void sigHandler(int signum)
{
// mostro l'arrivo di un SIGINT e resetta il flag resta_attivo
write(STDERR_FILENO, SIGINT_MSG, sizeof(SIGINT_MSG));
resta_attivo = 0;
}

Il codice è molto simile a quello presentato prima, ma con alcune interessanti varianti:

  • visto che si usa la sigaction(2) si prepara anche una struttura dati "struct sigaction" che imposta il funzionamento. Associata a questa struttura ci sono un po' di variabili e funzioni di supporto che si possono utilizzare (ben descritte nel man-page della sigaction(2)). Questo esempio l'ho scritto semplificandolo al massimo (e funziona bene ugualmente) ma si può complicare a piacere, in base alle esigenze.
  • la sigaction(2) una volta impostati i dati, si usa, più o meno, come la signal(2).
  • la funzione sigHandler() non usa più printf(3) ma usa write(2) che è asynchronous-safe.
  • il loop infinito ora è un loop pseudo-infinito, con una uscita controllata da una variabile flag gestita dalla funzione sigHandler(): qui si nota che l'obbligo di usare funzioni asynchronous-safe dentro il signal handler si estende anche alle variabili, e quindi bisogna usare un tipo sig_atomic specificato come volatile per poter modificare il flag dentro il signal handler.

Ed ora possiamo vedere cosa ci mostra il programma durante l'esecuzione:

aldo@Linux $ ./sigaction
^Cricevuto un SIGINT
loop teminato
aldo@Linux $ ./sigaction
ricevuto un SIGINT
loop teminato
aldo@Linux $ ./sigaction
Ucciso

Allora: l'esecuzione del programma non mostra nessun messaggio di errore visto che intercetta solo SIGINT. Il programma non fa nulla (entra nel loop pseudo-infinito di sleep(3)) e si risveglia quando arriva un segnale SIGINT: la prima esecuzione ho scritto "Ctrl + C" (riga 2 del test) e il programma è uscito fermando il loop e scrivendo il messaggio di ricezione. Nella successiva esecuzione ho usato, da un altro terminale (riga 5 del test), il comando kill(1) per inviare il segnale SIGINT: anche in questa esecuzione il programma mi ha mostrato il messaggio di ricezione ed è uscito fermando il loop. Infine ho eseguito di nuovo il programma e ho inviato un SIGKILL dal secondo terminale: il processo è stato ucciso e non ha scritto nessun messaggio (SIGKILL non si può intercettare).

Che ne dite? Per oggi può bastare, no? Come dite? L'articolo non vi è piaciuto?  Vabbè , continuiamo così, facciamoci del male....

Ciao, e al prossimo post!

sabato 26 febbraio 2022

The Last Keyword: _Generic, noreturn e static_assert
come usare le nuove keyword del C - pt.3

Pierre d'Alencon: La tua decenza sarà la tua fine.
Jacques Le Gris: E non c'è fine alla mia decenza.

Ebbene si: dopo una sofferta riflessione ho deciso di scrivere l'ultimo atto sulle (relativamente) nuove Keyword del nostro amato C. L'avevo promesso! E più volte! E quindi era diventata una questione d'onore, per cui come non citare un bel film che fa dell'onore il suo tema principale? The Last Duel è un altra gemma che si aggiunge alle altre che ci ha regalato il grandissimo Ridley Scott. Certo non è un film allegro, quindi non sono riuscito a trovare una citazione molto brillante... ma questo è un momento così, questi ultimi anni non sono stati esattamente un periodo per frasi brillanti. Ma noi programmatori siamo fiduciosi per natura! Speriamo bene...

...lotterò fino all'ultima keyword, lo giuro!...

Allora, riepiloghiamo: nella prima parte abbiamo parlato di complex e imaginary (citando di striscio anche bool, thread_local e atomic di cui avevamo già parlato in passato); nella seconda parte abbiamo parlato di alignas e alignof. Quindi, per completare l'opera, ci mancano solo _Generic, _Noreturn e _Static_assert, che sono state introdotte col C11 e che saranno l'argomento di questa terza (ed ultima) parte. Anche per queste keyword vale la solita regola descritta in questa auto-citazione:

...un po’ come è successo, per esempio, con le variabili booleane _Bool definite nell’header stdbool.h: questa norma del nome che inizia con undescore+maiuscola è seguita per tutte le nuove keyword del C, che possono, poi, essere usate con l’header corrispondente per “raddrizzare” il nome...

e, quindi, le nostre nuove keyword le useremo come noreturn e static_assert usando gli appositi header standard, meno _Generic che è l'eccezione che conferma la regola: non ha una macro di semplificazione (ce ne faremo una ragione...).

Ok, allora cominciamo proprio con _Generic: grazie a questa keyword si può usare una sorta di "programmazione generica" nel C, una possibilità che era anteriormente assente, e che ricorda un po' i meccanismi di overloading (e un po' anche quelli dei template) tipici del C++. La sintassi è la seguente:

_Generic(espressione_di_controllo, lista_di_associazione)

espressione_di_controllo: una espressione di assegnamento che rappresenta un tipo
lista_di_associazione: una lista di associazione con la seguente sintassi:

tipo: espressione, tipo: espressione, ..., default: espressione

Ok, ammettiamolo: questo dice poco, anzi dice che anche un linguaggio stabile, concreto e senza fronzoli come il C diventa astruso quando tenta di aggiungere qualche costrutto C++like (sarà il karma...). Ma forse se trasformiamo quanto sopra in un semplice esempio tutto sarà più chiaro:

#include <stdio.h>

// prototipi locali
int intAdd(int a, int b);
float floatAdd(float a, float b);
void errAdd();

// la mia funzione di addizione generica
#define genAdd(x, y) \
_Generic((x), int: intAdd(x, y), float: floatAdd(x, y), default: errAdd())

// main - funzione main
int main(int argc, char const *argv[])
{
// set variabili di test
int ia = 1;
int ib = 2;
float fa = 1.1;
float fb = 2.2;
double da = 1.3;
double db = 2.4;

// eseguo le somme generiche
genAdd(ia, ib);
genAdd(fa, fb);
genAdd(da, db);

return 0;
}

// intAdd - somma due int
int intAdd(int a, int b)
{
printf("%d + %d = %d\n", a, b, a + b);
return a + b;
}

// floatAdd - somma due float
float floatAdd(float a, float b)
{
printf("%.1f + %.1f = %.1f\n", a, b, a + b);
return a + b;
}

// errAdd - nessuna somma per i tipi passati
void errAdd()
{
printf("errore di tipo\n");
}

Si, così va meglio. Seguendo l'esempio si possono eseguire mille varianti per gli usi più svariati (e ricordarsi di rispettare la sintassi: default è opzionale, non si possono ripetere tipi, e un po' di altri dettagli che si trovano nel manuale). Io francamente non uso mai _Generic, ma usarlo non è una cattiva idea. Ah, il codice qui sopra produce il seguente risultato:

1 + 2 = 3
1.1 + 2.2 = 3.3
errore di tipo

E passiamo a noreturn. È una sorta di type specifier che vale solo per le funzioni, quindi è, a tutti gli effetti, un function specifier. La sintassi è questa:

_Noreturn dichiarazione_di_funzione

oppure, usando l'include apposito:

#include <stdnoreturn.h>
noreturn dichiarazione_di_funzione

e a che serve? Serve per dire che la funzione non avrà un ritorno. Questo non vuol dire che non ha un valore di ritorno, o meglio, che non ritorna nulla (per quello c'è già void!), ma vuol dire proprio che non ritorna! Ok, facciamo un esempio (anzi, due: crepi l'avarizia!): la system call _exit(2) e la funzione libc exit(3) sono due esempi ben calzanti di cosa vuol dire noreturn e non necessitano di ulteriori spiegazioni. E quindi quando si esegue una funzione di questo tipo il flusso del codice si ferma, e le successive istruzioni non vengono eseguite. Ok, forse vi sto confondendo le idee, meglio proseguire con un esempio:

#include <stdlib.h>
#include <stdio.h>
#include <stdnoreturn.h>

// prototipi locali
noreturn void okAbort();
noreturn void nokAbort();

// main() - funzione main
int main(void)
{
printf("sto per eseguire abort()...\n");
okAbort();
printf("questa scritta non sarà mai mostrata!\n");
}

// okAbort() - esegue "cose" e quindi un abort()
noreturn void okAbort()
{
// faccio cose
// ...

// chiamo la abort(3)
abort(); // ok: se scrivo codice dopo NON viene eseguito

// potrei scrivere codice (ma tanto non verrà mai eseguito!)
// ...
}

// nokAbort() - esegue "cose" e quindi un abort() condizionato
noreturn void nokAbort(int i)
{
// faccio cose
// ...

// chiamo la abort(3)
if (i > 0)
abort(); // nok: eseguo solo se i > 0

// se i <= 0 faccio cose ma non va bene: è una funzione noreturn!
// E quindi avro dei comportamenti non prevedibili (undefined behavior)
// e anche un bel Warning in compilazione!
// ...
}

Compilando ed eseguendo otterremo questo risultato:

sto per eseguire abort()...
Annullato (core dump creato)

Spero che l'esempio (con i soliti ampi commenti) abbia chiarito le idee... è necessario aggiungere, però, che noreturn è un po' come restrict (rileggere l'articolo, please) e anche un po' come volatile (altro articolo da rileggere): è uno specifier da usare con cautela, perché se usato male può dare comportamenti non prevedibili (undefined behavior). Con noreturn ci impegniamo con il compilatore a scrivere una funzione che farà quanto promesso: "non ritornare", e quindi il compilatore farà le opportune ottimizzazioni tenendo in conto questo. Ma se non manteniamo la promessa (è il caso di nokAbort() dell'esempio mostrato prima) avremo comportamenti imprevedibili, quindi occhio! Notare che nelle librerie di sistema le funzioni abort(3), exit(3) ecc. sono state ridefinite come noreturn (perché lo sono veramente!).

Dai, ci siamo quasi. Vedo la luce in fondo al tunnel: parliamo di static_assert e la finiamo qui.

Cosa è static_assert? È, come dice il nome, una assert statica (e grazie al...). Ok, spieghiamoci: sicuramente conoscete già la macro assert(3) che consente di eseguire dei controlli a run-time su delle istruzioni, abortendo l'esecuzione del programma se la asserzione fallisce. È abbastanza frequente l'uso in fase di debug per verificare cose tipo divisioni per 0, radici di numeri negativi, ecc. Ed è già predisposta per essere disabilitata per le versioni release dei programmi (uhm... io non uso quasi mai la assert(3), ma è una scelta molto personale, ammetto che è una macro molto utile). E la static_assert? È molto simile, ma funziona a compile-time invece che a run-time (e quindi è statica). La sintassi è:

_Static_assert(espressione, messaggio)

oppure, usando l'include apposito:

#include <assert.h>
static_assert(espressione, messaggio)

Inserendo delle static_assert "mirate" nel codice possiamo, già durante la compilazione, renderci conto se c'è qualcosa che non va. E a questo punto ci vuole un bell'esempio. Vai col codice!

#include <assert.h>

// main - funzione main
int main(int argc, char const *argv[])
{
// le mie asserzioni
static_assert(sizeof(int) == 4, "codice per sistemi con int da 4 bytes");
static_assert(sizeof(long) == 4, "codice per sistemi con long da 4 bytes");

// faccio cose
// ...

return 0;
}

Ecco, quello nell'esempio è un classico (ma non l'unico) uso della static_assert: quando si compila codice su varie piattaforme si possono aggiungere delle asserzioni in maniera che se la piattaforma non è compatibile già a livello di compilazione provochiamo uno stop in compilazione con relativo messaggio di errore: questo ci eviterà futuri grattacapi a run-time ("ma come, su quell'altra macchina funzionava benissimo!"). Ad esempio il codice appena mostrato, sulla mia macchina (Linux su amd64) produce questo durante la compilazione:

In file included from staticassert.c:1:
staticassert.c: In function ‘main’:
staticassert.c:8:5: error: static assertion failed: "codice per sistemi con long da 4 bytes"
8 | static_assert(sizeof(long) == 4, "codice per sistemi con long da 4 bytes");
| ^~~~~~~~~~~~~

la prima asserzione fila liscia come l'olio, mentre la seconda blocca la compilazione. Semplice, no?

E vabbè, dai, per oggi può bastare. Finalmente ho compiuto la mia vecchia promessa di ultimare l'argomento "nuove keyword" e ora mi sento veramente più leggero...

Ciao, e al prossimo post!