Retour à la première page.
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.
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.
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:
Voici mes astuces qui permettent de les gommer.
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.
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.
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).
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:
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
.stdlib.h
) EXIT_SUCCESS ou EXIT_FAILURE en cas de problème.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.
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.
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>
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.
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 :
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.
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.
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:
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)
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 :
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 !
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.
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.
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.
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.
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.
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.
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.
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"."
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.
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) !
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.
É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.
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.
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.
Ne pas oublier de rappeler la date de dernière mise à jour et le numéro de version.
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 :
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 |
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
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.
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.
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 !
Compiler, puis vérifier que le fichier ma_dll.dll est bien créé.
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 :
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.
A ce stade l'executable test_dll.exe doit avoir été créé.
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.
Ajouter le fichier ma_dll.dll dans le dossier bin et recommencer. Cette fois ci cela marche on doit obtenir ceci :
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 :
L'erreur est corrigée sans que l'on ait besoin de modifier ni de réinstaller le programme test_dll.exe.
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:
- une pour l'affichage dans la case de la feuille
- une pour la valeur réceptionnée dans la fonction VBA
- une pour la valeur réceptionnée par le programme C (lorsqu'elle est transmise par valeur)
- une pour le calcul, car je conseille de copier la donnée précédente dans une variable interne à la DLL
Je préfère que la DLL calcule avec ses propres variables en zone mémoire. Cela évite de faire de multiples "long jumps" et de ne les raliser que au moment des échanges entre Excel et la DLL. C'est pour cela que je conseille de copier immédiatement la valeur reçue dans une variable interne à la DLL.
Voici un module VBA de taille minimale, ici le calcul est tellement simple qu'il n'y a pas lieu de faire des copies locales des variables:
La première ligne avec le nom du module est facultative.
Mais pour chaque fonction de la DLL, il y a deux parties: en premier les déclarations des fonctions de la DLL (obligatoire), puis des fonctions ou subroutines en VBA qui appellent les fonctions de la DLL proprement dites.
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).
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.
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.
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.
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:
- Des fonctions qui acceptent les données envoyées par VBA.
- Une ou plusieurs subroutines VBA qui récupèrent les données dans cette feuille et les envoient à la DLL.
- Un boutton dans Excel pour activer cette fonction (il y a plusieurs façons de faire).
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.
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).
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