Retrouvez cet article dans : Linux Magazine 108
Un développeur élabore souvent un programme en réfléchissant au déroulement nominal de différentes étapes. Cette démarche générale doit absolument être complétée – au moins au moment de l’implémentation – en prenant en compte l’existence potentielle de situations perturbatrices (manque de mémoire, absence de fichier…). Les langages de programmation proposent différents mécanismes pour effectuer cette prise en compte (le langage Java propose les " exceptions "). Les mécanismes disponibles avec le langage C sont simples et très efficaces (à condition de faire preuve d’un minimum de rigueur) : il s’agit de ce qu’on peut appeler la " gestion des erreurs ".
|
1 |
Présentation générale |
Les gestion des erreurs dans la construction d’un programme consiste à analyser la façon dont une partie de code s’est exécutée, et à agir en fonction du résultat de cette analyse.
D’une manière générale, on pourra avoir un code se présentant de la manière suivante.
|
// Code 1. <CodeÀAnalyser> if (<ConditionDErreur1>) { <TraitementErreur1> } if (<ConditionDErreur2>) { <TraitementErreur2> } … if (<ConditionDErreurn>) { <TraitementErreurn> } <SuiteDuDéroulementNormalDuProgramme> |
Le code suivant montre un moyen courant de gérer une erreur de syntaxe lors de l’appel d’un programme.
|
… void rappelsyntaxe(const char *nomprogramme) { fprintf(stderr, "%s <NomFichier>\n"); exit(EXIT_FAILURE); } int main(argc, argv) int argc; const char *argv[]; { if (argc != 2) { rappelsyntaxe(argv[0]); } … } |
|
2 |
Détection des erreurs |
|
2.1 |
Généralités |
Le besoin de tester des erreurs éventuelles se fait souvent sentir lors du retour d’une fonction (pour savoir si l’exécution de cette fonction s’est bien passée), et notamment lors du retour d’un appel système.
|
Note |
|
Les appels système – en tant que passerelle entre la couche applicative et la couche noyau – donnent par essence accès à des fonctions privilégiées, et souvent accès à des périphériques. Ils sont donc fortement susceptibles d’échouer (au moins partiellement), soit pour des raisons de droits insuffisants de la part du processus qui invoque l’appel système, soit pour des raisons d’échec matériel. |
|
<AppelFonction> if (<ConditionDErreur1>) { <TraitementErreur1> } if (<ConditionDErreur2>) { <TraitementErreur2> } … if (<ConditionDErreurn>) { <TraitementErreurn> } <SuiteDuDéroulementNormalDuProgramme> |
|
2.2 |
Analyse des valeurs renvoyées |
La détection des erreurs peut souvent être réalisée en analysant les différentes valeurs renvoyées par les fonctions (valeur de retour ou valeur passées en paramètre par référence).
Le code suivant présente une méthode standard pour effectuer une allocation dynamique.
|
if ((tampon = (char *)malloc(TAILLEALLOCATION * sizeof(char))) == NULL) { fprintf(stderr, "Erreur lors de l’allocation de %d ‘char’.\n", TAILLEALLOCATION); … } |
La détermination de la réussite ou de l’échec de l’appel d’une fonction passe souvent par son code de retour, comme dans le code précédent. Il peut aussi arriver que de plus amples informations soient transmises par l’intermédiaire des arguments de la fonction. Ainsi, la fonction pcre_compile() renvoie par ses paramètres une chaîne de caractères décrivant une erreur potentielle qui est survenue pendant son exécution.
|
pcre *pcre_compile(const char *<Motif>, int <Options>, const char **<P_Err>, int *<P_DecErr>, const unsigned char *<P_Tableau>) |
pcre_compile() compile une expression rationnelle qui suit le format perl des expressions rationnelles.
Si la fonction pcre_compile() échoue, alors elle renvoie NULL et la variable pointée par <P_Err> pointe vers un message d’erreur. Il s’agit d’une chaîne statique qui fait partie intégrante de la bibliothèque libpcre.
Enfin, même lorsque l’erreur potentielle peut être détectée par le code de retour, il se peut que son analyse complète demande un traitement relativement complexe. Par exemple, la fonction strptime() – destinée à transformer une chaîne de caractères en structure struct tm – renvoie NULL si elle n’arrive pas à effectuer toutes les conversions, et renvoie sinon un pointeur sur le premier caractère de la chaîne d’entrée qui n’a pas été traité.
|
// Code 2. char *ret; if ((ret = strptime(chaine, format, &tm)) == NULL) { fprintf(stderr, "Certaines conversions n’ont pas pu être " "effectuées.\n"); exit(EXIT_FAILURE); } if (!ret) { fprintf(stderr, "La chaîne n’a pas être traitée à partir du tronçon " "’%s’.\n", ret); exit(EXIT_FAILURE); } |
|
2.3 |
Code d’erreur |
|
2.3.1 |
Accès direct |
Les conditions d’erreur peuvent souvent être déterminées au retour des fonctions par l’intermédiaire d’un code d’erreur.
Ce code d’erreur peut être transmis par le code de retour de la fonction, comme c’est généralement le cas avec les fonctions de manipulation de threads.
|
// Code 3. code = pthread_create(&tid, NULL, fonction, NULL); switch (code) { … case EAGAIN: fprintf(stderr, "Manque de ressource.\n"); exit(FAILURE); … } |
Ce code peut aussi être transmis par l’intermédiaire d’une variable globale. Le plus souvent, le code de retour de la fonction indique s’il y a eu un problème, et le problème est spécifié par l’intermédiaire de la variable.
La variable la plus couramment utilisée est errno.
|
// Code 4. #define FICHIER "/tmp/fichier.txt" if ((df = open(FICHIER, O_RDONLY)) == -1) { switch (errno) { … case ENOENT: fprintf(stderr, "Le fichier ‘%s’ n’existe pas.\n", FICHIER); break; … } exit(EXIT_FAILURE); } |
Les fonctions gethostbyname() et gethostbyaddr() renvoient un pointeur NULL si une erreur se produit, auquel cas la variable globale h_errno contient le code d’erreur.
|
// Code 5. struct hostent *hostent; #define NOM "localhost" if ((hostent = gethostbyname(NOM)) == NULL) { fprintf(stderr, "Erreur lors de la traduction de ‘%s’ en adresse " "IP.\n", NOM); switch (h_errno) { … case HOST_NOT_FOUND: fprintf(stderr, "Hôte inconnu.\n"); break; … } exit(EXIT_FAILURE); } |
|
2.3.2 |
Fonctions de traitement des codes d’erreur |
Plusieurs fonctions sont fournies pour traiter de façon automatique les codes d’erreur.
Les fonctions strerror() (non thread-safe) et strerror_r() (thread-safe) permettent de récupérer une chaîne décrivant un code d’erreur passé en argument (en utilisant éventuellement la catégorie LC_MESSAGES de la régionalisation pour sélectionner la langue appropriée). Ainsi, le code 4 pourrait être repris par le code 6 (pour un processus constitué d’un seul thread) ou par le code 7 (pour un processus constitué potentiellement de plusieurs threads).
|
// Code 6. #define FICHIER "/tmp/fichier.txt" if ((df = open(FICHIER, O_RDONLY)) == -1) { fprintf(stderr, "L’erreur suivante a été rencontrée lors de l’ouverture" " de ‘%s’ : ‘%s’.\n", FICHIER, strerror(errno)); exit(EXIT_FAILURE); } |
|
// Code 7. #define FICHIER "/tmp/fichier.txt" #define TAILLETAMPON 1000 char tampon[TAILLETAMPON];§\par if ((df = open(FICHIER, O_RDONLY)) == -1) { if (strerror_d(errno, tampon, TAILLETAMPON)) { fprintf(stderr, "Erreur lors de l’ouverture de ‘%s’.\n", FICHIER); } else { fprintf(stderr, "L’erreur suivante a été rencontrée lors de " "l’ouverture de ‘%s’ : ‘%s’.\n", FICHIER, tampon); } exit(EXIT_FAILURE); } |
La fonction perror() affiche un message sur la sortie d’erreur standard, décrivant l’erreur dont le code est donné par la variable globale errno. Ainsi, le code 6 pourrait être repris de la manière suivante.
|
#define FICHIER "/tmp/fichier.txt" if ((df = open(FICHIER, O_RDONLY)) == -1) { perror("open"); exit(EXIT_FAILURE); } |
Dans le contexte de la recherche d’informations sur le réseau, la fonction hstrerror() permet de récupérer une chaîne décrivant un code d’erreur passé en argument. Ainsi, le code 5 pourrait être repris par le code suivant.
|
struct hostent *hostent; #define NOM "localhost" if ((hostent = gethostbyname(NOM)) == NULL) { fprintf(stderr, "L’erreur suivante a été rencontrée lors de la traduction de " "’%s’ en adresse IP : ‘%s’.\n", NOM, hstrerror(h_errno)); exit(EXIT_FAILURE); } |
Il existe d’autres fonctions qui fonctionnent sur le même principe. Ainsi, par exemple, la fonction regerror() transforme la valeur de retour des fonctions regcomp() et regexec() en chaînes de caractères décrivant une erreur qui est survenue lors de l’utilisation de ces fonctions.
|
2.4 |
Faux positifs |
Une utilisation maladroite de certains indicateurs peut parfois amener faussement à croire à une réussite ou à une erreur.
Ainsi, dans le code 2, on pourrait facilement croire que le premier test (c’est-à-dire utiliser le code suivant) suffit pour détecter une erreur. Certaines erreurs de format détectées en cours d’analyse ne seront alors pas détectées.
|
if (strptime(chaine, format, &tm) == NULL) { fprintf(stderr, "Certaines conversions n’ont pas pu être " "effectuées.\n"); exit(EXIT_FAILURE); } |
De même, il peut arriver que l’espace de stockage réservé à un code de retour soit insuffisant et fasse trompeusement croire à une erreur. Par exemple, la fonction read() renvoie – dans une zone de type ssize_t – le nombre d’octets lus en cas de réussite, et (ssize_t)-1 en cas d’erreur. On pourrait ainsi penser que le code suivant permet de détecter correctement les erreurs.
|
ssize_t longueur; if ((longueur = read(df, tampon, quantite)) == -1) { perror("read"); return EXIT_FAILURE; } |
Cependant, il peut théoriquement arriver que la lecture se soit bien passée, et que la quantité lue soit égale à (ssize_t)-1 (pour des raisons de rebouclage des valeurs numériques). Pour cette raison, il est préférable de détecter les erreurs de la manière suivante :
|
ssize_t longueur; errno = 0 longueur = read(df, tampon, quantite); if (errno != 0) { perror("read"); return EXIT_FAILURE; } |
|
3 |
Traitement des erreurs |
|
3.1 |
Deux comportements |
Lorsqu’une erreur est découverte, deux comportements opposés peuvent être adoptés :
● L’erreur est considérée comme ayant un impact acceptable sur le reste du programme, et on continue normalement le déroulement de ce dernier.
● L’erreur est considérée bloquante, et on déroute le flot normal d’exécution du programme, voire on arrête totalement son exécution.
Il appartient au programmeur de décider si les erreurs empêchent la suite du déroulement normal du programme ou pas.
|
3.1.1 |
Impact inacceptable |
Considérons le code 1. Si <ConditionDErreuri> est vraie, et si cette erreur empêche la suite du déroulement normal du programme, alors il est possible d’effectuer un saut par goto, un retour de fonction par return ou une sortie de programme par exit() pour briser le flot normal des instructions et finir de traiter proprement l’erreur.
Le code suivant implémente une fonction copierchaine(), qui – si elle réussit – retourne un pointeur vers une zone nouvellement allouée (qu’il convient donc de libérer une fois qu’elle n’est plus utilisée) contenant la chaîne passée en argument, et qui – si elle échoue – retourne NULL.
|
char *copierchaine(source) const char *source; { char *retour; size_t longueur; size_t tailleallocation; if (source == NULL) { return NULL; /* Argument invalide. Il n’est pas possible de continuer le traitement de cette fonction. On la quitte.*/ } longueur = strlen(source); tailleallocation = longueur + 1; if ((retour = malloc(taille * sizeof(char))) == NULL) { return NULL; /* Erreur d’allocation mémoire. Il n’est pas possible de continuer le traitement de cette fonction. On la quitte. */ } snprintf(retour, tailleallocation, "%s", source); return retour; } |
|
3.1.2 |
Impact acceptable |
Les erreurs présentant un impact acceptable se traitent souvent par un message d’avertissement, quelques modifications de paramètres, et la reprise normale du déroulement du programme.
|
int main() { if ((err = lirefichierconfiguration())) { fprintf(stderr, "Erreur %d lors de la lecture du fichier de " "configuration. Utilisation de la configuration par " "défaut.\n", err); chargerconfigurationpardefaut(); } … } |
|
3.2 |
Libération de ressources |
Lorsque le programmeur choisit de dérouter le flot normal d’exécution du programme, il est parfois amené à libérer certaines ressources.
|
3.2.1 |
Libération directe |
La manière la plus intuitive consiste à appeler directement les fonctions de libération.
|
// Code 8. int fonction() { int df; char *chaine1, *chaine2; if ((df = open(NOMFICHIER, O_RDONLY))) { perror("open"); return EXIT_FAILURE; } if ((chaine1 = (char *)malloc(TAILLEALLOCATION * sizeof(char)))) { fprintf(stderr, "Erreur d’allocation mémoire.\n"); close(df); return EXIT_FAILURE; } if ((chaine2 = (char *)malloc(TAILLEALLOCATION * sizeof(char)))) { fprintf(stderr, "Erreur d’allocation mémoire.\n"); close(df); free(chaine1); return EXIT_FAILURE; } … free(chaine2); free(chaine1); close(df); return EXIT_SUCCESS; } |
|
3.2.2 |
Utilisation d’une fonction de libération |
On peut raccourcir le code de traitement d’erreur en créant une fonction de libération des ressources. On convient, pour chacune des variables contenant une ressource, d’une valeur particulière indiquant que la ressource n’a pas été allouée. Pour les allocations dynamiques de mémoire, ce peut être NULL. Pour les descripteurs de fichiers, ce peut être n’importe quelle valeur négative ; les appels système renvoient -1 généralement lorsqu’ils ont échoué ; la valeur -2 pourrait être choisie pour indiquer que la variable n’a pas encore été initialisée.
|
Note |
|
Il n’est pas nécessaire d’effectuer une distinction entre une variable non initialisée et une variable indiquant qu’il y a eu une erreur. Auquel cas, n’importe quelle valeur négative indique que la variable ne contient pas de descripteur ouvert. |
Le code 8 pourrait être repris de la manière suivante :
|
// Code 9. int df; #define DFNONINITI -2 char *chaine1, *chaine2; void liberationressources() { if (df > 0) { close(df); } if (chaine1 != NULL) { free(chaine1); } if (chaine2 != NULL) { free(chaine2); } } int fonction() { df = DFNONINITI; chaine1 = NULL; chaine2 = NULL; if ((df = open(NOMFICHIER, O_RDONLY))) { perror("open"); liberationressources(); /* Cet appel n’est pas fondamentalement nécessaire, mais permet d’avoir une présentation semblable de tous les blocs de gestion d’erreurs. */ return EXIT_FAILURE; } if ((chaine1 = (char *)malloc(TAILLEALLOCATION * sizeof(char)))) { fprintf(stderr, "Erreur d’allocation mémoire.\n"); liberationressources(); return EXIT_FAILURE; } if ((chaine2 = (char *)malloc(TAILLEALLOCATION * sizeof(char)))) { fprintf(stderr, "Erreur d’allocation mémoire.\n"); liberationressources(); return EXIT_FAILURE; } … liberationressources(); return EXIT_SUCCESS; } |
Si on veut éviter d’utiliser des variables globales, on peut passer l’ensemble des variables en paramètre de la fonction liberationressources(). Il pourrait aussi être possible de transférer en paramètre le message à afficher (ce qui permet d’alléger encore d’avantage le code de traitement d’erreur).
Les deux principaux inconvénients de cette méthode sont :
● qu’on est soit obligé de placer en global des variables qui auraient pu normalement être locales à une fonction (le principe de visibilité et d’accessibilité minimales ne semblent pas être réellement atteint), soit obligé de placer les variables en argument à la fonction de libération (ce qui peut être assez lourd s’il y a beaucoup de variables impactées) ;
● qu’on est obligé de créer une fonction de libération spécifique (ce qui peut alourdir considérablement un module si beaucoup de ses fonctions effectuent des allocations).
|
3.2.3 |
Utilisation de sauts (première méthode) |
On peut généralement se passer d’une fonction de libération en utilisant des directives goto. Le code 9 pourrait être repris de la manière suivante :
|
// Code 10. #define DFNONINITI -2 int fonction() { int df = DFNONINITI; char *chaine1 = NULL; char *chaine2 = NULL; int ret = EXIT_SUCCESS; if ((df = open(NOMFICHIER, O_RDONLY))) { perror("open"); ret = EXIT_FAILURE; goto fin; } if ((chaine1 = (char *)malloc(TAILLEALLOCATION * sizeof(char)))) { fprintf(stderr, "Erreur d’allocation mémoire.\n"); ret = EXIT_FAILURE; goto fin; } if ((chaine2 = (char *)malloc(TAILLEALLOCATION * sizeof(char)))) { fprintf(stderr, "Erreur d’allocation mémoire.\n"); ret = EXIT_FAILURE; goto fin; } … fin: if (df > 0) { close(df); } if (chaine1 != NULL) { free(chaine1); } if (chaine2 != NULL) { free(chaine2); } return ret; } |
|
3.2.4 |
Utilisation des sauts (deuxième méthode) |
Si les allocations sont réalisées de manière séquentielle (comme c’est le cas au code 10), il est possible de se passer des conventions indiquant la non-initialisation des ressources (permettant en temps normal d’éviter de libérer des ressources qui n’ont en fait été allouées). Ainsi, le code 10 peut être réécrit de la manière suivante :
|
int fonction() { int df; char *chaine1, *chaine2; int ret = EXIT_SUCCESS; if ((df = open(NOMFICHIER, O_RDONLY))) { perror("open"); ret = EXIT_FAILURE; goto fin1; } if ((chaine1 = (char *)malloc(TAILLEALLOCATION * sizeof(char)))) { fprintf(stderr, "Erreur d’allocation mémoire.\n"); ret = EXIT_FAILURE; goto fin2; } if ((chaine2 = (char *)malloc(TAILLEALLOCATION * sizeof(char)))) { fprintf(stderr, "Erreur d’allocation mémoire.\n"); ret = EXIT_FAILURE; goto fin3; } … fin3: if (chaine2 != NULL) { free(chaine2); } fin2: if (chaine1 != NULL) { free(chaine1); } fin1: if (df > 0) { close(df); } return ret; } |
Il faut faire attention à libérer les ressources dans l’ordre inverse dans lesquelles elles sont allouées.
|
3.2.5 |
Fonctions d’allocation de ressources |
Une fonction d’allocation de ressources ne doit évidemment pas libérer les ressources qu’elle est censée renvoyer. Avec ce genre de fonctions, il faut différencier les ressources devant être renvoyées (et qui ne doivent donc être libérées qu’en cas d’erreur) et les ressources intermédiaires utilisées uniquement pour construire les ressources à renvoyer (et qui doivent donc être libérées à la fois en cas d’erreur et lors d’une fin normale de la fonction).
Le code suivant présente une fonction qui permet d’allouer et de retourner deux chaînes de caractères : une contenant l’aboutement des lignes paires d’un fichier, l’autre contenant l’aboutement des lignes impaires de ce même fichier.
|
// Code 11. int df; #define DFNONINITI -2 char *lignespaires, *lignesimpaires; void liberationressources(ret) int ret; { if (df > 0) { close(df); } if (ret != EXIT_SUCCESS) { if (lignespaires != NULL) { free(lignespaires); } if (lignesimpaires != NULL) { free(lignesimpaires); } } } int fonction(fichier, p_lignespaires, p_lignesimpaires) const char *fichier; char **p_lignespaires; char **p_lignesimpaires; { *p_lignespaires = NULL; *p_lignesimpaires = NULL; df = DFNONINITI; lignespaires = NULL; lignesimpaires = NULL; if ((df = open(fichier, O_RDONLY))) { perror("open"); liberationressources(EXIT_FAILURE); /* Cet appel n’est pas fondamentalement nécessaire, mais permet d’avoir une présentation semblable de tous les blocs de gestion d’erreurs. */ return EXIT_FAILURE; } if ((lignespaires = (char *)malloc(TAILLEALLOCATION * sizeof(char)))) { fprintf(stderr, "Erreur d’allocation mémoire.\n"); liberationressources(EXIT_FAILURE); return EXIT_FAILURE; } if ((lignesimpaires = (char *)malloc(TAILLEALLOCATION * sizeof(char)))) { fprintf(stderr, "Erreur d’allocation mémoire.\n"); liberationressources(EXIT_FAILURE); return EXIT_FAILURE; } … liberationressources(EXIT_SUCCESS); *p_lignespaires = lignespaires; *p_lignesimpaires = lignesimpaires; return EXIT_SUCCESS; } |
Dans ce genre de situations, il est aussi possible d’utiliser la méthode des sauts pour la libération des ressources. Ainsi, le code 11 peut être repris de la manière suivante :
|
#define DFNONINITI -2 int fonction(fichier, p_lignespaires, p_lignesimpaires) const char *fichier; char **p_lignespaires; char **p_lignesimpaires; { int df = DFNONINITI; char *lignespaires = NULL; char *lignesimpaires = NULL; int ret = EXIT_SUCCESS; if ((df = open(fichier, O_RDONLY))) { perror("open"); ret = EXIT_FAILURE; goto fin; } if ((lignespaires = (char *)malloc(TAILLEALLOCATION * sizeof(char)))) { fprintf(stderr, "Erreur d’allocation mémoire.\n"); ret = EXIT_FAILURE; goto fin; } if ((lignesimpaires = (char *)malloc(TAILLEALLOCATION * sizeof(char)))) { fprintf(stderr, "Erreur d’allocation mémoire.\n"); ret = EXIT_FAILURE; goto fin; } … fin: if (df > 0) { close(df); } if (ret != EXIT_SUCCESS) { if (lignespaires != NULL) { free(lignespaires); } lignespaires = NULL; if (lignesimpaires != NULL) { free(lignesimpaires); } lignesimpaires = NULL; } *p_lignespaires = lignespaires; *p_lignesimpaires = lignesimpaires; return ret; } |
|
Auteur : Myriam Lagarde |
|
Développeur occasionnel sous OpenBSD |
Retrouvez cet article dans : Linux Magazine 108


