Le noyau de calcul

Sommaire (svp cliquer sur la ligne)

Retour à la première page.

1.Ecrit en C ou autres langages compilés

1.1.Le choix du langage n'est pas primordial

Réglons de suite la question du langage à utiliser. Nous parlons ici de calculs longs dont il est important de minimiser le temps de traitement. En conséquences, l'utilisation de langages compilés s'imposent, sauf pour des applications spécifiques ou si on doit utiliser une base existante. Les langages interprétés type Perl ou Tcl/Tk peuvent être assez rapides, mais pour de la véritable programmation scientifique ils sont encore trop long.

Il est souvent intéressant d'utiliser un langage largement disponible sur tous les systèmes informatique. Actuellement le transfert vers d'autres systèmes ne pose plus de difficulté, mais j'essaie d'éviter les outils propriétaires type Windev ou la suite .net. Les choix les plus courants sont le C, le C++, le Pascal et le Fortran. Dans la suite je traiterais principalement du C.

Le Fortran est un très bon choix, mais il faut éviter les Fortrans 77 ou antérieurs qui sont obsolètes bien que très rapides.

L'écriture orientée objet n'est pas toujours un plus, même si la protection des variables est une bonne technique. Je conseille d'éviter les "héritages de classes". ils compliquent souvent la lecture du programme et ralentissent son exécution.

1.2.un premier exemple

Commençons par un tout petit exemple ! (s.v.p. cliquer sur la ligne)

