Gestione Delle Eccezioni in C

Gestione degli errori

È comune per un programma gestire situazioni non standard, come cose che non funzionano a dovere, gestione dei timeout, o qualunque altra cosa che non fa parte del "normale" flusso di codice. È tipico in C (come per ogni linguaggio abbastanza anziano) gestire queste situazioni con codici di errore, possibilmente settando errno (#include ), e fornendo un array di stringhe costanti che descrivano l'errore (sys_errlist[], sempre in ). Così tipicamente la gestione degli errori in C richiede il controllo del valore ritornato, opzionalmente il controllo della variabile errno, e l'esecuzione del codice opportuno per la gestione dell'errore. Al contrario in C++ (e in tutti i linguaggi orientati agli oggetti) c'è il concetto delle eccezioni, cioè, quando qualcosa di inaspettato succede l'errore viene riportato al blocco try{..}catch che contiene la chiamata che ha generato l'errore, e viene gestito nel blocco che segue il catch. Catch instanzia un oggetto che contiene le informazioni relative all'errore (in realtà l'oggetto punta all'eccezione instanziata dalla chiamata che ha generato l'eccezione, e solo quando il tipo dichiarato in catch è un supertipo per l'eccezione instanziata, altrimenti si salta al try{..}catch superiore). Tutto questo richiede un supporto a runtime che ha guidato, ad esempio, gli sviluppatori del Symbian OS (aka EPOC32) nella scelta di fare a meno delle eccezioni all'interno del framework in C++ nel nome delle performance e della tolleranza agli errori di allocazioni (vedere il meccanismo TRAPD/Leave). La programmazione orientata agli oggetti non è strettamente legata ai linguaggi orientati agli oggetti, essa è piuttosto un modo di organizzare il codice usando i concetti tipici del modello ad oggetti (ereditarietà,polimorfismo, incapsulamento). Un buon esempio di libreria organizzata secondo il modello ad oggetti e scritta in puro linguaggio C è il GIMP Tool Kit. Mentre scrivo e progetto un programma in C++, Java, Python personalmente tendo a considerare il concetto delle eccezioni una parte integrante del modello ad oggetti, e questo è effettivamente un concetto mancante nella maggior parte delle librerie C implementate secondo il modello ad oggetti. Le funzioni setjmp() e longjmp() Questo articolo descrive cosa dovrebbe/potrebbe succedere in un _C_aso eccezionale. Il linguaggio C non ha un supporto runtime per la gestione delle eccezioni integrato nel linguaggio (come il try{..}catch del C++), non esistono classi e gerarchie (ovviamente), ma ci sono un paio di funzioni simpatiche, setjmp() e longjmp(), che si comportano in maniera simile al try{..}catch ... #include #include int foo(int p) { if (p) siglongjmp(env,p); /* return to sigstejmp returning p */ return 0; } static sigjmp_buf env; int main() { int a,r; if (!(r=sigsetjmp(env, 1))) { for (a=0; a<10; a++) { fprintf(stdout,"%d\n",foo(a)); fflush(stdout); } } else { fprintf(stdout,"exceptionally returned %d",r); fflush(stdout); } } sigsetjmp e siglongjmp sono varianti conformi al posix e compatibili con lo standard adottato da BSD per setjmp/longjmp (vedere la documentazione di GNU Libc) ... sì, assomiglia al try{..}catch(..) in C++ a parte la mancanza dell'argomento per il catch, e che c'e' solo un livello di eccezione (i blocchi try non possono essere annidati) Un altro esempio di longjmp #include #include static sigjmp_buf env; static sigjmp_buf env1; int bar(int a) { fprintf(stdout, "bar a:%d\n", a); if (a <10) siglongjmp(env1,a); } int foo(int p) { int r; if (! (r=sigsetjmp(env1,1))) { fprintf(stdout, "foo = %d\n",bar(p+1)); fflush(stdout); } else { fprintf(stdout,"some exception occurred %d\n", r); fflush(stdout); siglongjmp(env,r); /* try with this or without */ } if (f(p)) { fprintf(stdout, "well, done"); fflush(stdout); } return p; } int f(int p) { if (p>=8) siglongjmp(env,p); if (!(p%2)) return 1; return 0; } int main() { int a; int r; if (! (r=sigsetjmp(env,1))) { for (a=0; a<10; a++) { fprintf(stdout, "foo = %d\n",foo(a)); fflush(stdout); } } else { fprintf(stdout,"some exception occurred %d\n", r); fflush(stdout); } } Qui f() "genera un eccezione" che è gestita direttamente dal "try" in main(), in altre parole longjmp'a direttamente al setjmp di main (bypassando la funzione chiamante). Questo non fa parte del concetto standard di eccezione ed in effetti rende il codice poco leggibile, ed è la causa principale dell'assenza di longjmp dalla maggior parte del buon codice C. Ma un analisi più attenta della posizione in cui f() è chiamata rende chiaro che un salto alla funzione chiamante dovrebbe necessariamente puntare al blocco if (! (r=sigsetjmp(env1,1))) { ..} di foo() il che è chiaramente un errore. In realtà quello che accade è quello che accadrebbe in C++ quando viene chiamato throw: si ritorna al blocco try immediatamente superiore. È chiaro che setjmp/longjmp non rendono la vita molto facile nè a chi vuole leggere il codice nè a chi lo scrive Ma se invece di gestire direttamente la posizione dove si deve tornare (e quindi capire qual'è il blocco contenente il setjmp giusto), utiliziamo alcune funzioni di utilità che rendono tutto questo trasparente, lo scenario potrebbe cambiare e setjmp/longjmp risultare molto più pratiche e leggibili di quanto non ci si aspetti. Implementazione di uno stack di eccezioni Per implementare l'idioma delle eccezioni in C sono necessari: uno stack e una qualche struttura per l'eccezione. Nell'intento di capire meglio come le cose dovrebbero funzionare il codi è meglio di qualunque diagramma: #include .. int test_excp() { jmp_buf *my_env; exc_struct *my_excp; int r; my_excp=new_excp(); my_env = new_env(); if (!(r=setjmp(my_env))) { /* try */ a_throwing_func(); /* other code here * .. * if at the end all go right free stack and continue */ free_env(my_env); } else { /* catch */ switch (r) { /* given that type of exception be passed this way */ case 1: /* 1 is trapped */ excp_stru *l_excp; /* I want l_excp point to the returned exception * and also all allocated jmp_buf cleared until * the current one my_env */ l_excp = compact_excp(my_env); /* some management code */ manage(l_excp); break; default: /* throw anything else */ /* just does longjmp to parent level * (the parent level do compact exceptions and env) */ throw_up(my_env); } } free_env(my_env); /* in case of uncatched just throw (with parent env) */ throw_excp(gen_excp, "error"); } Bene, il codice non è così orribile. Ma c'è una considerazione rilevante: in ogni momento c'e' bisogno di una sola struttura che contenga i dati dell'eccezione. Questa regola è vera anche in C++, e in Java, e in ogni linguaggio che ha un runtime delle eccezioni. Quindi basta una sola variabile globale exception del tipo più generico di eccezione, inizializzata all'avvio del programma. Visto il codice sopra, le funzioni di utilità sono: * new_env() restituisce un new *jmp_buf * free_env(jmp_buf *env) libera env_stack fino a env (incluso) * compact_excp(jmp_buf *) libera env_stack fino a env (escluso) * throw_up(my_env) longjmp'a al jmp_buf subito sotto my_env nello stack env_stack ed esegue un pop su env_stack * throw_excp( void(*excp_fill) (struct excp_struct *, va_list ap), ... ) riempie exception (chiamando excp_fill con i parametri exception e va_arg passati), poi esegue un longjmp alla posizione i testa ad env_stack. Ora, quello che manca sono i tipi di dato: typedef struct { int errno; int excp_type; void *excp_data; } exc_struct; static jmp_buf *env_stack; excp_struct * exception int enstack_sz; int enstack_maxsz; [enstack(_sz|_max) and env_stack are unpacked, all could be packed, as the interface aims to be transparent] Problemi di performance Sembra che ci siamo alcuni problemi di performance nel esempio sopra: free_env(jmp_buf *env), compact_excp(jmp_buf *), throw_up(jmp_buf *) tutte richiedono di scorrere lo stack per cercare la giusta posizione dove deve essere tagliato. Effettivamente, guardando l'esempio la struttura jmp_buf è utilizzata direttamente solo al momento del setjmp e da nessun altra parte (e così deve essere se la gestione è trasparente). La soluzione naturale è usare un indice (l'indice in env_stack del jmp_buf corrente) da passare alle tre funzioni, più una terza funzione che ritorna il jmp_buf giusto. Analogamente la struttura exc_struct, che come detto è sempre la variabile exception non dovrebbe essere direttamente visibile. In realtà la strana funzione throw_excp( void(*excp_fill) (struct excp_struct *, va_list ap), ... ) e manage(l_excp) possono essere rimpiazzate con (usando un modello semplificato ad oggetti) con varie XXX_excp_fun() (con XXX il nome dell'eccezione) che modificano la variabile globale exception e throw_excp(env) che esegue il longjmp. Così new_excp() non serve e new_env() deve semplicemente ritornare un indice di tipo int, tutte le funzioni possono avere come argomento questo indice, ma non appare mai l'uso esplicito dei tipi di dato jmp_buf e excp_struct (si chiama incapsulamento se ricordo bene). Così il codice d'esempio test_excp() diventa: #include .. int test_excp() { env_p env; int r; env = new_env(); if (!(r=setjmp(get_env(env)))) { /* try */ a_throwing_func(); } else { // catch switch (r) { /* give that type of exception be passed this way */ case 1: /* 1 is trapped */ compact_excp(env); /* some management code */ fprintf(stdout,"%s",gen_excp_get_msg()); fflush(stdout); break; default: /* throw anything else */ /* just does longjmp to parent level * (the parent level do compact exceptions and env) */ throw_up(env); } } free_env(env); /* in case of uncatched just throw (with parent env) */ gen_excp("error"); throw_excp(); } Le funzioni utility Visto le ultime considerazioni le fuzioni sono: * new_env() restituisce env_p (con significato env pointer: typedef env_p int) * free_env(env_p env) libera env_stack fino a env (incluso) * compact_excp(env_p env) libera env_stack fino a env (escluso) * throw_up(env_p env) longjmp al jmp_buf sotto env in env_stack (env-1 diventa l'indice della testa dello stack) * throw_excp() longjmp al jmp_buf in testa allo stack Inoltre, per ogni tipo di eccezione che si andrà a definire servono le funzioni: * xxx_excp(....) che inizializza l'eccezione attuale con il valori .... (sono quattro punti, non è un variadic argument) * xxx_excp_methodX() vari metodi per gestire l'eccezione (get, set ..) (può controllare il tipo di eccezione, ed eventualmente riportare l'errore) L'idea è di salvare il tipo di eccezione nella struttura e di utilizzarla come parametro per longjmp così da poter implementare una gestione simile a quella del C++. Ora la definizione delle funzioni utility: typedef env_p int; env_p new_env() { /* (re)allocate env_stack if needed */ enstack_sz ++; if (enstack_sz > enstack_maxsz) { enstack_maxsz = enstack_sz; env_stack = (jmp_buf *) realloc(env_stack, enstack_sz * sizeof(jmp_buf)); } return (enstack_sz - 1); } #define free_env(env) enstack_sz=env #define compact_excp(env) enstack_sz=env+1 #define get_env(env) env_stack[env] inline void throw_up(env_p env) { longjmp(enstack[env-1], exception->excp_type); } inline void throw_excp() { longjmp(env_stack[enstack_sz - 1], exception->excp_type); } void gen_excp(const char * str) { exception->excp_type = 1; exception->excp_data = str; } const char * gen_excp_get_msg() { return (const char *) exception->excp_data; } Ho usato la keyword inline definita nello standard C99 perché non posso usare una macro che contenga longjmp e voglio comunque nascondere la variabile exception dall'interfaccia Ho seguito la regola dell'uomo stanco: "non fare niente che non sia strettamente necessario". [la regola completa prosegue con "E se proprio devi farlo, cerca di farlo fare a qualcun altro"] Un esempio completo #include #include /* data type */ typedef struct { int errno; int excp_type; void *excp_data; } exc_struct; static jmp_buf *env_stack; exc_struct * exception; int enstack_sz; int enstack_maxsz; typedef env_p int; void init_excp() { exception = (exc_struct*) malloc(sizeof(exc_struct)); enstack_sz=0; enstack_maxsz=0; env_stack=NULL; } void clean_excp() { if (exception!=NULL) { free(exception); exception=NULL; } if (env_stack!=NULL) { free(env_stack); enstack_sz=0; enstack_maxsz=0; env_stack=NULL; } } /* utility funcs */ env_p new_env() { // (re)allocate env_stack if needed enstack_sz ++; if (enstack_sz > enstack_maxsz) { enstack_maxsz = enstack_sz; env_stack = (jmp_buf *) realloc(env_stack, enstack_sz * sizeof(jmp_buf)); } return (enstack_sz - 1); } #define free_env(env) enstack_sz=env #define compact_excp(env) enstack_sz=env+1 #define get_env(env) env_stack[env] inline void throw_up(env_p env) { longjmp(enstack[env-1], exception->excp_type); } inline void throw_excp() { longjmp(env_stack[enstack_sz - 1], exception->excp_type); } void gen_excp(const char * str) { exception->excp_type = 1; exception->excp_data = str; } void gen2_excp(const char * str) { // assert(exstack_sz == enstack_sz); exception->excp_type = 2; exception->excp_data = (void *) str; } const char * gen_excp_get_msg() { return (const char *) exception->excp_data; } // the test code void better_sith(int a) { /* do something ... * .. * .. if the case throw */ if (a%2) { gen_excp("(I am better_sith) it is not odd"); throw_excp(); } if (a==2) { gen2_excp("(I am better_sith) I do not like the number 2"); throw_excp(); } } int dosome_sith(int a) { if (a<8) { env_p env; int r; // this function's personal exception catch env=new_env(); if (!(r=setjmp(get_env(env)))) { /* try */ better_sith(a); } else { if (r == 1) { /* what to catch: 1 gen_ exception */ compact_excp(env); fprintf(stdout, "dosome_sith::exception message: %s\n",gen_excp_get_msg()); fflush(stdout); } else { /* throw everything else */ throw_up(env); } } free_env(env); /* gen_excp("I'm bored, I want return"); throw_excp(); */ /* this works too */ } else { gen_excp( "(I am dosome_sith) all wrong guy, you should not give me such values"); throw_excp(); } } int main() { int a; int r; env_p env; init_excp(); fprintf(stdout,"INIT PHASE\n"); fflush(stdout); env = new_env(); if (!setjmp(get_env(env))) { for (a=0;a<=10; a++) { fprintf(stdout,"main:: I try with %d\n",a); fflush(stdout); dosome_sith(a); } } else { compact_excp(env); fprintf(stdout,"main:: catched exception message: %s\n", gen_excp_get_msg()); fflush(stdout); } free_env(env); clean_excp(); } Se si escludono le funzioni di utility, startup e cleanup, non è necessario usare puntatori che possono rendere il codice poco leggibile, e il tutto è realizzato con meno di cento righe di codice C. Guardando meglio c'è qualcosa che può essere aggiustato. La prima è: cosa succede se chiamo throw_excp() quando non ci sono eccezioni nello stack? Questa situazione in C++ causa la brutale terminazione del programma, in Java il compilatore rifiuta il codice. Qui si può pensare di aggiungere un assert prima di chiamare longjmp. Il comportamento sarebbe diverso da quello del C++ perché il messaggio di errore in caso di terminazione brutale per un eccezione non gestita nel C++ è più informativo di qualcosa tipo "throw_excp() è stata chiamata fuori da un un blocco setjmp", cioè non ho informazioni riguardo alla posizione. La seconda questione è: perché saltare ad un blocco setjmp che non gestisce il tipo di eccezione appena generato? Questo può essere risolto usando una bitmap di eccezioni ... ma il codice sarebbe molto più costoso che non due o tre longjmp (se il compilatore supporta la keyword inline). Entrambe i problemi potrebbero essere risolti aggiungendo una fase iniziale, alla maniera di un noto metodo usato per interfacciarsi ai DBMS: estenzione del linguaggio. Storicamente questo è il metodo di interfacciamento a DBMS più odioso e insopportabile. Fortunatamente non è strettamente necessario usarlo, ed evito accuratamente di immaginarlo. Qualche conclusione Ricordando l'inizio di questo articolo, dal punto di vista di una libreria orientata aglio oggetti scritta in C non ci sono problemi di efficenza nell'uso di longjmp al posto dell'utilizzo di codici di errore, inoltre con un certo numero di tipi di eccezioni utilizzare una tale libreria potrebbe essere molto comodo e pratico (come visto non c'è bisogno di usare puntatori fuori dalla definizione delle eccezioni), e, dopo avere scritto le parti mancanti (per esempio l'eventuale settaggio di errno), si può comunque integrare il tutto alla libreria C standard. Il framework Symbian OS C++ non supporta le eccezioni per motivi di efficenza, e, soprattutto, per problemi di gestione della memoria in casi eccezionali, ma usando il modo presentato qui per la gestione delle eccezioni e aggiungendo un cleanup stack (alla maniera di Symbian), usando obstacks o qualunque altra cosa risulti pratica allo scopo, questo idioma (l'utilizzo di setjmp/longjmp attraverso funzioni di utilità) risulta essere più versatile e robusto di quello adottato da Symbian. La storia non finisce qui. In realtà ci sono dei problemi che sorgono quando si usa setjmp/longjmp, per esempio come questi interagiscono con i thread POSIX (solo per dare un'idea, ogni thread deve avere la sua eccezione, e mascheramento dei segnali, e via dicendo). Integrare questo idioma in una libreria completa (e sufficientemente complessa) potrebbe non essere così facile. Riferimenti Le funzioni setjmp() e longjmp() sono definite nello standard POSIX e single unix, inoltre la documentazione è nelle rispettive pagine di man (sezione 3), e, in vari formati, nella documentazione della GNU Libc (formati cartacei possono essere acquistati dal sito www.gnu.org) Symbian OS è documentato nel proprio sito, la documentazione è distribuita come zip di file html, disponibili online, e distribuita inoltre dai rivenditori symbian (Nokia, Siemens, Sony-Ericsson, Motorola, etc.) in vari formati