Dans le cadre du tutoriel « La gestion des erreurs en C », j'avais proposé de recoder un mécanisme de gestion d'exceptions à l'aide des sauts non-locaux. Ce sujet a ainsi pour but de recueillir vos solutions.
Caractéristiques communes :
− Syntaxe confortable (enfin, question de goût) car très proche du C++ : les mots-clés try, catch() (ces derniers commençant un bloc délimité par des accolades) et throw(), qui s’utilisent de la même manière qu’en C++. Les seules différences sont les parenthèses du throw() et l’inclusion du headertrycatch.h (et liaison à la bibliothèque).
Au fait, le paramètre de catch n’est pas forcément une déclaration, ça peut être le nom d’une variable déclarée précédemment (portée plus large).
− On ne peut lancer et attraper que des int (ou équivalents) non nuls.
− J’ai veillé à ne pas réserver de noms autres que « try », « catch » et « throw », pour ne pas perturber l’utilisation.
Avec une pile statique
Limitations :
− Les imbrications sont limitées (jusqu’à CATCH_STACKSZ fois).
− La pile statique prend une place en mémoire assez importante.
− Il doit impérativement y avoir un et un seul bloc catch pour chaque bloc try (sinon, plantage vu que l’on empile dans try et dépile dans throw). Ce n’est toutefois pas contraignant vu que ça correspond à l’utilisation normale, mais il faut le garder à l’esprit.
Je précise que ce code n’est pas testé !
#define CATCH_STACKSZ 16
/* pile globale : st est la pile, p est un pointeur sur l’élément actuel */
extern struct {
struct {
int code;
jmp_buf env;
} *p, st[CATCH_STACKSZ];
} catch;
/* catch.p est initialisé au 1er élément de catch.st - 1 (indique qu’on
n’est dans aucun bloc try). */
#define CATCH_ERROR(msg) \
fputs( \
"error: file `" #__FILE__ "`, line " #__LINE__ ", function `" \
#__func__ "`: " msg ".\n" , \
stderr)
/* version non sécurisée */
#define _try \
if( ++catch.p , !( catch.p->code = setjmp(catch.p->env) ) )
/* entrée dans un bloc try → on empile */
/* version sécurisée (attention les yeux) */
#define try \
if(catch.p == catch.st + CATCH_STACKSZ - 1) /* On est déjà au sommet de la pile. */ \
CATCH_ERROR("too much nested `try` blocks (a maximum of "
#CATCH_STACKSZ " is supported), skipping this one"); \
else _try
/* version non sécurisée */
#define _catch(ID) \
for(ID = (catch.p--)->code ; (catch.p+1)->code; (catch.p+1)->code = 0)
/* sortie d’un bloc try → on dépile */
/* version sécurisée */
#define catch(ID) \
if(catch.p == catch.st + CATCH_STACKSZ - 1) /* On est déjà au sommet de la pile. */ \
CATCH_ERROR("skipping corresponding `catch` block"); \
else _catch(ID)
/* version non sécurisée */
#define _throw(CODE) \
longjmp(catch.p->env, (CODE))
/* version sécurisée */
#define throw(CODE) \
if(catch.p == catch.st - 1) { /* On n’est dans aucun bloc try. */ \
CATCH_ERROR("throw not in a try block"); \
exit(CODE); /* Pourquoi pas ? */ \
} else _throw(CODE);
Avec une pile automatique
Avantages :
− comportement « parfait » (pas de limite d’imbrication).
− pile automatique : utilisation plus efficace de la mémoire (et plus rapde qu’avec de l’allocation dynamique). − code plus peaufiné et complet (j’ai fait un effort dessus, j’aurais pu le faire aussi pour le code précédent), dont des macros « bonus » : catchSwitch() (raccourci pour faire un switch sur l’exception capturée) et rethrow() (pour relancer une exception capturée, l’équivalent de throw sans rien en C++).
#ifndef INCLUDED_TRYCATCH_H
#define INCLUDED_TRYCATCH_H
/* Ce header nécessite C99. */
#if !defined (__STDC_VERSION__) || __STDC_VERSION__ < 199901L
#error : USE OF “TRYCATCH.H” REQUIRES C99.
#endif
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
/* type : pile d’environnements `jmp_buf` (utilisée dans la macro `try`) */
struct try {
struct try *prev;
jmp_buf env;
};
/* type : informations de gestion de la pile de jmp_buf et des exceptions */
struct catch {
struct try *envs; /* haut de la pile (NULL si pas dans un bloc `try`) */
int code; /* code numérique de l’exception (0 si sans objet) */
};
/* globale de gestion du système d’exceptions */
extern struct catch catch;
/* gestion des erreurs (message d’erreur détaillé) */
#define Throw(msg) \
fprintf(stderr, "error [file `%s`, line %u, function `%s`]: %s.\n", \
__FILE__, __LINE__, __func__, (msg))
/** TRY **/
/* Crée un élément de la pile de classe automatique (non nommé et local au bloc
`try`), le rajoute au sommet de la pile globale, puis appelle `setjmp` pour
définir le point de retour de `throw`.
Dépile à la fin. On doit dépiler dans le `for` car l’élément de pile lui est
local. Dans tous les cas, la boucle `for` ne doit pas se répéter.
Deux cas de figure :
− sortie normale du bloc `try` : cf la 3è partie du `for` pour dépiler, et la
1ère condition pour ne pas recommencer le bloc (utilise le comportement en
cours-circuit de &&) ;
− sortie avec un `throw` : cf le ternaire de la 2ème condition, pour dépiler
et arrêter le `for`. */
#define try \
for( catch.code = 1, catch.envs = & (struct try) { .prev = catch.envs } \
; catch.code && ( setjmp(catch.envs->env) \
? (catch.envs = catch.envs->prev, 0) \
: 1 ) \
; catch.code = 0, catch.envs = catch.envs->prev \
)
/** CATCH (+ catchSwitch) **/
/* Exécute le contenu du bloc s’il y a une exception (code non nul), en stockant
son code dans la variable de type `int` passée (qui peut être une déclaration,
auquel cas cette variable sera locale au bloc `catch`).
Un bloc `catch` n’aura d’effet que s’il est le premier après un bloc `try`
(remet le code d’exception à 0). */
#define catch(VAR) \
for(VAR = catch.code ; catch.code ; catch.code = 0)
/* Raccourci pour effectuer un `switch` sur le code de l’exception, sans nommer
une variable pour le stocker (s’utilise comme un `switch` normal). */
#define catchSwitch() \
for(; catch.code ; catch.code = 0) \
switch(catch.code)
/** THROW (+ rethrow) **/
/* « Lance » le code d’exception passé, qui doit être non nul. L’exécution du
programme revient grâce à `longjmp` au dernier bloc `try` englobant. Si aucun
bloc `try` n’englobe `throw`, quitte le programme avec le code lancé. */
#define throw(CODE) \
do { \
catch.code = (CODE); \
if(!catch.code) { \
Throw("throwing a null exception code"); \
exit(EXIT_FAILURE); \
} \
else if(!catch.envs) { \
Throw("`throw` outside of a `try` block"); \
exit(catch.code); \
} \
else \
longjmp(catch.envs->env, catch.code); \
} while(0)
/* « Relance » l’exception qui a été capturée (possible seulement dans un bloc
`catch`). */
#define rethrow() \
throw(catch.code)
#endif
On dit que les goto c'est moche car ça permet de sauter partout dans la fonction, mais alors que penser d'un goto qui saute a travers tout les .c d'un projet?
On dit que les goto c'est moche car ça permet de sauter partout dans la fonction, mais alors que penser d'un goto qui saute a travers tout les .c d'un projet?
J'aurais jamais compris ça perso :S
Bah pourtant tu en as l'utilité en plein devant les yeux... Les exceptions de C++ sont utiles, ça pourrait l'être également en C. Ça peut servir aussi en multi-threading, pour reproduire un mécanisme multitâche coopératif.
Maintenant, c'est sûr que pour une gestion des erreurs communes, c'est pas le top. Mais je pense que les débutants sont moins attirés par les sauts non-locaux, vu que ça reste quand même assez complexe à utiliser. Donc des codes spaghettis en setjmp, on en trouvera très peu.
On dit que les goto c'est moche car ça permet de sauter partout dans la fonction, mais alors que penser d'un goto qui saute a travers tout les .c d'un projet?
Ce n'est pas aussi anarchique que cela non plus. Cela ne permet pas de "sauter d'un .c à un autre", mais de revenir directement à une fonction appelante sans effectuer de multiples retour
Si ce topic est réellement destiné à être référencé par le cours de lucas-84, il serait peut-être bon de rajouter des détails sur les codes que l’on présente (avantages et limites comparées, fonctionnement général…).
Il faudrait peut-être aussi éditer le premier message pour introduire le sujet aux lecteurs qui débarquent.
Je viens de recevoir une alerte concernant le tutoriel, indiquant que la norme spécifie un comportement indéterminé lorsqu'on stocke le retour de setjmp dans une variable. J'ai vérifié dans la norme, et c'est effectivement le cas.
Citation
Environmental limits
4 An invocation of the setjmp macro shall appear only in one of the following contexts:
— the entire controlling expression of a selection or iteration statement;
— one operand of a relational or equality operator with the other operand an integer
constant expression, with the resulting expression being the entire controlling expression of a selection or iteration statement;
— the operand of a unary! operator with the resulting expression being the entire
controlling expression of a selection or iteration statement; or
— the entire expression of an expression statement (possibly cast tovoid).
5 If the invocation appears in any other context, the behavior is undefined.
Donc mon code et le premier code de Maëlan sont actuellement erronés.
Malheureusement, le résultat est quelque peu problématique...
En effet, dès lors que l'appel à erreurSeg() est effectué en-deçà du try (éventuellement à l'extérieur), le signal SIGSEGV n'a plus d'effet et le programme continue vaille que vaille...
Un appel en début de main(), en revanche, provoque bien l'arrêt du programme.
Si quelqu'un se sent d'en donner une explication...
[...]ça pourrait l'être également en C. Ça peut servir aussi en multi-threading, pour reproduire un mécanisme multitâche coopératif.
Ca m'intrigue. Je veux dire l'intérêt technique d'un goto et sur l'intérêt d'un système multi-thread aussi inutile que le coopératif.
D'ailleurs, je m'adresse à Taurre, mais j'ai eu l'occasion de discuter du pourquoi "processus léger" pour qualifier un thread et en fait cela vient tout simplement de la distinction entre processus lourd qui représente l'application globale, ou le processus du point d'entrée si tu veux, et les threads qui sont plus petits. D'autre part, ce sont des processus légers car ils sont peu coûteux, léger en somme.
Si quelqu'un se sent d'en donner une explication...
La variable pointeur n'est pas initialisée, donc le comportement de *pointeur=valeur*valeur; est indéfini.
Du coup, je doute que le problème vienne vraiment du code de Maëlan. Essaie avec int*pointeur=NULL; (devrait planter dans tous les cas)
La variable pointeur n'est pas initialisée, donc le comportement de *pointeur = valeur * valeur; est indéfini.
Effectivement, l'initialisation à NULL rend l'erreur systématique. Par ailleurs, j'ai pu reproduire l'effet de masquage avec un autre type de code (un appel à un logger en première instruction).
Cela suffit à dire que c'est bien l'init qui est à revoir et non le code.
Toutefois, bien qu'indéterminé, le masquage ne semble pas être aléatoire. Pour être précis, lorsque l'erreur surgit, elle le fait systématiquement et lorsqu'elle est masquée, c'est également systématique (i.e. indépendant du nombre de tests ou de compiles).
Il y aurait donc un effet de bord commun au try/catch et à mon logger (à moins qu'il faille augmenter le nombre de tests).
Du coup, à nouveau, si quelqu'un se sent de proposer une explication... parce qu'une erreur, certes c'est moche, mais si elle se prend à jouer à cache-cache, ça devient une horreur !
Présenté autrement, savoir ce qui peut arriver et pourquoi cela peut arriver en de tels cas d'indétermination en aidera plus d'un à s'arracher un peu moins de cheveux en phase de correction de code.
Au fait, dans cet article de Developpez.com (lien donné par Gugelhupf au café) sur les exceptions en C++, l’auteur présente les différentes implémentations possibles de ce mécanisme. Selon lui, il y en a deux types (partie V).
L’approche « à saut » consiste à sauvegarder le contexte en entrant dans un bloc try, et à le restaurer en cas d’exception. Ça correspond donc exactement à ce qu’on a fait ici, d’autant plus que l’auteur parle explicitement de setjmp/longjmp. Elle serait très performante lors du lancement d’une exception, mais entraînerait un surcoût important lors de l’entrée dans un bloc try (sauvegarde du contexte).
L’approche « sans coût » consiste à générer statiquement une table des points de remontée des exceptions (blocs catch) qui permettrait de retrouver le bloc catch auquel sauter en cas d’exception (d’après sa portée). Ce serait beaucoup plus lourd au lancement d’une exception, mais aussi beaucoup plus performant dans le cas général puisqu’il n’y a rien à faire.
Voilà, pour la culture. J’aller demander si ça tentait quelqu’un d’implémenter le 2e type, mais je viens de me rendre compte que ça ne va pas être trop possible, il faudrait travailler au niveau inférieur …