Remarquons en premier l'entête, c'est un commentaire qui commence par /** @brief. Il contient diverses indications : version date bug,... Elles sont formatées pour être interprétées par doxygen. Cet utilitaire permet rapidement de faire une documentation technique, nous y reviendrons plus loin.

Mais l'indication principale est ce que fait le programme. Cette indication primordiale est parfois absente ! Dans l'exemple ce n'est bien grave car le code est minuscule, mais dès que le traitement se complique c'est très gênant pour réutiliser du code ancien et même pour simplement comprendre ce qu'il fait.

Sinon cet exemple a beaucoup de défauts:

  • les données ne sont pas testées,
  • ni la validité des calculs,
  • le calcul n'est pas daté
  • les résultats ne sont pas enregistrés (tracés)

Voici mes astuces qui permettent de les gommer.

2.Lecture des données

2.1.Lecture au clavier, dans la ligne de commandes

2.1.a.Entrées au clavier à consommer avec modération

Lors de l'apprentissage de la programmation, les données sont souvent introduites au clavier comme c'est le cas dans l'exemple précédent. Pour un tout petit utilitaire, c'est suffisant. Mais si il a plus que quatre valeurs, les humains ont du mal à les entrer sans se tromper en tout cas c'est vrai pour moi !. Mais le véritable inconvénient est que cela allonge notablement le temps, au point qu'il est fastidieux de réaliser une série de calculs. Donc demander des informations à l'écran est une technique à réserver aux cas extrèmement simples.

Il est possible d'intercepter les certaines erreurs de frappe. Ceci peut être fait en comptant le nombre de valeurs décodées par scanf comme ci dessous.

Version corrigée qui teste la valeur entrée (s.v.p. cliquer sur la ligne)

Le test de la validité est une "dérivation" par rapport au déroulement normal. Il faut le commenter, y compris la variable locale nb. Il existe une meilleure solution avec strtod qui permet d'afficher la donnée lue mais mal interprétée.

Si le programme demande 4 valeurs et que l'on se trompe dans la dernière, il faut tout refaire ! Cela peut être crispant.

2.1.b.Mettre les données dans la ligne de commandes

Il y a deux solutions pour éviter de lire une donnée au clavier: la première la lire dans un fichier, l'autre l'ajouter comme argument de la ligne de commandes. Nous allons examiner la seconde solution qui est naturelle en Linux, mais peut aussi être utilisée avec Windows. Les fignes de commandes peuvent être rapellées, cela facilite la correction d'erreur de frappes.

La librarie getopt facilite la gestion des arguments en ligne de commande. Elle est standard dans l'environnement Linux et il existe des portages pour Windows (par exemple dans Analyser les options passées en ligne de commande).

Exemple de valeur entrée en ligne de commandes (s.v.p. cliquer sur la ligne)

D'accord, c'est long juste pour le calcul du carré d'un nombre ! Le lancement du programme se fait par :

[ codes]$ ./carre -x 28
son carré vaut : 784

Un appel sans argument donne la forme correcte de la ligne de commandes et un petit message indiquant ce que fait le programme (ceci est important).

[ codes]$ ./carre3
command line : carre -x value
  -x value : double dont on veut calculer le carré
  -h       : aide en ligne

Calcule le carré d'un nombre

Et un appel incorrect

[ codes]$ ./carre3 -x bidule
erreur! x n'est pas un nombre ! bidule

Remarquez dans le code:

  • Une directive de compilation permet de distinguer le système Linux qui a getopt.h en standard de Windows où il faut l'adjoindre au programme.
  • La mini "aide en ligne" est appelée lorsqu'il n'y a pas d'argument ou si un d'eux n'est pas reconnu.
  • Le test du nombre entré qui utilise strtod. Le message d'erreur reporte l'argument invalide. Ce test n'est pas parfait car il se fait avoir avec quelques cas comme les appels ./carre3 -x 3,4 ou ./carre3 -x 1 456 m.
  • Le programme retourne une valeur standard (définie dans stdlib.h) EXIT_SUCCESS ou EXIT_FAILURE en cas de problème.

2.2.Lecture dans un fichier

Lorsqu'il y a un grand nombre de données nécessaires au calcul, il n'est plus question de les demander à l'écran, ni de les mettre en arguments de la ligne de commande. Elles sont lues dans un fichier de données.

Pas de nom de fichier en dur dans le code

Il est préférable de ne pas inscrire le nom du fichier en dur dans le code. D'une part cela oblige à de nombreuses opérations de copies si il y a plusieurs cas à effectuer, et d'autre part c'est une source de non portabilité du programme. Implicitement le nom en dur suppose que l'arborescence des fichiers soit identique dans l'ordinateur hôte et dans celui du développeur.

A mon avis, le mieux est d'introduire le nom de fichier comme argument de la ligne de commandes.

Exemple de ligne de commandes : nomprogramme -i fichin -o fichout (s.v.p. cliquer sur la ligne)

Il faut également modifier l'aide en ligne (procédure usage)

Avez vous remarqué qu'un nom de fichier par défaut est prévu ? Il faut prévoir le cas ou l'utilisateur oublie un argument, sinon il faut vérifier que tous les arguments obligatoires sont présents, puis éventuellement signaler à l'utilisateur qu'il en manque.

Voici l'ouverture des fichiers.
    if (fichdat==NULL) fichdat=fichdat_def;
    fprintf(stdout,"Ouvre fichier données %s\n",fichdat); fflush(stdout);
    if ((dat=fopen(fichdat, "r"))==NULL) {
       printf("error during opening: %s %s\n",fichdat,strerror(errno));
       exit(errno);
    }

La première ligne fait pointer fichdat vers le nom par défaut. La seconde ligne est uniquement un message pour suivre l'avancement du programme à l'écran. Elle est terminée par l'ordre flush pour forcer l'affichage immédiat. D'après la norme, celui ci est implicitement rajouté après une écriture à l'écran, mais il m'est arrivé que cela ne soit pas le cas.

L'ouverture du fichier se fait par fopen. La réussite de l'opération est testée, comme c'est une opération courante, la librairie errno aide à la gestion de ces erreurs. Il faut dans ce cas ajouter en début de programme :

#include <errno.h>

2.3.Les données ont des unités !

Il m'est arrivé, il y a longtemps, de voir des fichiers de données contenant uniquement les valeurs à lire. La maintenance de ce type de fichier est délicate et même impossible sans une documentation précise.

Cas d'un mauvais fichier de données C'est un exemple vécu !!!

Dans ce cas, une lecture minutieuse et quelques suppositions ont permis de comprendre la signification de chaque valeur. Mais le risque d'une mauvaise interprétation existe.

Pour les programmes scientifiques chaque donnée est plus que uniquement un nombre. Implicitement une donnée est l'ensemble de :

    exemple de valeur "complète"
  • son type (scalaire, vecteur, tableau, arborescence, ...)
  • sa valeur : 1.231
  • sa signification : "longueur du tuyau"
  • son unité : (m) ou (cm) ...
  • sa plage de validité : [0 : 3]
  • sa précision : 0,7 %
  • son origine : "relevé sur le plan XXXX" ou "mesuré par Paul le DD/MM/YY"

Pour faire bien, il faudrait indiquer tout ces aspects dans le fichier de données. La structure naturelle du fichier de données devrait utiliser le format <XML>.
Ce n'est pas ce que ce que j'utilise car l'écriture et la lecture des fichiers <XML> est un peu verbeuse et fastidieuse. De plus il n'est pas toujours facile de comprendre le fichier avec un simple éditeur de texte.

J'utilise une technique de lecture plus simple. C'est un fichier texte avec possibilité d'ajouter des commentaires.

Fichier texte avec commentaires

En fait, dans une ligne contenant une donnée, tout ce qui est après le "!" n'est pas interprété, mais reporté dans le fichier de résultats. Ce n'est pas parfait mais cela permet de garder une lecture facile du fichier de données avec un éditeur classique. En contre partie, le fichier devient rapidement long car on ne peut mettre qu'une valeur par ligne.

Voici l'utilitaire de lecture (s.v.p. cliquer sur la ligne)

qui se lit de la façon suivante:

extern char        er_msg[MAXCHAR];                                    // error message
double longueur;                                                   // Pipe length (m)

    longueur = litreel(tra,dat);                                   // Pipe length
    if ((longueur<1e-6) || (longueur>1.e3)) {                      // Validity range ]0:1000]
       sprintf(er_msg,"Pipe length is out range ]0 ; 1000] %g",longueur);
       alarme(13,imp,CORRECTION,"nom_du_module",er_msg);
       longueur = 0.84;
    }

Le programme alarme est décrit plus loin. Ce qui est important ici est que le test de validité de la donnée est effectué immédiatement après la lecture. Un numéro interne au programme (le 13) a été attribué à cette erreur.

Quelques commentaires sur l'utilitaire de lecture:

- Les premières fonctions adjustl et trim sont de simples suppressions des blancs aux extrémités des chaînes de caractères (manque des bibliothèques standard). Toutes ces fonctions sont dans un fichier ce qui permet la compilation séparée.

- Une fois la mémoire allouée et les chaînes initialisées, la fonction de lecture commence par sauter les lignes blanches et traiter les lignes de commentaires (les imprime dans le fichier de sortie si il commences par #<). Lorsque la fonction de lecture trouve la première ligne qui n'est pas vide et ne commence pas par "#", elle essaie de la décoder. Pour cela, la ligne lue est coupée en deux au niveau du premier caractère "!" ou "#". La partie de gauche doit contenir le nombre à lire et celle de droite est considérée comme étant la légende. Le nombre est décodé, avec un test de réussite, et l'ensemble est formaté pour l'écriture.

Avec le contenu de l'exemple précédent, le fichier pointé par tra doit contenir:

 Ce commentaire débutant par "#>" ou "!>" sera lu comme chaîne de caractères puis imprimé dans le fichier de résultats.
longueur du tuyau (m)...............................: 1.231

Attention Si le fichier de données est mal constitué, par exemple lorsque l'ordre des lignes n'est pas respecté. Il ne sera pas possible de le repérer facilement dans le fichier de trace. L'erreur est affichée avec la fonction alarme qui est décrite plus loin.

Il existe une fonction similaire pour lire une chaîne de caractères (voir le chapitre exemples pour servir de point de départ ).

J'ai un peu modifié mes fonctions de lecture pour intercepter au plus tôt les lignes mal formées et mieux retourner les erreurs. Maintenant il se présente de la façon suivante:

Voici l'utilitaire de lecture amélioré (s.v.p. cliquer sur la ligne)

qui se lit de la façon suivante:

extern char        er_msg[MAXCHAR];                                    // error message
double longueur;                                                   // Pipe length (m)

    ErrCode = litreel(tra,dat,&longueur);                             // Pipe length
    if ((longueur<1e-6) || (longueur>1.e3)) {                      // Validity range ]0:1000]
       sprintf(er_msg,"Pipe length is out range ]0 ; 1000] %g",longueur);
       alarme(13,imp,CORRECTION,"nom_du_module",er_msg);
       longueur = 0.84;
    }

Les erreurs de constitution du fichier de données à tester sont :

NaN ! erreur (not a number Linux)

#_N/A ! une erreur écrite par Excel

23,45 ! cas virgule au lieu de point (toujours Excel))

1 187.7 ! cas séparateur de milliers avec un espace (faux mais ne génère pas d'erreur)

1187.7C ! cas unité présente et collée

1187.7 °C ! cas unité présente et séparée

23.45 ! donnée réelle mais lue avec une lecture d'entier (faux mais ne génère pas d'erreur)

2.4.Lecture de tableaux dynamiques

La première question à se poser est: A t-on besoin d'avoir lu tout le tableau pour faire les calculs ?

Si la réponse est non, ce qui est souvent le cas lors de l'utilisation de données de production. Alors, il n'y a pas réellement de manipulation de tableau dans le programme. Les variables contenant les données d'une ligne peuvent être déclarées en entête de programme. Dans ce cas le déroulement du calcul est :

    Tant qu'il reste des données à lire faire
  1. lire une ligne (soit une série de données) qui remplace les anciennes valeurs
  2. effectuer les calculs à partir de ces valeurs
  3. recommencer jusqu'a ce que tout le fichier soit parcouru

2.4.a.Données tabulées et un de leurs inconvénients

Ici il faut lire toutes les valeurs avant calcul. Si, par chance, la taille du tableau est fixe et connu lors de la conception, il est possible de déclarer le tableau entièrement lors de l'écriture du code.

Mais ce n'est pas toujours la bonne solution. Toute la place mémoire du tableau est réservée dans le fichier exe qui devient rapidement très volumineux. Même si l'époque des disquettes est révolue cela peut être gênant. La bonne solution est d'utiliser les tableaux dynamiques, elle est expliquée dans le paragraphe suivant.

Mais avant, un petit mot sur un inconvénient peu connu des données tabulées. Leur utilisation demande beaucoup de temps CPU !

Prenons le cas classique ou une conductivité est tabulée par pas de 10 degrés entre 0 et 1000 C. Pour estimer la conductivité à 753 C, il faut parcourir tout le tableau, puis interpoler à partir des valeurs les plus proches. A ma grande surprise, lors du profilage du code c'est cette phase qui prend le plus de temps CPU ! Je l'ai remarqué dans plusieurs cas distincts.

La bonne solution est de remplacer le tableau de chiffres par une formule interpolée. Cela demande plus de travail de la part de l'utilisateur et du concepteur, mais le gain en temps est appréciable.

2.4.b.tableaux dynamiques

Ils n'existent pas en Fortran 77, et c'est pour cela que je conseille de ne plus l'utiliser. J'ai longtemps été un adepte enthousiaste du Fortran 77 avant de passer au Fortran 90 puis au C.

Avant de lire le tableau, il faut "allouer la mémoire" (ce qui veut dire réserver de la place et se souvenir de l'adresse de début) avec la fonction malloc ; calloc. Pour réserver une place suffisante il faut connaître la taille du tableau. Aussi, tant que c'est possible j'indique le nombre de lignes à lire dans le fichier avant le tableau. Il faut pour cela connaître sa taille avant la constitution du fichier :

xx ! Nombre de point dans la table
température1   conductivité1
température2   conductivité2
...
[END]

Malheureusement, souvent la taille du tableau ne peut pas être connue avant l'exécution du programme.

La technique simple est de lire une première fois le tableau pour compter les lignes, puis faire l'allocation de mémoire, revenir en début de tableau (avec les fonctions fsetpos et fgetpos et enfin lire réellement le tableau. Avec la fonction realloc, ce n'est pas nécessaire la place mémoire est augmentée avec les besoins.

Voici un exemple

Les lignes sont ajoutées au tableau jusqu'à celle qui contient [END] (ou que l'on atteigne la fin de fichier). Les éléments du tableau sont stockés dans des structures. Notez que l'ordre scan et ses variantes n'est pas utilisé, ce qui permet d'utiliser ses propres tests de validité des données.

3.Affichage lors du déroulement

3.1.Contrôler la quantité d'information à l'écran et dans le fichier de trace

Lors de leurs exécutions, mes programmes écrivent des messages sur leur déroulement à la fois à l'écran (sur stdout) et dans un fichier (de trace ou de log). Les messages à l'écran permettent de rassurer l'utilisateur en l'informant que le programme n'est pas planté. Le fichier de trace permet d'analyser un calcul terminé surtout en cas d'erreur.

3.1.a.affichage à l'écran

Les messages à l'écran doivent être succincts et peu nombreux pour éviter de trop ralentir le calcul. D'ailleurs de trop nombreux messages ne sont en général pas lisibles car ils défilent trop vite sur les ordinateurs modernes. Dans le cas ou le programme est lancé automatiquement et régulièrement en tâche de fond, il est préférable de ne pas envoyer du tout de messages.

C'est pourquoi il faut laisser le choix à l'utilisateur d'afficher ou non ces messages. Pour cela, une variable globale (appelée verbo) contrôle chaque écriture à l'écran.

if (verbo>0) {fprintf(stdout,"Open data file : %s\n",fichdat); fflush(stdout); }

L'ordre fflush(stdout) permet de forcer l'affichage immédiat (nécessaire avec Mingw).

Dans le programme, la variable verbo ne peux prendre que la valeur 0 (pas d'affichage) ou 1. Sa valeur par défaut est à 1, mais elle peut être modifiée dans la ligne de commande. Dans le cas ou le programme comporte plusieurs fichiers séparés, elle doit être déclarée dans chaque module avec le qualificatif extern sauf dans le programme principal.

3.1.b.messages dans le fichier trace

Le fichier trace doit contenir plus d'informations pour permettre l'analyse du calcul. Pour mieux contrôler la quantité d'information j'utilise une technique similaire à la précédente, la variable debug peut prendre plusieurs valeurs.

Voici leurs déclarations

/* Declare common variables
   ------------------------ */

