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.

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 Apicella: Cioè, 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!

domenica 23 gennaio 2022

Strptime: No Way Home
come si processano data e ora con la strptime in C (e C++)

Stephen Strange: Peter, a cosa devo il piacere?
Peter Parker: Mi dispiace disturbarla, signore.
Stephen Strange: Abbiamo salvato mezzo universo insieme, puoi evitare di chiamarmi signore.
Peter Parker: Stephen...
Stephen Strange: Suona strano ma te lo concedo.

Per la serie "non fare promesse se non puoi mantenerle", rinvio al prossimo articolo (ma davvero?) la conclusione dell'argomento in corso, quello delle nuove keyword del C. Il fatto è che a volte mi trovo con dei problemi reali da risolvere, problemi che sono sempre un buon spunto per la scrittura. Ad esempio, ultimamente mi sono scontrato con uno dei rompicapo "classici" della programmazione, la gestione di data/ora (da qui in avanti datetime) di una applicazione vera, con tutti gli annessi e connessi, come il formato e il timezone (uh, quest'ultimo ha provocato nel corso degli anni dei mostruosi mal di testa a innumerevoli colleghi, incluso il sottoscritto... maledetto timezone!).

Ebbene si, mi sono sentito un po' come il Peter Parker dell'ottimo Spider-Man: No Way Home, alle prese con i suoi soliti problemi esistenziali, di identità, di grandi responsabilità... perché una applicazione deve funzionare sempre bene (sono un perfezionista) e, quindi, deve girare bene a casa tua, nel tuo ufficio e, soprattutto, anche nel resto del mondo, e questo è vero solo quando i datetime sono stati gestiti come si deve. Per questo ci sto sempre attento, e non aspetto che si presenti l'errore per risolverlo: "prevenire è meglio che curare" (e "non ci sono più le mezze stagioni", e "si stava meglio quando si stava peggio", e..., non facciamoci mai mancare i luoghi comuni).

...preparati a non sapere più neanche che giorno è oggi...

E veniamo al dunque: capita molto frequentemente di dover processare un datetime che ti è arrivato, in formato "human readable",  attraverso i più svariati canali (dall'interfaccia utente, dalla rete, ecc.). Fortunatamente i comitati ISO si sono occupati anche dei formati delle date (se ne occupano, normalmente, tra una nuova versione del C++ e la successiva, ah ah ah), e quindi possiamo usare, ad esempio, lo standard ISO 8601, per essere sicuri di riuscire a interpretare (quasi) qualsiasi formato (ma non vi preoccupate: ci sarà sempre qualcuno che non ama gli standard e vi proporrà qualche formato "custom" super-complicato, tanto per complicare anche la vostra vita). Ovviamente, quando ci arriva un datetime dobbiamo trasformarlo da "human readable" a "machine readable", per poterlo poi trattare nel codice.

Un metodo abbastanza classico e di uso quasi universale (per fortuna) è trasformarlo in una struttura dati di tipo struct tm che contiene tutti i campi che ci necessitano per processare il datetime in una applicazione (e/o usare direttamente il tempo di riferimento dei sistemi POSIX, il numero di secondi passati da Epoch, ma questa è un altra storia).

Un modo tipico di processare il datetime è quello di usare la funzione standard della libc strptime(3) (nonché la sua versione speculare, la strftime(3)). E, a questo punto, direi di vedere direttamente un esempio per chiarire meglio il meccanismo: vai col codice!

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

// main() - funzione main
int main(void)
{
// decodifico il datetime con strptime(3)
struct tm tm;
memset(&tm, 0, sizeof(struct tm));
strptime("2022-01-21 14:29:44", "%Y-%m-%d %H:%M:%S", &tm);
printf("il datetime sorgente è: 2022-01-21 14:29:44\n");

// mostro i campi della struct tm
printf("\nla struttura tm corrispondente è:\n");
printf("tm_sec = %d\n", tm.tm_sec); /* Seconds (0-60) */
printf("tm_min = %d\n", tm.tm_min); /* Minutes (0-59) */
printf("tm_hour = %d\n", tm.tm_hour); /* Hours (0-23) */
printf("tm_mday = %d\n", tm.tm_mday); /* Day of the month (1-31) */
printf("tm_mon = %d\n", tm.tm_mon); /* Month (0-11) */
printf("tm_year = %d\n", tm.tm_year); /* Year - 1900 */
printf("tm_wday = %d\n", tm.tm_wday); /* Day of the week (0-6, Sunday = 0) */
printf("tm_yday = %d\n", tm.tm_yday); /* Day in the year (0-365, 1 Jan = 0) */
printf("tm_isdst = %d\n", tm.tm_isdst); /* Daylight saving time */

// ricostruisco il datetime con strftime(3)
char buf[256];
strftime(buf, sizeof(buf), "%d %b %Y %H:%M", &tm);
printf("\nil datetime ricostruito è: %s\n", buf);

exit(EXIT_SUCCESS);
}

E questo codice, una volta compilato ed eseguito, da il seguente risultato:

il datetime sorgente è: 2022-01-21 14:29:44

la struttura tm corrispondente è:
tm_sec = 44
tm_min = 29
tm_hour = 14
tm_mday = 21
tm_mon = 0
tm_year = 122
tm_wday = 5
tm_yday = 20
tm_isdst = 0

il datetime ricostruito è: 21 Jan 2022 14:29

Tutto questo è molto semplice: si processa la data "human readable" in formato ISO 8601 con strptime(3), si ottiene una struct tm corrispondente e, passando questa struttura a strftime(3), si può riformattare il datetime (anche con un formato diverso, se necessario). Inutile ricordare che strptime(3) appartiene a una famiglia di funzioni che fanno capo a time(2), con cui si possono fare un sacco di cose interessanti (ma, magari, ne parleremo un altra volta).

E adesso veniamo al problema reale che mi ha ispirato questo articolo: la strptime(3) è una notevole funzione, ma alcuni formati non li digerisce benissimo, e lo standard ISO 8601 (ahimè) ne prevede un sacco. Il caso con cui mi sono scontrato è un datetime come il seguente che mi arrivava contenuto in un messaggio da processare:

2022-01-21T14:29:44.278Z

questo datetime è corretto da un punto di vista formale, anche se è poco usuale visto che contiene i millisecondi e, a causa di questo, il trattamento con strptime(3) a volte funziona e a volte no. Questo non è, ovviamente, un comportamento accettabile, e una possibile soluzione (sempre che proprio non vi servano anche i millisecondi) potrebbe essere quella di non usare strptime(3) e di usare, invece, scanf(3). Vediamo come:

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

// prototipi locali
void myStrptimeScanf(const char *s, const char *format, struct tm *tm);

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

// decodifico e ricostruisco il datetime con strptime(3) e strftime(3)
//

datetime = "2022-01-21T14:29:44.278Z";
myStrptimeScanf(datetime, "%Y-%m-%dT%H:%M:%S%z", &tm);
strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm);
printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf);

