Gestion des exceptions en langage C

Rédigé par niconux Aucun commentaire
Classé dans : C/C++, Développement Mots clés : Exception, C

La bibliothèque thématique de programme en C se devait de traiter la gestion des exceptions qui est une manière très efficace de gérer les erreurs au sein d'un programme, quel qu'il soit.

Cet article s'efforcera de donner quelques implémentations de système de gestion d'exception au sein d'un programme écrit en langage C. Ces implémentations pourront souffrir de plus ou moins de limitations.

Dans des langages de programmations "modernes" (loin de moi de dire que le C est un langage du passé) comme le C++, Java ou C#, la gestion des exceptions se traduit par les instructions : try-throw-catch.

...
 try
{
 /* traitement, instruction, ... */
 }
 catch (TypeException e)
{
 /* gestion de l'exception : traitement de l'erreur */
}
 ... 

 

Voyons comment cette mécanique peut être implémentée en C.

Dans notre exemple précédent, toutes les instructions du bloc try peuvent déclencher (soulever) une exception qui sera traitée dans le bloc catch.
Si l'exception est de type TypeException alors le code présent dans le catch sera exécuté.

  • Fonctions setjmp et longjmp :

ANSI-C fournit de nombreuses fonctions : mathématiques (log, sqrt, ...), permettant la manipulation de chaînes de caractères (strcmp, ...) et des fonctions permettant de gérer les entrées/sorties (I/O) (getc, printf, ...).
Toutes ces fonctions sont très largement utilisées et non rien de compliqué, par contre deux fonctions s'avèrent très différentes et pas forcément simple à prendre en main.
Ces deux fonctions sont : setpjmp et longjmp, c'est ces deux fonctions qui vont nous permettre de gérer les différents sauts pour le traitement de l'exception.
 

  • Définitions des fonctions setjmp et longjmp :

setjmp et longjmp sont définit dans setjmp.h comme suit :
 

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

 

Les objets déclarés dans <setjmp.h> permettent d'éviter la séquence normale d'appel et de retour de fonction, ce qui s'utilise essentiellement pour revenir immédiatement d'une fonction profondément imbriquée.

La macro setjmp sauvegarde l'état du programme dans env, en vue de son utilisation par longjmp. setjmp retourne zéro en cas d'appel direct et une valeur non nulle en cas d'appel ultérieur via longjmp. On ne peut appeler longjmp que dans certains contextes, essentiellement à l'intérieur des tests de if, de switch et des boucles, et seulement dans des expressions simples.

longjmp remet le programme dans l'état sauvegardé lors du dernier appel à setjmp, grâce aux informations mémorisées dans env, et l'exécution reprend comme si la fonction setjmp venait de s'exécuter et avait retourné la valeur non nulle val. La fonction qui contient le setjmp ne doit pas être terminée. Les objets accessibles gardent les mêmes valeurs qu'au moment de l'appel de longjmp ; cependant, si des variables automatiques non volatiles de la fonction qui a appelé setjmp ont été modifiées depuis cet appel, leurs valeurs sont indéfinies après longjmp.

 
Pour de plus amples détails sur ces deux fonctions vous pouvez par exemple pour référer à la page wikipédia : setjmp.h (EN)
 

  • Implémentation Try-Catch :

 

#include <stdio.h>
#include <setjmp.h>

#define TRY do{ jmp_buf ex_buf; if( !setjmp(ex_buf) ){
#define CATCH } else {
#define ETRY } }while(0)
#define THROW longjmp(ex_buf, 1)

int main(int argc, char** argv)
{
   TRY
   {
      printf("Try...\n");
      THROW;
      printf("Dead code\n");
   }
   CATCH
   {
      printf("An exception rised!\n");
   }
   ETRY;

   return 0;
}