int         verbo=1;                           //!< trace flag (display)
int         debug=2;                           //!< trace flag (file)
                                               //!< 0 = no message
                                               //!< 1 = main steps
                                               //!< 2 = 1 + global results
                                               //!< 3 = all results

Il existe aussi la possibilité d'utiliser les directives de compilation pour contrôler la quantité de messages écrits. Avec cette technique, le code est mieux optimisé. Mais l'utilisateur n'a plus la main. De plus il faut produire plusieurs exécutables, un avec l'affichage des messages et un sans, ce qui complique la gestion exécutables installés chez les utilisateurs.

4.Gestion des erreurs

4.1.Difficile mais aussi important que le manuel

L'idée est d'éviter d'avoir un programme qui s'arrête brusquement et laisse l'utilisateur perplexe sans indications pour surmonter l'erreur. Malheureusement, il n'y a pas de recette simple. Les techniques existantes de vérification des codes sont relativement complexes et sont une branche à part du métier de développeur.

La solution radicale est de tester toutes les exceptions possibles, par exemple vérifier que le dénominateur n'est pas nul avant de faire une division. En fait cela n'améliore pas beaucoup le comportement du point de vue de l'utilisateur. Au lieu d'avoir un message incompréhensible en anglais du style overflow at xxxxxx il aura un message de votre cru du style erreur: le dénominateur est nul dans le module trucmuche. C'est mieux, mais cela n'indique pas ce qu'il faut faire.