exit(EXIT_SUCCESS);
}

// myStrptimeScanf() - funzione wrapper per strptime(3)
void myStrptimeScanf(
const char *s, // datetime sorgente
const char *format, // formato del datetime sorgente
struct tm *tm) // struct tm destinazione
{
memset(tm, 0, sizeof(struct tm));
int year, mon, mday, hour, min;
float sec;
sscanf(s, "%d-%d-%dT%d:%d:%fZ", &year, &mon, &mday, &hour, &min, &sec);
tm->tm_year = year - 1900; // anno da 1900
tm->tm_mon = mon - 1; // 0-11
tm->tm_mday = mday; // 1-31
tm->tm_hour = hour; // 0-23
tm->tm_min = min; // 0-59
tm->tm_sec = (int)sec; // 0-60
}

E questo codice, una volta compilato ed eseguito, da il seguente risultato:

datetime: 2022-01-21T14:29:44.278Z - datetime ricostruito: 21 Jan 2022 14:29:44+0000

Questo metodo funziona ma non è molto flessibile, visto che il tipo di data accettato è solo quello dell'esempio, quindi dovremmo riscrivere la funzione per ogni tipo di datetime che ci può arrivare (e lo standard, come detto sopra, ne prevede molti). Tra l'altro il datetime proposto pur essendo abbastanza tipico (a parte i millisecondi) potrebbe a sua volta avere della varianti come queste:

2022-01-21T14:29:44Z
2022-01-21T14:29:44.278Z
2022-01-21T14:29:44+0100
2022-01-21T14:29:44+01.00
2022-01-21T14:29:44.278+0100
2022-01-21T14:29:44.278+01.00

dove ha un ruolo importante anche il maledetto timezone, che in questo caso è UTC (e ricordate: usare UTC è sempre una buona idea, può risparmiarvi un sacco di problemi).

Che fare, allora? La soluzione migliore è, direi, continuare a usare la strptime(3), che è molto flessibile (basta giocare con l'argomento <format>) e poi gestisce bene gli errori (cosa che non fa il codice con la scanf(3) mostrato qui sopra): "ma chi ce lo fa fare di riscrivere tutta la gestione degli errori per formati e/o valori sbagliati se lo fa già benissimo la strptime(3)? ". Il trucco da usare è molto semplice: si può aggiustare  il datetime prima di passarlo alla strptime(3). E quindi, ripeto, se il nostro problema è solo quello dei millisecondi (che, dopo una rapida ricerca in rete, ho scoperto che è un problema non infrequente) possiamo operare nella seguente maniera:

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

// prototipi locali
char *myStrptime(const char *s, const char *format, struct tm *tm, size_t len);

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

// decodifico e ricostruisco il datetime con strptime(3) e strftime(3)
//

datetime = "2022-01-21T14:29:44.278Z";
myStrptime(datetime, "%Y-%m-%dT%H:%M:%S%z", &tm, 19);
strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm);
printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf);

