Partage
  • Partager sur Facebook
  • Partager sur Twitter

Reproduction d'un mécanisme d'exception

Venez partager votre solution !

28 avril 2012 à 10:50:44

Bonjour,

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.

Bonne continuation !
  • Partager sur Facebook
  • Partager sur Twitter
Staff désormais retraité.
28 avril 2012 à 11:01:30

Salut,

Je reposte donc mon dernier code en date:


#ifndef EXCEPTION_H
#define EXCEPTION_H

#include <setjmp.h>

#define TRY for (es->next = &(struct exception) { .next = es->next } \
, es->num = 1; es->num && !setjmp(es->next->buf) \
; es->next = es->next->next, es->num = 0)
#define CATCH(NUM) for (; es->num == (NUM); es->num = 0)
#define FINALLY for (; es->num; es->num = 0)
#define THROW(NUM) for (jmp_buf *buf = &es->next->buf \
; (es->next = es->next->next, es->num = (NUM)); longjmp(*buf, es->num))

extern struct exception {
	jmp_buf buf;
	int num;
	struct exception *next;
} *es;

#endif /* EXCEPTION_H */




#include <stddef.h>

#include "exception.h"

struct exception *es = &(struct exception) { .next = NULL };




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

#include "exception.h"

enum {
	TEST1 = 1, TEST2 = 2, TEST3 = 3
};


static void
test3(void)
{
	THROW(TEST3);
}


static void
test2(void)
{
	THROW(TEST2);
}


static void
test1(void)
{
	TRY {
		test2();
	} CATCH (TEST2) {
		puts("TEST2");
		THROW(TEST1);
	} 
}


int
main(void)
{
	TRY {
		test1();
	} CATCH (TEST1) {
		puts("TEST1");
	}

	TRY {
		;
	}

	TRY {
		test3();
	} FINALLY {
		puts("Exception!");
	} 

	return EXIT_SUCCESS;
}

  • Partager sur Facebook
  • Partager sur Twitter
28 avril 2012 à 13:21:45

Et moi le mien. :)

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 header trycatch.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

#include "trycatch.h"

struct catch  catch =  {NULL, 0};



Test



#include "trycatch.h"
#include <stdio.h>



typedef  enum {EXC_Z, EXC_NOTRY, EXC_42, EXC_DIVBYZ, EXC_ORDER}  Exception;

int division(int a, int b) {
	if(!a)
		throw(EXC_Z);        /* doit provoquer une erreur (code nul) */
	else if(a==-1)
		throw(EXC_NOTRY);    /* doit provoquer une erreur (throw sans try) */
	else if(b==42)
		throw(EXC_42);       /* exception relancée avec rethrow() */
	else if(!b)
		throw(EXC_DIVBYZ);     /* exception gérée dans le catchSwitch() */
	else if(a<b)
		throw(EXC_ORDER);      /* idem */
	return a / b;
}

int main(void) {
	int a,  b;

	while(1) {
		fputs("a, b? ", stdout);
		scanf("%i%i", &a, &b);

		try {
		
			try {
				printf("%i / %i  =  %i\n", a, b, division(a,b));
			}
			catchSwitch() {
			  case EXC_DIVBYZ:
				puts("exc: /0");     break;
			  case EXC_ORDER:
				puts("exc: a<b");    break;
			  default:    /* EXC_42 ou EXC_NOTRY */
				rethrow();          break;
			}
			
		}
		
		catch(Exception e) {
			if(e == EXC_42)
				puts("exc: 42");
			else    /* EXC_NOTRY */
				rethrow();    /* erreur : pas dans un bloc `try` */
		}
	}
}
  • Partager sur Facebook
  • Partager sur Twitter
28 avril 2012 à 13:39:30

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
  • Partager sur Facebook
  • Partager sur Twitter
28 avril 2012 à 13:51:08

Citation : Mr21

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.
  • Partager sur Facebook
  • Partager sur Twitter
Staff désormais retraité.
28 avril 2012 à 16:18:52

Citation : Mr21


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 ;)
  • Partager sur Facebook
  • Partager sur Twitter
28 avril 2012 à 17:00:19

Est-ce seulement autorisé ? Il me semblait que les goto non-locaux ne sont pas standard, et que l'on doit plutôt utiliser setjmp/longjmp à la place.
  • Partager sur Facebook
  • Partager sur Twitter
J'ai déménagé sur Zeste de savoir — Ex-manager des modérateurs.
28 avril 2012 à 17:02:11

Citation : GuilOooo


Est-ce seulement autorisé ? Il me semblait que les goto non-locaux ne sont pas standard, et que l'on doit plutôt utiliser setjmp/longjmp à la place.



Oui, tu as raison, les goto non locaux ne sont pas standard. Je faisait allusion à setjmp et longjmp ;)
  • Partager sur Facebook
  • Partager sur Twitter
28 avril 2012 à 20:04:56

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.
  • Partager sur Facebook
  • Partager sur Twitter
2 mai 2012 à 17:36:35

Hey,

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.
  • Partager sur Facebook
  • Partager sur Twitter
Staff désormais retraité.
3 juillet 2012 à 16:28:51

Bonjour,

Je viens de tester le 2nd code de Maëllan (pile automatique), à ceci près que j'ai provoqué une erreur de segmentation en guise de test :

void erreurSeg(void)
{
	int *pointeur, valeur;
	valeur = 3;

	fprintf(stdout, "Et la, ca devrait planter !\n");
	*pointeur = valeur * valeur;
	fprintf(stdout, "Test rate...\n");

	return ;
}


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...
  • Partager sur Facebook
  • Partager sur Twitter
Anonyme
3 juillet 2012 à 17:33:16

Citation : Lucas-84

[...]ç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.
  • Partager sur Facebook
  • Partager sur Twitter
3 juillet 2012 à 17:50:00

Citation : Slimus

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)
  • Partager sur Facebook
  • Partager sur Twitter
5 juillet 2012 à 10:57:31

Citation : IATGOF

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.

  • Partager sur Facebook
  • Partager sur Twitter
8 juillet 2012 à 16:28:28

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 …
  • Partager sur Facebook
  • Partager sur Twitter