Donc il ne vous reste que quelques astuces, les resources de votre intelligence et la volonté d'écouter vos utilisateurs.

Une pratique courante que chaque fonction retourne un code d’erreur sur son déroulement. Un exemple est au chapitre 2.3 la fonction litreel initialement s’utilisait par

    mon_reel = litreel(dat, imp);

Maintenant la fonction s'appelle par :

    ErrCode = litreel(dat, imp, &mon_reel);

Ce qui laisse la possibilité de traiter les erreurs de lecture (en plus des erreurs de validité de la donnée).

La meilleure solution est de capter l'anomalie le plus tôt possible et d'en avertir l'utilisateur le plus clairement possible. Enfin, ceci est plus facile à dire qu'à faire et nécessite beaucoup de temps lors de la phase de test du programme. Bien sûr, la liste des erreurs reconnues est ajoutée à la notice.

4.2.Filtrer les entrées

L'introduction des données est un point crucial où sont introduites la majorité des erreurs pour un programme stable. Une attention particulière doit être portée sur la vérification de la validité des valeurs. C'est pour cela qu'il faut se forcer à définir un plage de validité pour chaque donnée lue.

Vous trouverez un example de test de validité dès la lecture des données un peu plus haut. Cet example affiche les erreur avec un utiliaire "alarme"."