L'idée de cette implémentation repose sur le mapping du TRY sur une instruction if.
Au premier appel cela retourne 0 et le code exécuté est celui se trouvant le bloc du then. CATCH est simplement le bloc else. Quand le bloc THROW est exécuté, cela appelle la fonction longjmp avec comme valeur pour le second paramètre 1 (en fait autre chose que 0).
Cette implémentation propose aussi un bloc ETRY qui représente la fin du bloc try-throw-catch. C'est  voulu car toutes les opérations faites dans le try-throw-catch interviennent dans le cadre d'une boucle do-while.

Deux raisons importantes à cela :

  1. L'instruction TRY-ETRY est gérée comme un seul bloc
  2. Définir de multiples instructions TRY-ETRY dans le même bloc (réutilisation de la variable ex_buf)
  • Implémentation Try-Catch - multi-exceptions :

Une gestion d'exception dans un cas réel a la possibilité de gérer de multiples exceptions et de différentes natures.

Pour répondre à ce fonctionnel, il est nécessaire d'opérer à certaines modifications à l'exemple précédent.

L'instruction TRY-ETRY est traduit par un switch au lieu d'un if. Chaque instruction CATCH n'est plus un simple else, mais un case.
CATCH devient maintenant une macro avec paramètre. Le paramètre représente la nature de l'exception traité dans le bloc.
Chaque instruction CATCH doit fermer le case précédent (avec un break).
 

#include <stdio.h>
#include <setjmp.h>

#define TRY do{ jmp_buf ex_buf; switch( setjmp(ex_buf) ){ case 0:
#define CATCH(x) break; case x:
#define ETRY } }while(0)
#define THROW(x) longjmp(ex_buf, x)

#define MISMATCH_EXCEPTION (1)
#define NULL_EXCEPTION (2)
#define TYPE_EXCEPTION (3)

int main(int argc, char** argv)
{
   TRY
   {
      printf("Try...\n");
      THROW( NULL_EXCEPTION );
      printf("Dead code\n");
   }
   CATCH( MISMATCH_EXCEPTION )
   {
      printf("Mismatch exception rised !\n");
   }
   CATCH( NULL_EXCEPTION )
   {
      printf("Null exception rised !\n");
   }
   CATCH( TYPE_EXCEPTION )
   {
      printf("Type exception rised!\n");
   }
   ETRY;

   return 0;
}

 

  • Implémentation Try-Catch-Finally :

Dans cette dernière implémentation, nous allons mettre en place la gestion du finally. Le bloc finally permet d'être exécuté après le bloc try ou n'importe quel bloc catch.

L'exécution du finally est possible dans les trois cas suivants :

  1. Après un bloc TRY
  2. Après un bloc CATCH
  3. Quand un type d'exception n'est pas connu.
#include <stdio.h>
#include <setjmp.h>

#define TRY do{ jmp_buf ex_buf; switch( setjmp(ex_buf) ){ case 0: while(1){
#define CATCH(x) break; case x:
#define FINALLY break; } default:
#define ETRY } }while(0)
#define THROW(x) longjmp(ex_buf, x)

#define MISMATCH_EXCEPTION (1)
#define NULL_EXCEPTION (2)
#define TYPE_EXCEPTION (3)

int main(int argc, char** argv)
{
   TRY
   {
      printf("Try...\n");
      THROW( NULL_EXCEPTION );
      printf("Dead code\n");
   }
   CATCH( MISMATCH_EXCEPTION )
   {
      printf("Mismatch exception rised !\n");
   }
   CATCH( NULL_EXCEPTION )
   {
      printf("Null exception rised !\n");
   }
   CATCH( TYPE_EXCEPTION )
   {
      printf("Type exception rised!\n");
   }
   FINALLY
   {
      printf("... finally\n");
   }
   ETRY;

   return 0;
}

 

Malgré les limitations que l'on a pu voir, il est possible de mettre en place un système de gestion d'exceptions dans un programme C, même si l'on ne peut pas profiter de toutes les fonctionnalités que l'on peut retrouver dans le système d'exception en Java, C++, ...

Vous pouvez retrouver d'autres articles et programmes dans la bibliothèque thématique réservée au langage C.

Écrire un commentaire

Quelle est le premier caractère du mot kc8mdu ?

Fil RSS des commentaires de cet article