|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
Cet article a été largement inspiré par un travail que j'avais à effectuer il y a de cela quelques mois, à savoir la réalisation d'une bibliothèque de fonctions mathématiques partageable par plusieurs tâches. C'est-à-dire, dans la terminologie propre à Amiga, une "shared library" (bibliothèque partagée). J'avoue qu'à cette époque si j'avais possédé LibTool, j'aurais largement augmenté mon temps de sommeil... Lorsque, sur un ordinateur, vous développez de grosses applications, vous êtes obligés, à un moment ou à un autre, de découper votre programme en plusieurs morceaux. Ce découpage peut être réalisé de deux manières différentes : soit grâce à des overlays, soit à l'aide de bibliothèques. Une structure à base d'overlays est, je vous le rappelle, une décomposition sous forme arborescente de votre programme, en morceaux (généralement des fonctions distinctes du programme) qui sont chargés et déchargés lors de leurs appels, le chargement et le déchargement étant effectué automatiquement. Très simples à mettre en oeuvre, les overlays ont toutefois deux inconvénients : d'abord la complexification rapide de l'arborescence pour de très grosses applications, et les risques de duplication du code si les overlays sont mal conçus (pour de plus amples informations sur le sujet reportez-vous à l'article "Les Overlays" paru dans le numéro 27 de l'ANT). Pour remédier à ces problèmes, on utilisera la notion de "library" ou, en français bien de chez nous, de bibliothèque de fonctions. Cette notion consiste à regrouper sous la forme de composants logiciels, généralement thématiques, des fonctions indépendantes de bas niveau (fonctions mathématiques ou de gestion des entrées/sorties par exemple). Lorsque votre programme appelle cette fonction, la bibliothèque associée est chargée en mémoire. La fonction est exécutée et les résultats sont renvoyés au programme principal. Totalement satisfaisante sur des machines monotâches, cette méthode montre vite ses limites sur des machines multitâches : comme ces bibliothèques sont indépendantes de tout autre composant logiciel, lorsque deux programmes utilisent la même bibliothèque, cette dernière est recopiée deux fois en mémoire. Cette surconsommation de mémoire étant, vous l'avouerez, très problématique, il était nécessaire d'y remédier en introduisant le concept de bibliothèques partagées. Une bibliothèque partagée n'est plus, cette fois, totalement indépendante, mais liée au système d'exploitation. Ce dernier est donc en mesure de savoir, lorsqu'une tâche appelle une bibliothèque, si cette dernière est déjà présente en mémoire et dans l'affirmative, ne la recharge pas. Le code associé aux bibliothèques partagées étant réentrant plusieurs applications peuvent alors y accéder en même temps. Toute série de routines qui est susceptible d'être utilisée par plusieurs applications doit donc être incluse dans une bibliothèque. Des exemples classiques de ce regroupement peuvent être facilement trouvées au niveau du système de l'Amiga comme les bibliothèques Graphics et Intuition. Enfin, dernier avantage des bibliothèques de fonctions, vous pouvez offrir à d'autres développeurs des fonctions sous forme de bibliothèques qu'ils peuvent utiliser dans leurs propres programmes sans que vous soyez obligé de donner les sources associés ou que vous les obligiez à utiliser un langage particulier. Bienvenue dans un monde meilleur... A l'époque ou j'étais intéressé par la construction d'une bibliothèque de fonctions, j'ai étudié un grand nombre d'exemples dont le point commun était une complexité certaine, tant au niveau de la conception de la bibliothèque qu'au niveau de sa maintenance et de son évolution. Ayant découvert LibTool et ayant été surpris par sa simplicité d'utilisation, j'ai donc décidé de vous expliquer son fonctionnement, sur un exemple simple, afin que vous puissiez vous aussi goûter aux joies des bibliothèques sans les inconvénients liés à leur construction. Notre exemple va consister à convertir une série de fonctions C travaillant sur les vecteurs (addition, soustraction, produit scalaire et produit vectoriel) en une bibliothèque partagée. On utilisera, outre LibTool, le compilateur Lattice C (vous pouvez faire de même avec votre compilateur favori). La première chose que nous allons étudier c'est la structure d'une bibliothèque partagée. Structure d'une bibliothèque partagée Avant d'étudier cet exemple, il me semble nécessaire d'effectuer quelques rappels sur la création des bibliothèques sur Amiga. Une bibliothèque de fonctions partagées se compose de quatre parties distinctes :
Figure 1 Cette structure est définie comme suit : Les champs "lib_Version" et "lib_Revision" sont utilisables pour marquer la version exacte de la bibliothèque à un moment donné (pour effectuer des contrôles de versions lors de l'appel à cet bibliothèque). Le champ "lib_IdString" est un pointeur sur une chaîne de caractères qui peut contenir des informations d'ordre général sur le produit (un texte de votre choix en quelque sorte). "lib_Sum" est le champ qui contient la somme de contrôle (checksum) associée à la bibliothèque. Cette somme de contrôle est utilisée par Exec pour s'assurer de l'intégrité de la bibliothèque une fois son chargement effectué. Ceci permet de contrôler d'éventuels écrasements mémoire dus à d'autres tâches. Enfin, "lib_OpenCnt" est un compteur qui est incrémenté à chaque ouverture de la bibliothèque par OpenLibrary() et décrémenté à chaque fermeture par CloseLibrary(). Lorsque ce compteur vaut zéro cela signifie que plus personne n'utilise la bibliothèque et, par conséquent, Exec peut au besoin la décharger pour récupérer la place mémoire. Une fois les paramètres associés à cette structure entièrement renseignés, nous pouvons créer la Vector Table définissant les différents décalages ("offsets") associées aux fonctions. Chaque élément de cette table est codé sur six octets. Les deux premiers représentent le codage d'une instruction assembleur de saut (JMP) et les quatre suivants, l'adresse de saut à cette fonction. Cette table de vecteur est référencée en négatif par rapport à l'adresse de base. Par exemple le vecteur associé à la fonction numéro 6 se trouve à l'adresse -36. Ces multiples de six sont également appelés "Library Vector Offsets" ou LVO dans les publications anglo-saxonnes. Fonctions privées En plus des fonctions applicatives proprement dites, il est nécessaire, pour chaque bibliothèque, d'écrire quatre fonctions de gestion nommées Open(), Close(), Expunge() et Reserved().
Pour créer une bibliothèque, il vous faudra donc, outre vos propres routines, compiler et relier les quatre fonctions de gestion de la bibliothèque, la table des vecteurs de saut et la structure RomTag. Il s'agit là d'un travail en général pénible et répétitif, et qui peut être sans trop de problèmes automatisé - c'est justement ce que LibTool se propose de faire. Règles de programmation Avant d'utiliser LibTool, il est nécessaire de définir quelques règles de programmation pour la création d'une bibliothèque. Primo, le code source que vous développez doit être réentrant, ce qui signifie que vous ne devez en aucun cas faire usage de variables globales, tout du moins pas sans certaines précautions. En effet, comme il est possible que plusieurs tâches utilisent en même temps la bibliothèque, voire la même fonction, elles peuvent accéder en même temps en écriture aux variables globales qui sont, quant à elles, uniques. Je vous laisse le soin d'imaginer l'imprévisibilité du fonctionnement de tels programmes. Secundo, si votre bibliothèque a besoin, pour une raison ou pour une autre d'utiliser des fonctions appartenant à une autre bibliothèque, vous devez obligatoirement ouvrir cette dernière au sein de la fonction Open() et la refermer lors du déchargement de la bibliothèque dans la fonction Close(). Un exemple typique d'une telle utilisation, est une bibliothèque utilisant les fonctions internes de l'Amiga (Graphics, Intuition...). Tertio, vous ne devez pas appeler de fonctions du style printf() ou scanf() utilisant l'une des trois entrées/sorties standard stdin, stdout et stderr. La raison en est simple, ces entrées/sorties standard ne sont pas accessibles aux bibliothèques partagées. Pour gérer vos erreurs, une solution de remplacement consiste à renvoyer de simples codes d'erreur qui seront traités au niveau du programme appelant. Description de notre exemple La bibliothèque d'exemple que nous allons créer contient quatre fonctions de traitement vectoriel dans l'espace. Cet exemple a le gros avantage d'être très court tout en reflétant correctement l'usage des bibliothèques. Ces quatre fonctions sont :
Afin de vous aider à écrire le code de l'application qui utilisera la bibliothèque, LibTool génère aussi un fichier en-tête constitué soit de routines "glue" (de liaison), soit de pragmas. Les routines de liaison sont utilisées pour récupérer les arguments passés via la pile et les stocker dans les registres appropriés avant d'appeler la bibliothèque. Bien sûr, l'utilisation de pragmas élimine automatiquement les routines de liaison. Les données nécessaires à la réalisation de ces différents éléments seront transmises à LibTool via un fichier ".FD". Bien connu des programmeurs sur Amiga, ce fichier contiendra des commandes supplémentaires reconnues uniquement par LibTool. Voici celui correspondant à notre bibliothèque : La syntaxe de cette commande est la suivante :
L'option "-c" indique à LibTool que notre bibliothèque de fonctions est écrite en C et par conséquent, que l'application va passer les arguments via la pile. L'option "-h" indique qu'il est nécessaire de créer les includes associés aux fonctions de la bibliothèque pour les inclure dans l'application. Enfin, l'option "-m" demande de créer le startup-code associé à la bibliothèque. L'option "-o glue.asm" demande la création du fichier de liaison contenant les stubs pour chaque fonction. Si nous avions voulu obtenir un fichier pragmas nous aurions rajouté l'option "-p". Une fois la commande exécutée, vous aurez dans le répertoire courant trois nouveaux fichiers (glue.asm, veclib.h et veclib.src) représentant respectivement le fichier de liaison, les includes et le startup-code de la bibliothèque. Il ne reste plus désormais qu'à :
Voici l'ensemble des commandes résumées pour obtenir notre bibliothèque avec les deux compilateurs les plus célèbres du marché. Pour le Manx :
Pour le Lattice
Pour utiliser notre bibliothèque, il suffit dès lors d'inclure dans le fichier source les includes générés :
...et d'ouvrir notre bibliothèque au début du programme, comme n'importe quelle autre bibliothèque système : Pour le Manx
Pour le Lattice
Voilà, notre application est prête à fonctionner. Il existe bien entendu de nombreuses précisions à apporter sur les bibliothèques partagées.
|