Voici la fonction d'affichage de l'erreur

L'erreur est reportée à la fois à l'écran et dans le fichier de trace. Il y a quatre niveaux d'erreur gradués entre MSG et GRAVE. Seul le dernier cas arrête immédiatement le programme. L'explication contextuelle de l'erreur est écrite dans la chaîne err_msg directement dès qu'elle est levée.

4.3.Regrouper la liste des erreurs

La façon de faire précédente peut encore être améliorée. Avec cette façon simple d'écrire, les numéros d'erreur ainsi que leurs libellées sont définis un peu n'importe où dans le programme. Il y a un risque d'utiliser deux fois le même numéro pour deux erreurs différentes.

Une astuce est d'utiliser un enum pour définir les n° d'erreurs dans un fichier "header". En effet, le numéro de l'erreur n'a pas de signification en lui même, c'est juste un code interne au programme.

Un enum, permet de attribuer un pseudo-indicateur plus explicite. Appeler l'erreur BUFFER_FULL au lieu de l'erreur n°163, c'est plus lisible pour le programmeur.

#ifndef ERRORLIST_H_
#define ERRORLIST_H_
/** @enum ErrorCode
 ******************
 *
 * @brief All error codes used in the source.
 */
enum ErrorCode {
    // parameter error code
    WRONG_TYPE_SIMUL=160,
    WRONG_TYPE_REPORT,
    NEGATIVE_TIME_STEP,

    // run time error
    BUFFER_FULL,
    ERFC_MATH_ERROR,

};

#endif  /* ERRORLIST_H_ */

L'autre avantage et que lorsque on écrit la documentation, la liste de toutes les erreurs est déjà établie, reste à la documenter.

C'est la seule utilité des enum (à mon avis) !

5.La présentation des résultats dans un fichier

Leur forme est souvent de peu d'importance, il est rare que ces fichiers soient utilisés tel quels. Mais autant le rendre facile à lire une fois ouvert dans un éditeur de texte. Nous y reviendrons dans faire de belles sorties.

Ici il n'y a qu'un point important: pas de fichier orphelin, c'est à dire dont on a le résultat mais pas d'indications sur les données initiales, le nom et la version du programme de calcul, la date et l'heure du calcul.

6.Documentation, fichier de test, fiche de vie, etc

6.1.La documentation fait la qualité du programme

Écrire la documentation prend beaucoup de temps en fait autant que d'écrire le programme. Cette phase est souvent négligée alors que c'est une part importante de la qualité perçue d'un programme.

Sur le long terme il est payant d'écrire une bonne documentation, aussi considérez ce travail avec la même valeur et la même priorité que d'écrire le programme lui même.

6.2.Le HTML pour son universalité

J'écris la documentation en html, car c'est le langage de description d'hypertexte le plus simple et le plus universel. Il existe d'autres choix comme le postscript, le pdf, chm (standard Windows), voir doc Word. Mais html reste universel et il est relativement facile de le convertir dans les autres formats. Son principal défaut est d'être souvent mal paginé une fois imprimé.

La notice est souvent lue sur un écran, donc je vous conseille de privilégier une police de caractère simple comme Helvética. Certaines notices écrites avec la police "Times new roman" sont très jolies une fois imprimées, mais difficiles à lire à l'écran.

6.3.Un contenu pour plusieurs niveaux de lecture de la documentation

La documentation est lue par plusieurs catégories de personnes (Utilisateur, programmeur mainteneur, informaticien système, contrôleur qualité). Donc elle doit contenir de divers items dont voici une liste non exhaustive.

Pour tous
l'objet du programme, ce que fait le modèle
le domaine d'utilisation en confiance, hors confiance,
la théorie sur lequel il est basé,
la liste de l'évolution des versions
Pour l'utilisateur
la description du fichier de données (avec exemples),
les limites de validité,
la description du fichier de résultats,
l'utilisation du modèle en ligne de commande et/ou avec un GUI,
liste des erreurs reconnues,
FAQ
Pour informaticien
l'installation avec la liste des fichiers utiles, la désinstallation,
les programmes tiers,
ressource machine, temps utilisateur,
évolution des versions
Pour le développeur mainteneur
La liste des fichiers source, de la notice et des exemples
le nom du compilateur utilisé,
la description de la technique numérique si elle est particulière,
un lien vers la sortie de doxygen,
liste des bugs connus,
liste des souhaits de développement
Pour le contrôleur qualité
un lien vers le compte rendu d'audit

Ne pas oublier de rappeler la date de dernière mise à jour et le numéro de version.

6.4.Fiche de vie du programme

Les professionnels recommandent d'établir une fiche de vie du programme. C'est en fait un tableau regroupant les évolutions datées des versions avec un rappel succinct des modifications, accompagné des validations (jeux test et résultats de calcul), du nom du valideur. Il peut y avoir une liste de bogues et une liste de souhaits d'améliorations.