datetime = "2022-01-21T14:29:44+0100";
myStrptime(datetime, "%Y-%m-%dT%H:%M:%S%z", &tm, 19);
strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm);
printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf);

datetime = "2022-01-21T14:29:44+01.00";
myStrptime(datetime, "%Y-%m-%dT%H:%M:%S%z", &tm, 19);
strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm);
printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf);

datetime = "2022-01-21T14:29:44.278Z";
myStrptime(datetime, "%Y-%m-%dT%H:%M:%S%z", &tm, 19);
strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm);
printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf);

datetime = "2022-01-21T14:29:44.278+0100";
myStrptime(datetime, "%Y-%m-%dT%H:%M:%S%z", &tm, 19);
strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm);
printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf);

datetime = "2022-01-21T14:29:44.278+01.00";
myStrptime(datetime, "%Y-%m-%dT%H:%M:%S%z", &tm, 19);
strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm);
printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf);

exit(EXIT_SUCCESS);
}

// myStrptime() - funzione wrapper per strptime(3)
char *myStrptime(
const char *s, // datetime sorgente
const char *format, // formato del datetime sorgente
struct tm *tm, // struct tm destinazione
size_t len) // lunchezza della parte data+ora+secondi
{
// estraggo la parte data+ora+secondi
char part_one[32];
memcpy(part_one, s, len);
part_one[len] = 0;

// estraggo la seconda parte
char part_two[32];
snprintf(part_two, sizeof(part_two), "%s", &s[strlen(part_one)]);

// ricompongo le due parti e applico strptime(3)
memset(tm, 0, sizeof(struct tm));
char mys[32];
char *mytimezone;
if ((mytimezone = strchr(part_two, 'Z')) != NULL) {
// caso speciale con Z finale
snprintf(mys, sizeof(mys), "%s%s", part_one, mytimezone);
return strptime(mys, format, tm);
}
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);
return strptime(mys, format, tm);
}

// caso non riconosciuto: ritorna errore
return NULL;
}

E questo codice, una volta compilato ed eseguito, da il seguente risultato:

datetime: 2022-01-21T14:29:44.278Z - datetime ricostruito: 21 Jan 2022 14:29:44+0000
datetime: 2022-01-21T14:29:44+0100 - datetime ricostruito: 21 Jan 2022 14:29:44+0100
datetime: 2022-01-21T14:29:44+01.00 - datetime ricostruito: 21 Jan 2022 14:29:44+0100
datetime: 2022-01-21T14:29:44.278Z - datetime ricostruito: 21 Jan 2022 14:29:44+0000
datetime: 2022-01-21T14:29:44.278+0100 - datetime ricostruito: 21 Jan 2022 14:29:44+0100
datetime: 2022-01-21T14:29:44.278+01.00 - datetime ricostruito: 21 Jan 2022 14:29:44+0100

Come avrete notato ho spezzato il datetime sorgente in due parti: la prima contiene data+ora+secondi, e può avere qualsiasi formato, tanto poi useremo la strptime(3) giocando con l'argomento <format>. Per rendere più generica la funzione ho aggiunto un nuovo argomento con la lunghezza della prima parte, così si può accettare (quasi) qualsiasi datetime. La seconda parte può contenere o non contenere i millisecondi (e se li contiene li tagliamo via, tanto nella struct tm non si possono mettere), e il timezone si processa con l'apposito formato "%z" che va bene per tutte le varianti mostrate sopra. Una volta ricomposta la stringa datetime sorgente possiamo chiamare la strptime(3) come se nulla fosse: una soluzione semplice per un problema solo apparentemente complesso (e ricordate: la soluzione più semplice è sempre la migliore, o non conoscete il Rasoio di Occam?).

Ok, per oggi può bastare, e prometto che ritorneremo (nel prossimo articolo) con l'argomento che avevamo lasciato pendente, quello delle nuove keyword del C (e se andate a controllare ho copiato/incollato esattamente la stessa promessa dello scorso articolo... la manterrò questa volta? ah ah ah).

Ciao, e al prossimo post!

P.S.

Questo articolo contiene (spero) argomenti e spiegazioni interessanti. Ma la funzione finale proposta, la myStrptime(), 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). Che fare allora? Correggere o modificare questo articolo 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”, che potete trovare qui. Ciao di nuovo!