A chaque évolution notable (version stable), il faut archiver tous les fichiers. Il arrive de faire des améliorations qui n'en sont pas et de devoir reprendre la version antérieure.

Le contenu minimal de la fiche de vie est :

  • Identification du logiciel
  • Historique des versions
  • bugs ouverts ou todo list

Exemple du contenu de l'historique des versions.

 Date  Numéro de
version
Nature des modifications
Remarques
Identification
des jeux tests associés
Visa du responsable
de la validation

Oct 2005 0.01 Ecriture initiale de toto jeux1 Moi même
Nov 2005 0.02 Version développement, ajout du module bidule jeux2 encore moi même
Oct 2006 0.03 Correction de bug dans le module bidule jeux2 son pote

6.5.Bien commenter le code

Ce sujet fait l'objet de très nombreuses discussions sur le net, elles sont parfois passionnées. J'ai tendance à être un peu verbeux et redondant.

    Les règles de base sont :
  • rendre le code agréable à lire, c'est à dire l'aérer
  • montrer la structure du code au premier coup d'oeil, mettre des titres, indenter, ...
  • expliquer la fonctionnalité attendue de chaque élément de la structure
  • expliciter chaque variable par un nom explicite et un commentaire (spécialement pour les variables globales ou communes)
  • avertir sur les exceptions possibles et sur les cas particuliers

Penser au développeur nouveau sur ce projet. Souvent des commentaires qui paraissent clairs voir superflus lors de l'écriture deviennent hermétiques lorsque le contexte du développeur n'est pas connu ou a été oublié.

C'est encore plus vrai pour les astuces et les corrections. De fait la majorité des parties obscures d'un programme viennent de la chasse initiale aux bogues. Le développeur stressé parce que son programme ne réagit pas comme il le devrait ne commente pas les modifications.

Si vous le pouvez, utilisez l'anglais pour les commentaires. En français, l'audience informatique est vraiment réduite.

6.6.Eviter les inclusions multiples

Dans le code j'utilise souvent la macro preprocesseur #define nom_explicite valeur pour remplacer une valeur symbolique pour un nom plus explicite pour le programmeur. Mais ceci peut engender dre erreur d'édition de lien en cas d'inclusions multiples.

La directive pragma once permet d'éviter cela. Sinon il faut le fairer manuellement comme on le voit dans l'example avec enum.

6.7.Se faire un beau listing avec enscript

Enscript est fourni avec toutes les distribution Linux. Il effectue la mise en page de listings avant impression. Il est possible de s'en servir pour se faire un beau listing coloré en html.

$enscript -E --color --language=html --toc -pfoo.html *.h *.c

kwrite et la pluspart des éditeurs modernes permet également de sauvegarder la coloration syntaxique dans un fichier html.

6.8.Utiliser doxygen

doxygen extrait certains commentaires du code et les assemble dans une documentation technique hypertexte. Cette documentation est orientée pour les développeurs et la maintenance de l'application. Elle ne dispense donc pas d'une notice d'utilisation et de description du contenu scientifique.

Utiliser doxygen nous invite à commenter correctement le contenu de chaque fonction, la nature des paramètres d'entrée, les bugs et autre. L'avantage principal est de retrouver rapidement ces informations sans avoir à "fouiller" dans tous les fichiers.

Documenter un code pour que doxygen en fasse une bonne documentation n'est pas si évident que cela. Voici quelques conseils, très loin d'être exaustifs.

7.Validation et jeux tests

Seul l'utilisateur final est juge de la justesse et stabilité du code. Pour le satisfaire, la qualité commence dès l'écriture et un code bien structuré et documenté facilite l'atteinte de cet objectif.

7.1.La validation lors de l'écriture

Le développeur prend rapidement l'habitude de tester chaque blocs de lignes nouvellement écrites. Le piège est de ne vérifier que dans les cas attendus, les plus courants que le résultat est conforme aux attentes. Par exemple dans le programme simple du début il faut tester son comportement lorsque l'entrée:

  • n'est pas un nombre,
  • est un nombre très grand
  • à un séparateur virgule ex: 45,6 au lieu de 45.6
  • à un séparateur de millier ex: 1 023.5 au lieu de 1023.5

Notez que dans certains cas, le programme semble avoir fonctionné normalement (cas du nombre avec virgule). Le programme ne proteste pas et le résultat semble plausible. Mais il est faux !!!

Les deux derniers cas ne sont pas si exotiques, surtout lorsque ces données sont issues d'un autre programme. Excel #@$ m'a déjà causé beaucoup de misères de ce type !

Citons au passage un grand classique du bug: les noms de fichier comportant un espace comme le célèbre "Mes Documents". Un programme insuffisamment testé ne lit que la première partie du nom Mes et bien sur ne trouvera pas le dossier.

7.2.Les exemples ou jeux tests

Les fichiers de validation (ou jeux tests) ont beaucoup de valeur. Il faut absolument archiver les fichiers de données avec leurs résultats à coté du code source et de la notice.

Ils servent aux utilisateurs novices, mais aussi aux futurs développeurs. Cela permet de vérifier que les améliorations ne dégradent pas les résultats.

8.Internationalisation

C'est la rançon du succès et aussi un effet de la mondialisation. Pour l'instant tous les messages du code de calcul sont de préférence en anglais.

Un utilitaire comme gettext permet de traduire les messages et de maintenir le fichier de traduction. Le principal inconvénient est qu'il n'est pas possible de préciser complètement le dossier où se trouvent les messages traduits. Ils sont placés dans le sous dossier xxxx/fr/LC_MESSAGES ou xxxx est le nom du dossier qui peut être défini par le développeur.

Cela peut poser des difficultés lors de l'installation de l'ensemble sur un autre ordinateur.

9.Créer une DLL avec DevC++

Un petit ajout sur la création d'une DLL. Je ne reviens pas sur l'utilité d'une DLL, même si en pratique il y aurait à dire.

Ce n'est pas ma spécialité, donc cette section sera très basique. Je me suis beaucoup inspiré des documents "Tutorial : Utiliser des DLL" des auteurs "Charles « Xs » « Delire8 » « Xcept » Langevin" et "La compilation séparée en C" par Jessee Michaël C. Edouard.

9.1.Première étape : création de trois dossiers

Commençons par créer le dossier du projet que j'appelle ici "essai_dll". Nous aurons besoin d'un minimum de trois sous dossiers.

  • un pour écrire et compiler la DLL, appelé ici "ma_dll"
  • un pour écrire et compiler le programme utilisant la DLL, il est appelé ici "test_dll"
  • un pour executer les tests, il est applelé simplement "bin"

Les second et troisième dossiers pourraient être rassemblés. On risque alors de le pas repérer un appel à la DLL par un lien "en dur", c'est pourquoi je préfère les séparer.

9.2.seconde étape créons la DLL

Ouvrons dans DevC++ un nouveau projet de type "DLL" ! Le template propose alors deux fichiers (.c et . h) que nous allons légèrement modifier pour obtenir ceci.

DLL très simple (s.v.p. cliquer sur la ligne)

 

Le fichier de déclaration contient :

C'est un peu abscon, et il ne faut pas chercher à comprendre ! Par rapport aux références citées au dessus j'ai ajouté __stdcall. Je ne sais pas exactement ce que cela fait mais ceci est nécessaire pour pouvoir appeler la DLL à partir de Excel.

Je sais, l'addition est fausse, mais ici c'est fait exprès !

Pour info le fichier make file contient (simplifié seulement la partie en C)

Compiler, puis vérifier que le fichier ma_dll.dll est bien créé.

9.3.Troisième étape : créer un programme de test

Dans DevC++ créer un nouveau projet de type "console" et enregistrer sous le nom "test_dll".

Remplacer le contenu du fichier exemple par celui ci :

Utilisation simple de la DLL (s.v.p. cliquer sur la ligne)

A ce stade, la compilation se passe bien, mais pas l'édition des liens. Il faut indiquer au programme que l'on utilise "ma_dll.dll" comme "library". Cela se voit le make file suivant sur la ligne LIBS.

Pour info le fichier make file contient (simplifié seulement la partie en C)

A ce stade l'executable test_dll.exe doit avoir été créé.

9.4.Quatrième étape : tester le fonctionnement

Mettre le fichier test_dll.exe dans le dossier bin, puis le "double cliquer". Comme le programme ne trouve pas la DLL, il doit afficher une erreur.

erreur chargement <b>DLL</b>

Ajouter le fichier ma_dll.dll dans le dossier bin et recommencer. Cette fois ci cela marche on doit obtenir ceci :

première execution

Le calcul dans la DLL est faux ! Normal, on l'a fait exprès pour vérifier que l'opération est bien réalisée par la nouvelle DLL. On peut maintenant retourner à l'étape 1 et corriger la fonction addition. Recréer une DLL et remplacer la première version dans le dossier bin.

Double cliquer sur le fichier exe, et normalement ont doit obtenir ceci :

seconde execution

L'erreur est corrigée sans que l'on ait besoin de modifier ni de réinstaller le programme test_dll.exe.

10.Faire fonctionner un programme de calcul en se servant de Excel comme interface

10.1.A quoi sert vraiment d'écrire une DLL ?

La justification habituelle de passer à l'écriture DLL est la possibilité de corriger des erreurs (bugs) sans tout réinstaller (comme c'est décrit juste avant). En pratique et dans le cas d'une amélioration du noyau de calcul, il est aussi facile de remplacer tout l'exe, celui-ci étant rarement excessivement volumineux. Une autre utilisation classique est de confier des tâches spécifiques à des fonctions écrites par d'autres développeurs. Cette technique permet de développer tout en ne concervant qu'un conctact minimal vial l'API avec le concepteur du module. Celles-ci étant génériques et disponible sur le net.

Pour nous, la véritable utilité d'une DLL est de pouvoir relier le noyau de calcul à un autre environnement. En particulier, cela permet d'utiliser Excel comme interface utilisateur.

Il y a plusieurs étapes à réaliser pour faire un programme de calcul en se servant d'Excel comme interface

    1) écrire les données dans une feuille Excel, que l'on appelle "Data" ou "Données" (un peu par manque d'imagination et surtout parce que c'est le plus logique !).

    2) créer un bouton pour activer une fonction VBA qui envoie les données vers une fonction de la DLL

    3) créer un bouton qui active une fonction VBA qui lance le calcul et indique uand celui ci est terminé.

    4) créer un bouton qui copie les résultats et les affiche dans un feuille de calcul, que l'on appelle "Result" ou "Résultats".

Cela peut être le même bouton et la même fonction VBA qui effectue les opérations 2 à 4 en particulier si le calcul est court.

Ces multiples étapes font que les occasions de bug sont multipliées par rapport à un fonctionnement classique. En particulier, il y a au minimum 3 (mais je conseille 4) implantations d'une donnée en mémoire informatique:

Notons que l'on précise le chemin complet de la DLL en dur. Cela contreviens à l'esprit de ce texte car cela entrave la portabilité de l'interface. Mais c'est une limitation de Excel ! Par défaut Excel recherche les DLL dans un dossier spécifique du système ou dans son dossier de travail, mais pas dans celui ou se trouve fichier xls! Cette écriture "en dur" n'est pas très pratique pour l'installation d'un modèle sur un autre ordinateur. Il est possible d'améliorer cela (voir plus loin).

Le second point à noter est que les variables sont déclarées ByVal pour respecter la recommandation précédente. Le troisième point est que les entiers sont déclarés en As Long.

Et le le contenu de la feuille Excel qui effectue la somme de "a+b" de quatre façons différentes (dans Excel, par VBA, par la DLL via la VBA et par appel direct).

feuille excel appel dll

Dans les trois appels à des fonctions VBA ou la DLL (via VBA) les données sont converties en entiers longs. Du coup le résultat obtenu est tronqué, donc pas correct ! Il est possible d'écrire des DLL respectant les types génériques de Excel, mais cela dépasse la simple utilisation de Excel comme interface de modèle.

10.3.Localiser la DLL

Revenons au chemin de la DLL, j'utilise une astuce d'un collègue Belge. La lettre du disque et le chemin d'accès sont indiqués dans la feuille Excel. Le script suivant permet de changer le dossier de travail et ainsi charger correctement la DLL. L'utilisateur n'a qu'à indiquer le chemin dans le bonnes cellules lors de l'installation du fichier de la DLL.

Avec cette astuce, il n'est plus nécessaire d'indiquer le chemin dans les déclarations des fonction en VBA, on peut se contenter de donner le nom de la DLL.

Localisation de la DLL flexible (s.v.p. cliquer sur la ligne)

La localisation doit être executée chaque fois que le classeur du modèle est ouvert. Je n'ai pas cherché à la faire exécuter automatiquement à l'ouverture, elle est activée paril suffiet d'appuyer sur un Bouton [INIT] sur la feuille de données.

indication du chemin dll

10.4.Modifications pour rendre un modèle compatible avec une DLL pour Excel

La première modification concerne la lecture des données en entrée. Celle-ci sont regroupées dans une feuille appelée Data page. Il faut remplacer les fonctions de lecture dans un fichier. Cela nous demande de créer trois objets:

Fonction VBA d'envoi des données (s.v.p. cliquer sur la ligne)

Dans cette exemple, le nom de la feuille Excel est indiquée en dur. Donc si l'utilisateur la renomme, la fonction VBA ne fonctionnera plus ! Une solution est de remplacer Worksheets("Data page") par Activesheet.

Mais cela ne resoud pas tous les problèmes, en particulier si l'utilisateur insère des lignes ou des colonnes, la fonction VBA ne sera pas mise à jour. C'est pour cela que les adresses des cellules à lire sont indiquées via les variables ncol et nligne pour pouvoir corriger rapidement.

Voici la fonction de la DLL qui récupère ces données

L'exemple précédent n'est pas complet. Il manque les tests sur la validité des données data_xls, si ces données ne sont pas des doubles ni dans la plage permise, il faut renvoyer une code d'erreur. Pour l'instant je ne sais pas traiter en interne le cas ou Excel envoie une donnée non numérique à la fonction. Heureusement le debugger de VBA le détecte (à vérifier).

La seconde transformation est de ne plus utiliser des entiers de type int mais de type long, les remplacer dans le code ne pose pas de problème, ni ne ralenti son execution.

Un autre conseil est de bien faire remonter les erreurs du modèle vers Excel/VBA. Cela implique de modifier sa façon d'écrire les programmes comme dans l'exemple précédent.

Il deviens difficile d'afficher le déroulement de calculs long, même lorsque l'on traite de "données de production" (comme dans affiche). L'utilisateur peut avoir l'impression que soit Excel est figé (Application.ScreenUpdating = True permet d'y remédier), soit qu'aucn calcule ne se passe. Je n'ai pas trouvé de solution correcte sauf à prévenir l'utilisateur ou à découpé l'executin en plusieurs morceaux (avec autant d'appel à la DLL).

10.5.Quelles types de variables peut-on communiquer entre la DLL et Excel

Il est possible d'envoyer des long, double, char (un seul dans le send DLL vers VBA!), un table par l'adresse du premier élément et même des structures..

Il n'est pas possible d'envoyer simplement un entier court int.

Par contre je ne sais pas récupérer une chaine de caractères.


Retour à la première page. Dernière révision le 5/Feb/2017