Création et utilisation de DLL avec Delphi
Date de publication : 12/08/05 , Date de mise à jour : 19/08/05
Par
Olivier Lance (olance.developpez.com)
Apprenez à créer simplement des DLL avec Delphi !
Mes autres articles se trouvent sur
Introduction
I. Création du projet
I-1. Création du fichier
I-2. "library" plutôt que "program"
I-3. L'avertissement de Borland
II. Premier exemple de DLL
II-1. Ce qu'il faut savoir
II-2. Code minimal
II-2-a. Compilation et inclusion de ressources
II-2-b. Déclaration et implémentation de fonctions/procédures
II-2-c. Exportation des fonctions
II-3. Utiliser la DLL
II-3-a. Méthode statique
II-3-b. Méthode dynamique
III. Aller plus loin
III-1. Quand la liaison statique est suffisante
III-1-a. La DLL
III-1-b. Le programme
III-2. Lorsque la liaison dynamique s'impose
III-2-a. La (les) DLL
III-2-b. Le programme
IV. Réflexes à prendre, astuces, ...
IV-1. Partager des types entre application et DLL
IV-2. Veiller aux types de variables utilisés
IV-3. Les conventions d'appel
IV-4. Déboguer une DLL
Conclusion
Remerciements
Références
Introduction
Une DLL, ou Dynamic Link Library, est un fichier contenant du code compilé à la
manière d'un exécutable. L'utilité est toutefois ici d'exporter les fonctions ou les procédures correspondant
à ce code, afin de les mettre à disposition de toute application sachant les exploiter. Le but est ainsi
de créer un code réutilisable par différents programmes, quels qu'ils soient.
Delphi nous offre donc la possibilité de créer des DLL et d'en utiliser, qu'elles aient été programmées avec Delphi
ou un autre langage. Nous allons donc étudier la procédure à suivre pour créer et utiliser ces DLL, ainsi que certaines
habitudes qu'il est bon de prendre pour assurer une éventuelle compatibilité avec des programmes qui ne seraient pas développés avec Delphi.
I. Création du projet
I-1. Création du fichier
La première chose à faire est de créer le projet qui permettra de compiler notre future
DLL. Assurez-vous d'avoir bien enregistré/fermé tout projet en cours, puis à partir du menu "Nouveau | Autre..."
créez un nouveau projet de type "Expert DLL", disponible dans l'onglet "Nouveau".
 |
Dans Delphi 2005, la procédure est la même, si ce n'est que l'item "Expert DLL" se trouve dans le dossier "Projets Delphi"
de l'arborescence des nouveaux éléments.
|
La validation de la boîte de dialogue vous ramènera à l'éditeur de Delphi qui contiendra la nouvelle unité créée, dont voici le contenu :
| Projet DLL par défaut |
library Project1;
uses
SysUtils,
Classes;
{$R *.res}
begin
end. |
Lorsqu'on le voit pour la première fois, ce fichier peut être déroutant. C'est pourquoi nous
allons nous attacher à en décrire les différences avec un projet d'application "classique".
I-2. "library" plutôt que "program"
Tout d'abord il faut bien voir que le fichier généré par Delphi n'est pas une unité ".pas",
mais bien un fichier projet ".dpr". Cela ne signifie en aucun cas que vous ne pourrez pas utiliser d'unités dans votre
projet, mais que celles-ci ne sont pas nécessaires au premier abord : à l'instar d'un programme en mode console, vous
pouvez entrer tout le code de votre DLL dans le fichier du projet.
A partir de cette remarque, une autre différence est aisément identifiable : l'habituel "program Project1;"
a été remplacé par "library Project1;". "library" est le mot réservé qui indiquera à Delphi qu'il doit générer non
pas un exécutable mais une DLL.
Vous pouvez vous-même le vérifier en créant une nouvelle application puis en remplaçant dans le fichier DPR le mot "program"
par "library". A la compilation, le fichier verra son extension changée de ".exe" à ".dll".
I-3. L'avertissement de Borland
L'autre différence avec un fichier DPR d'application est le long commentaire ajouté par Borland au
début du fichier.
Il donne quelques informations sur des précautions à prendre lors du développement de DLL dans Delphi.
En effet, le type string dans Delphi est un type particulier dont la gestion en mémoire diffère de la gestion des
chaînes de caractères par Windows. Ainsi Borland prévient que, si vous comptez faire transiter des variables de type string
entre votre DLL et les applications qui l'utilisent, il vous faudra :
- Ajouter 'ShareMem' en première position dans la clause uses de votre DLL et des applications qui l'utiliseront
- Déployer la DLL 'BORLNDMM.DLL' (Borland Memory Manager) avec votre propre DLL.
Cette bibliothèque de Borland remplace le gestionnaire de mémoire par défaut de votre DLL dans le but de permettre l'échange en mémoire de vos variables de type string.
Autant dire que ce n'est pas vraiment avantageux d'utiliser des string avec des DLL ! Cela limiterait d'ailleurs leur
emploi à des programmes incluant ShareMem, donc des programmes Delphi.
Ce problème avec les string n'est pas le seul à devoir être relevé, car les types de variables entre les langages ont souvent
des caractéristiques différentes. Tout cela sera discuté dans la dernière partie de cet article.
II. Premier exemple de DLL
II-1. Ce qu'il faut savoir
Avant de commencer à programmer une DLL, il serait judicieux, pour bien comprendre le code, de voir comment
un fichier DLL est construit puis exploité au niveau de Windows. Nous ne donnerons pas les spécifications du format dans le détail puisque
ce n'est pas le but de cet article, mais seulement quelques points qui semblent importants pour saisir le code et les explications
qui suivront.
 |
Sur le site www.wotsit.org vous trouverez en recherchant le mot-clef "DLL"
une série de fichiers décrivant le format Portable Executable (PE), format de Windows pour les EXE, DLL et autres.
|
Comme bon nombre de fichiers, les DLL commencent par un entête donnant diverses informations sur le fichier,
et la taille des données qui suivent. Celles-ci sont principalement, dans le cas du format PE, organisées en tables et en segments.
Nous ne nous intéresserons qu'aux tables des DLL, et principalement à deux d'entre elles, dont une tout particulièrement. La première est plutôt donnée à
titre informatif, il s'agit de la table des ressources. Cette table liste toutes les ressources contenues dans les segments de données, renseignant leur nombre, le nom,
la taille, le type de chacune etc.
 |
Vous pouvez créer des DLL qui ne contiennent que des ressources de tout type, sans quelque forme de code que ce soit. L'intérêt ?
Cela vous permet de réunir par exemple un jeu complet d'icônes dans un unique fichier que Windows sera à même d'exploiter pour
personnaliser vos différents dossiers et raccourcis. Voyez cette page
de Nono40 pour en savoir plus !
|
La seconde table intéressante est la table des exportations. Comme dit plus haut, l'objectif d'une DLL est de fournir à plusieurs
applications une série de fonctions ou de procédures. Ces dernières doivent donc être accessibles à qui sait les appeler, et pour cela elles sont exportées
par l'intermédiaire de la table d'exportation. Cette table recense toutes les fonctions ou procédures exportées en fournissant leur identifiant et l'adresse
où les trouver dans la DLL. Les identifiants utilisés sont soit un nom sous forme d'une chaîne de caractères, soit un nombre unique au sein de la DLL.
 |
La table d'exportations ne permet pas de retrouver les types des paramètres que demandent les fonctions ou procédures qui y sont référencées.
|
Voyons maintenant comment sont agencés ces différents éléments et, parallèlement, comment Windows les utilise :
En règle générale, chaque exécutable est lié à une ou plusieurs DLL, ne serait-ce que pour les appels à l'API Windows. Une partie du code nécessaire aux applications
est donc contenue dans différentes DLL, et un accès doit être effectué à chacune d'entre elles pour le bon fonctionnement du programme.
Lorsqu'un exécutable est lancé dans Windows, il est chargé en mémoire par un PE-Loader. C'est par son intermédiaire que les liaisons avec les DLL seront effectuées.
Pour cela, le PE-Loader va charger les DLL nécessaires et, grâce à leur table d'exportations, retrouver toutes les fonctions ou procédures dont l'application a besoin. Il copiera en mémoire
le code de ces routines de telle sorte que l'exécutable pourra y accéder au besoin, et lancera l'application.
Ce fonctionnement implique trois "phases" distinctes dans la création d'une DLL :
- La compilation et l'inclusion de ressources éventuelles.
- La déclaration puis l'implémentation des fonctions et procédures de la DLL.
- La demande d'exportation de ces fonctions pour qu'elles soient mises à disposition d'autres applications.
Nous allons immédiatement voir les solutions minimales pour réaliser ces trois étapes.
II-2. Code minimal
II-2-a. Compilation et inclusion de ressources
Cette première étape n'est pas indispensable dans la réalisation d'une DLL. Qui plus est, la création et
l'inclusion de ressources dans un projet pourrait faire l'objet d'un tutoriel complet. C'est pourquoi nous
ne développerons pas cette partie. Vous pouvez vous référer à
cet article de
DelphiCool si vous ne savez pas
manipuler les ressources.
Dans le code de la DLL, si vous souhaitez inclure un fichier de ressources que vous avez compilé, ajoutez simplement la
directive de compilation '{$R fichier_ressources.res}' pour que Delphi le prenne en compte lors de la compilation.
Votre code ressemble donc à ceci :
| Code minimal |
library DLL1;
uses
SysUtils,
Classes;
{$R *.res}
{$R fichier_ressources.res}
begin
end. |
La compilation de ce code, si "fichier_ressources.res" existe dans le répertoire de votre projet, vous donnera d'ores et déjà une DLL valide mais... vide de code !
Nous allons donc maintenant voir comment lui donner une quelconque utilité.
II-2-b. Déclaration et implémentation de fonctions/procédures
Notre première DLL aura pour fonction de calculer la somme de deux nombres qui lui seront passés en paramètres, en appelant
la fonction Somme. Rien de bien compliqué au niveau de l'implémentation, voici donc le nouveau code :
| Première fonction |
library DLL1;
uses
SysUtils,
Classes;
{$R *.res}
function Somme(A, B: Integer): Integer; stdcall;
begin
Result := A + B;
end;
begin
end. |
Il n'y a pas besoin de plus ! La fonction est implémentée directement avant le "begin ... end.", sans déclaration
préliminaire.
Seule la convention d'appel "stdcall" est inhabituelle. Elle permet de spécifier la manière dont les paramètres sont envoyés
à la fonction. Cette convention n'est pas la seule, mais est la plus courante. Ce point sera discuté en dernière partie.
Maintenant que la fonction est implémentée, la DLL peut être compilée sans souci, mais la fonction ne sera toujours pas accessible
par un programme extérieur.
C'est le but de la dernière étape.
II-2-c. Exportation des fonctions
Par rapport à la complexité de l'organisation d'un fichier DLL pour ce qui est de l'exportation de fonctions, Borland a su faire
quelque chose de simple côté code. Les fonctions à exporter doivent être signalées dans une clause "exports", en précisant
éventuellement un nom identifiant la fonction grâce à "name" et/ou un indice pour cette fonction grâce à "index".
Le nom et l'indice sont facultatifs, et des valeurs par défaut pour chacun de ces deux paramètres sont attribuées s'ils ne sont pas
renseignés.
Dans ce premier exemple, nous ne préciserons rien. Notre fonction sera donc exportée sous le nom 'Somme', avec un indice égal à 1.
Voici le code complet de notre DLL :
| Première DLL |
library DLL1;
uses
SysUtils,
Classes;
{$R *.res}
function Somme(A, B: Integer): Integer; stdcall;
begin
Result := A + B;
end;
exports
Somme;
begin
end. |
 |
Attention, la clause exports ne peut pas être utilisée autrement que pour des procédures ou des fonctions !
Il est impossible d'y mentionner des variables, classes ou autres objets.
|
Dans la suite de cet article, le fichier compilé de cette DLL sera "DLL1.dll".
II-3. Utiliser la DLL
Notre DLL est maintenant prête à être utilisée, il suffit de faire une petite application apte à en appeler la fonction Somme.
Créez un nouveau projet dans Delphi et créez une interface qui ressemble à celle-ci :

Interface du programme
Une fois cette interface créée, il faut lier notre application à la DLL. Il y a pour cela deux méthodes :
la méthode statique, la plus simple, et la méthode dynamique, plus complexe mais qui a également son utilité.
Nous utiliserons la même interface pour les deux méthodes, seul le code changera.
II-3-a. Méthode statique
La méthode de liaison statique est la plus simple à mettre en place dans Delphi. Elle ne demande pas de programmation à proprement
parler, mais d'une simple déclaration pour chaque fonction ou procédure à importer.
Le principe est le suivant : vous fournissez à Delphi les informations dont il a besoin pour lier votre programme à la DLL, et il se
charge lui-même des opérations de liaison avec celle-ci. Ces opérations se passent au démarrage de votre application, et apportent donc un
inconvénient : si la DLL n'est pas présente lors du lancement de l'application ou que la fonction demandée n'est pas trouvable dans
la DLL donnée, le programme plante et ne pourra être exécuté.
Il n'y a alors aucun moyen d'outrepasser le message d'erreur. Utilisez donc cette méthode en connaissance de cause et veillez à ce
que chaque DLL que vous utilisez soit bien déployée avec votre application.
Les informations à fournir à Delphi pour importer votre fonction de la DLL sont les suivantes :
- Le nom ou l'index de la fonction/procédure dans la DLL
- La liste des paramètres, dans l'ordre, et le type de chacun
- Le type de retour s'il s'agit d'une fonction
- Le nom de la DLL où trouver la fonction/procédure
Il apparaît évident qu'il faut parfaitement connaître la fonction que l'on veut utiliser pour pouvoir la lier au programme. Cette
liaison se fait, comme annoncé plus haut, au moyen d'une déclaration dans l'unité où vous souhaitez utiliser la fonction, dans la
section "interface". Voici pour continuer notre exemple la déclaration de la fonction Somme à importer de la DLL "DLL1.dll" :
| Déclaration de la fonction à importer |
....
const
NomDLL = 'DLL1.dll';
function Somme(A, B: Integer): Integer; stdcall; external NomDLL;
implementation
.... |
Comme vous pouvez le constater cette déclaration ne diffère pas énormément de celles dont vous devez avoir l'habitude.
Nous retrouvons la convention d'appel "stdcall". Il est important qu'elle apparaisse dans la déclaration de la fonction,
et surtout que la convention d'appel utilisée soit identique entre la DLL et le programme, sans quoi les erreurs seront au rendez-vous.
Le mot réservé qui suit, "external", précède le nom de la DLL qui contient la fonction à importer. L'utilisation de la constante
NomDLL pour stocker le nom de la DLL permet une mise à jour facile sir le fichier change de nom
Après external nous aurions pu ajouter "name" ou "index", pour spécifier un nom ou un index particulier identifiant
la fonction dans la DLL. Voici ce que cela aurait donné :
function Somme(A, B: Integer): Integer; stdcall; external NomDLL name 'Somme';
function Somme(A, B: Integer): Integer; stdcall; external NomDLL index 0; |
Pour ce qui est de notre exemple, nous n'avons pas besoin de préciser quoi que ce soit. En effet, en l'absence de "name" ou
de "index", Delphi utilise le nom de la fonction déclarée dans notre code comme nom d'importation. Cela correspond ici à
"Somme", qui est bien le nom d'exportation de notre fonction dans la DLL. Si vous vouliez utiliser "Addition" pour nom de fonction
dans le code de votre application, vous seriez alors obligé de spécifier "name 'Somme'" ou "index 0" dans votre déclaration.
Maintenant que la fonction est déclarée, Delphi saura où aller la chercher et il ne reste plus qu'à l'utiliser comme toute autre fonction
dans notre code. La seule chose que nous souhaitons est que le programme affiche la somme des termes A et B lorsqu'on clique sur le bouton
"A + B = ?". Il s'agit donc d'appeler notre fonction dans l'événement OnClick de ce bouton :
| OnClick du bouton "A + B = ?" |
procedure TForm1.btnSommeClick(Sender: TObject);
begin
ShowMessageFmt('%d + %d = %d', [A.Value, B.Value, Somme(.Value, B.Value)]);
end; |
Ce qui nous donne le résultat escompté :
II-3-b. Méthode dynamique
La méthode dynamique est plus compliquée à mettre en place, du fait qu'il faille écrire quelques lignes de code pour obtenir un lien
valide vers la fonction désirée. Toutefois, cette méthode offre beaucoup plus de souplesse par rapport à la gestion des erreurs, de
la mémoire etc. En effet cette fois, nous allons gérer nous-mêmes l'intégralité du processus de liaison, au moyen principalement de
deux API Windows : LoadLibrary et GetProcAddress.
Pour pouvoir utiliser notre fonction, il faut procéder en plusieurs étapes :
Dans un premier temps, demander au système de charger notre DLL en mémoire par l'intermédiaire de l'API LoadLibrary. Ensuite,
si celle-ci est bien chargée, on y récupère l'adresse de notre fonction, grâce à l'API GetProcAddress. Cette adresse nous la
stockons dans une variable de type function ou procedure, dont les paramètres corrects ont été renseignés. La fonction
s'utilise ensuite aussi facilement qu'une autre.
Voici le code illustrant ces propos, toujours avec notre même exemple de la fonction Somme :
| Liaison dynamique |
...
implementation
...
function LierFonction(DLL: String; var HandleDLL: THandle; NomFct: String; IndexFct: Integer = -1): Pointer;
begin
Result := nil;
HandleDLL := 0;
HandleDLL := LoadLibrary(pAnsiChar(DLL));
If HandleDLL = 0 then
Exit;
If IndexFct < 0 then
Result := GetProcAddress(HandleDLL, pAnsiChar(NomFct))
else
Result := GetProcAddress(HandleDLL, pAnsiChar(IndexFct));
end;
procedure TForm1.btnSommeClick(Sender: TObject);
var HandleDLL: THandle;
Somme : function(A, B: LongInt): LongInt; stdcall;
begin
Somme := LierFonction('DLL1.dll', HandleDLL, 'Somme');
If assigned(Somme) then
try
ShowMessageFmt('%d + %d = %d', [A.Value, B.Value, Somme(.Value, B.Value)]);
finally
FreeLibrary(HandleDLL);
end
else
ShowMessage('Erreur de chargement de la fonction "Somme"');
end; |
Afin de compléter les commentaires du code, voici quelques explications :
La fonction LierFonction est là pour charger la DLL en mémoire, puis récupérer et renvoyer un pointeur sur la fonction qui nous intéresse.
L'idée est de pouvoir réutiliser cette fonction pour n'importe quel chargement de fonction/procédure dans une DLL.
Voici la description des paramètres :
- DLL : Le nom du fichier DLL à charger.
- HandleDLL : Il s'agit d'une variable que l'appelant doit pouvoir réutiliser. A la sortie de la fonction, elle contiendra soit la valeur 0 si le chargement a échoué, soit le handle de la DLL dans le cas contraire. Ce Handle doit être utilisé pour libérer la DLL.
- NomFct : Nom de la fonction à charger. Il s'agit du nom d'exportation de la fonction qui nous intéresse au sein de la DLL.
- IndexFct : Index de la fonction à charger. Par défaut à -1, l'index ne sera utilisé à la place du nom que si sa valeur est donnée.
La procédure qui suit LierFonction n'est autre que l'événement OnClick de notre bouton "A + B = ?". Deux variables y sont
déclarées. La première, de type THandle, est là pour satisfaire au paramètre var "HandleDLL" de LierFonction.
La seconde est plus inhabituelle : il s'agit d'une variable de type function, dont les paramètres et la convention d'appel sont déclarés à l'identique
de notre fonction Somme dans DLL1.dll. Cette déclaration donne à Delphi les informations dont il aura besoin lorsque nous
appellerons la fonction, mais du côté de la mémoire, notre variable n'est qu'un simple pointeur sur une adresse qui contient le code
de la fonction à exécuter. Ainsi, l'adresse de la fonction récupérée par GetProcAddress peut être assignée à notre variable, ce
qui nous permet d'utiliser la fonction exportée de la DLL comme nous l'aurions fait avec toute autre fonction.
Lorsque le bouton "A + B = ?" est pressé, l'adresse de Somme est donc retrouvée avec LierFonction, puis si la fonction a bien été trouvée,
elle est appelée dans un ShowMessage, et enfin, chose à ne pas oublier, la DLL est libérée de la mémoire, ce qui justifie la sauvegarde dans une
variable de son Handle.
Comme en témoigne la copie d'écran, le résultat est identique à la méthode statique du côté utilisateur, mais du côté de la programmation la méthode dynamique nous offre
les moyens de contrôler les erreurs de chargement de la DLL et de la fonction elle-même, à un niveau où il est encore possible de prévoir
une réaction "pensée" du programme.

Cette première DLL et l'application l'utilisant sont maintenant terminées. Vous avez désormais en main les éléments pour créer vos propres
bibliothèques de fonctions et les exploiter à bon escient.
Dans la prochaine partie nous verrons comment aller plus loin avec les DLL et leur trouverons une application qui s'approche plus de
quelque chose d'utile qu'une simple somme !
III. Aller plus loin
Le but de cette partie est d'implémenter une mise en oeuvre plus concrète d'une ou plusieurs DLL et de leur utilisation dans une
application que nous développerons. Nous verrons deux exemples, chacun nécessitant l'une ou l'autre des méthodes statique ou dynamique.
Ils permettront de voir plus précisément comment choisir entre ces deux approches de liaison avec une DLL.
III-1. Quand la liaison statique est suffisante
Imaginons un programme simple, un éditeur de texte. Nous souhaitons que cet éditeur propose deux méthodes d'enregistrement
distinctes, dont les implémentations seront stockées dans une DLL. De même, cette DLL contiendra l'implémentation des deux
méthodes de chargement correspondantes.
Les éléments que nous avons sont les suivants : un nombre de fonctions fini et connu, dont les paramètres et les types de
renvoi sont également connus, le tout implémenté dans une DLL toute aussi connue.
Le choix de la liaison statique est alors ici évident. Tout est déjà préparé, il n'y a qu'à charger les fonctions dès le
lancement de l'application pour que le tout fonctionne. Bien évidemment, la méthode dynamique pourrait être utilisée, mais
comme vous vous en rendez sûrement compte, ce serait "beaucoup" de code pour bien peu, dans notre cas.
C'est ainsi que la différence doit être faite. Dans tous les cas vous pourrez utiliser la méthode dynamique, mais parfois il
sera superflu de faire plus qu'une déclaration statique, à moins que vous ayez un besoin impératif de gérer ou d'outrepasser
des erreurs éventuelles. Dans notre cas, si la DLL n'est pas présente le programme n'a plus aucun intérêt, ce n'est donc pas
un mal en soit qu'il ne puisse pas s'exécuter !
Passons maintenant à l'implémentation de la DLL, puis du programme en lui-même.
III-1-a. La DLL
Notre DLL doit contenir deux fonctions exportées : une de sauvegarde, et une de chargement.
Pour l'exemple nous implémenterons une fonction de cryptage par XOR et une autre par décalage de caractères, à l'instar
du code de César. Rien de bien compliqué, juste de quoi faire deux méthodes différentes !
Nous passerons en paramètres aux fonctions de chargement/enregistrement la méthode de cryptage à utiliser et la valeur entière qui lui servira.
Tout d'abord créons nos deux méthodes de cryptage. Elles se résument chacune en une ligne, qui crypte le caractère passé
en paramètre soit en lui appliquant un XOR avec une valeur donnée, soit en incrémentant la valeur ASCII de ce caractère d'un
pas également renseigné.
Voici la fonction de cryptage XOR :
| Cryptage XOR |
function xorChar(Caractere: Char; Valeur: Integer): Char;
begin
Result := Chr(Ord(Caractere) xor Valeur)
end; |
Et voici celle de cryptage par "décalage" :
| Cryptage par décalage |
function cesChar(Caractere: Char; Decalage: Integer): Char;
begin
Result := Chr(Ord(Caractere) + Decalage);
end; |
Ces deux fonctions implémentées, nous allons les exploiter au niveau du chargement et de la sauvegarde.
Commençons par cette dernière : il faut lui passer un nom de fichier à enregistrer, le texte qu'il doit contenir et les
informations concernant le cryptage à utiliser.
Pour l'enregistrement proprement dit, nous utiliserons un Stream de fichier dans lequel nous écrirons le texte fourni en le
cryptant, caractère par caractère.
Voici le code commenté de la fonction d'enregistrement :
| Fonction d'enregistrement |
function Sauver(Fichier, Contenu: pAnsiChar; Methode: TCryptMethod; Valeur: Integer): Boolean; stdcall;
var
Stream: TFileStream;
p : pChar;
Data : Char;
begin
Result := True;
p := Contenu;
Stream := TFileStream.Create(Fichier, fmCreate);
try
try
while p^ <> #0 do
begin
case Methode of
cmXOR : Data := xorChar(p^, Valeur);
cmCesar: Data := cesChar(p^, Valeur);
end;
Stream.Write(Data, 1);
inc(p);
end;
Data := #0;
Stream.Write(Data, 1);
except
on Exception do
Result := False;
end;
finally
Stream.Free;
end;
end; |
Afin de pouvoir renvoyer False en cas de problème lors de l'écriture du fichier, le code a été inclus dans un bloc try..except.
Comme vous le voyez, on utilise un pointeur sur caractère pour avancer dans le texte à enregistrer, du fait de l'utilisation du type pChar.
Cette méthode d'écriture caractère par caractère n'est sans doute pas des plus optimisées, mais elle a l'avantage d'être simple,
et de suffire pour cet exemple !
Le type TCryptMethod est défini ainsi, au début de l'unité :
type
TCryptMethod = (cmXOR = 0, cmCesar = 1); |
Pour la méthode de chargement, un problème apparaît : nous ne savons pas à l'avance la taille du buffer à allouer pour contenir le texte que
nous souhaitons charger. Il n'est pas question de laisser la DLL allouer un buffer de la bonne taille, car c'est à l'application que
doit appartenir la gestion de sa propre mémoire. Nous allons donc employer une "astuce" très en vogue au niveau des API Windows.
Tout comme la fonction d'enregistrement, nous allons demander un nom de fichier, un buffer pour contenir le texte et les informations
sur la méthode de cryptage à employer.
L'astuce, vraiment simple mais efficace, se trouve au niveau de ce buffer : soit l'appelant a effectivement fourni un pointeur vers
un buffer alloué en mémoire, auquel cas on y effectue le chargement du texte, soit l'appelant a passé nil en paramètre, et la fonction
renvoie alors la taille nécessaire du buffer à allouer, en se basant sur la taille du fichier qu'on lui demande de charger.
Ainsi le programme appellera un première fois la fonction avec un buffer à nil pour obtenir la taille du fichier, allouera son buffer
et rappellera la fonction en lui passant ce dernier pour terminer le chargement.
Pour ce qui est du chargement à proprement parler, nous écrirons le fichier crypté en son ensemble dans le buffer, puis le décrypterons
caractère par caractère avant de rendre la main au programme appelant.
Ce qui nous donne le code suivant :
| Fonction de chargement |
function Charger(Fichier, Destination: pAnsiChar; Methode: TCryptMethod; Valeur: Integer): Integer; stdcall;
var
Stream: TFileStream;
p : pChar;
begin
Result := 0;
Stream := TFileStream.Create(Fichier, fmOpenRead);
If Destination = nil then
begin
Result := Stream.Size;
Stream.Free;
Exit;
end
else
try
Stream.Seek(0, soFromBeginning);
Stream.Read(Destination^, Stream.Size);
p := Destination;
while p^ <> #0 do
begin
case Methode of
cmXOR : p^ := xorChar(p^, Valeur);
cmCesar: p^ := cesChar(p^, -Valeur);
end;
inc(p);
end;
finally
Stream.Free;
end;
end; |
Nos deux fonctions sont maintenant prêtes (vous remarquerez pour chacune d'elles que la convention d'appel stdcall a été précisée),
il reste à les déclarer comme exportées pour y avoir accès à partir de notre programme.
Pour cela, l'ajout de ces trois lignes de code en fin d'unité suffit :
| Exportation des fonctions |
exports
Sauver index 0 name 'Sauver' ,
Charger index 1 name 'Charger'; |
Compilez votre DLL, et copiez-la dans un nouveau répertoire qui contiendra les sources de notre programme.
Pour la suite de notre exemple, la DLL aura pour nom "OpFichiers.dll".
III-1-b. Le programme
Nous passons donc à l'implémentation du programme qui exploitera cette DLL. Commençons par l'interface.
Elle doit ressembler à la capture d'écran suivante, où les noms des composants utilisés dans le code figurent en rouge :
Au niveau du code, nous allons commencer par les déclarations.
Tout d'abord, le type TCryptMethod, puis les deux fonctions Sauver et Charger, le tout dans la section
interface de l'unité. Nous y déclarons également une constante CRYPT_VAL que nous passerons en paramètre aux
fonctions pour le XOR et le décalage de caractère.
| Déclarations |
interface
...
type
TCryptMethod = (cmXOR = 0, cmCesar = 1);
function Sauver (Fichier, Contenu : pAnsiChar; Methode: TCryptMethod;
Valeur: Integer): Boolean; stdcall; external 'OpFichiers.dll';
function Charger(Fichier, Destination: pAnsiChar; Methode: TCryptMethod;
Valeur: Integer): Integer; stdcall; external 'OpFichiers.dll';
const
CRYPT_VAL = 15;
... |
Comme prévu nous déclarons donc statiquement les deux fonctions, en précisant comme nous l'avons vu en deuxième partie le nom
de la DLL qui contient le code de chacune, et la convention d'appel à utiliser.
Nos fonctions peuvent maintenant être utilisées comme n'importe quelle autre fonction au sein de notre code, et nous pouvons
donc passer à l'implémentation du programme.
Son fonctionnement est on ne peut plus simple. Le choix de la méthode de cryptage/décryptage se fait grâce au composant cbxMethode,
dont la sélection est prise en compte dans les OnClick des boutons de chargement et d'enregistrement.
Voyons tout d'abord pour ce dernier :
| OnClick du bouton 'Sauver' |
procedure TForm1.Button2Click(Sender: TObject);
var
Fichier: String;
begin
If not PromptForFileName(Fichier, 'Fichier texte (*.txt)|*.txt', '', '', '', True) then
Exit;
If not Sauver(pAnsiChar(Fichier), memTexte.Lines.GetText, TCryptMethod(cbxMethode.ItemIndex), CRYPT_VAL) then
ShowMessage('Erreur de sauvegarde !');
end; |
Vous remarquerez que l'on peut difficilement faire plus simple !
Après avoir demandé un nom de fichier pour le texte à enregistrer, on appelle Sauver avec les paramètres nécessaires
pour la sauvegarde et le cryptage du texte contenu dans notre mémo. Il faut bien sûr que l'ordre des items de cbxMethode
concorde avec la définition du type TCryptMethod pour que tout se passe correctement.
Le code de chargement n'est guère plus compliqué, si ce n'est que l'astuce énoncée plus haut implique quelques lignes de code
supplémentaires :
| OnClick du bouton 'Charger' |
procedure TForm1.Button1Click(Sender: TObject);
var
Fichier: String;
Contenu: pAnsiChar;
Taille : Integer;
begin
If not PromptForFileName(Fichier, 'Fichier texte (*.txt)|*.txt') then
Exit;
Taille := Charger(pAnsiChar(Fichier), nil, TCryptMethod(cbxMethode.ItemIndex), CRYPT_VAL);
GetMem(Contenu, Taille);
Charger(pAnsiChar(Fichier), Contenu, TCryptMethod(cbxMethode.ItemIndex), CRYPT_VAL);
memTexte.Lines.SetText(Contenu);
FreeMem(Contenu);
end; |
Les commentaires disent tout : dans un premier temps on demande à la DLL de nous fournir la taille du buffer à allouer puis,
une fois la mémoire réservée, on rappelle la fonction Charger pour récupérer le contenu du fichier et l'afficher dans notre
mémo.
Comme vous pouvez le voir c'est une méthode assez efficace, et le "coût" d'une ligne de code supplémentaire nous apporte la sécurité
de gérer en interne l'allocation et la libération de la mémoire utilisée pour le chargement.
Ce programme est maintenant terminé. Pour résumer la situation, la liaison statique nous était donc permise ici par notre connaissance
du nombre et de la définition exacts des fonctions qui seraient employées. Celles-ci, implémentées dans la DLL, ont pu être exploitées
directement sans autre ajout qu'une déclaration adéquate pour chacune d'elle dans l'unité du programme.
Cette méthode permet d'ajouter à notre guise de nouvelles fonctions d'enregistrement et de chargement à l'application, mais n'offre
pas cependant toute la flexibilité que l'on peut attendre des DLL.
Comme nous allons le voir à présent, une plus grande souplesse peut être obtenue grâce à la liaison dynamique.
III-2. Lorsque la liaison dynamique s'impose
Nous allons maintenant nous attaquer au développement d'un programme qui très sincèrement ne servira à rien de concret, mais qui va nous
permettre de nous intéresser à un cas d'école pour ce qui est des DLL : les plugins.
Il existe de nombreuses manières de réaliser un système de gestion de plugins ; ce que nous verrons ici n'est donc pas la méthode, mais
une méthode parmi tant d'autres. Voyez les références pour d'autres points de vue sur le sujet.
De notre côté, nous irons assez simplement. Imaginons un programme possédant un composant TMainMenu. Nous souhaitons pouvoir ajouter, grâce à
des plugins, un nombre indéfini d'items à ce menu.
Le programme devra donc chercher, dans un répertoire réservé par exemple, tous les plugins qu'il est censé intégrer à son menu.
Devant notre méconnaissance du nombre de fichiers qui seront trouvés, et même de la présence ou non de plugins, nous n'avons pas le choix
d'opter pour une liaison dynamique. Celle-ci permettra de s'adapter à chaque situation, et d'éviter toute erreur de chargement qui pourrait bloquer
le fonctionnement du reste du programme.
III-2-a. La (les) DLL
Vous l'aurez compris, les plugins seront des DLL. Et il va de soi qu'il faut instaurer un minimum de "norme" dans ces DLL pour que le
programme puisse les reconnaître et les exploiter.
Nous allons donc commencer par implémenter une fonction d'identification du plugin. Si le programme ne trouve pas cette fonction dans une
DLL, il la considérera comme n'étant pas un plugin valide.
Créez une nouvelle DLL et ajoutez-y la fonction suivante :
| Fonction d'identification du plugin |
function GetName: ShortString; stdcall;
begin
Result := 'Exemple - DLL n°1';
end; |
Cette fonction a un double usage. Par sa présence, elle indiquera donc au programme qu'il a bien à faire à un plugin qui lui est destiné, et
par sa valeur de retour elle donnera à l'application le Caption de l'item à créer dans le menu.
Cet item doit ensuite recevoir des sous-items, qui donneront accès aux fonctionnalités que propose le plugin. La liste de ces sous-items
doit être donnée par la DLL, de même que les fonctions qu'ils vont appeler lors d'un OnClick doivent être indiquées par la DLL et
stockées dans celle-ci. Nous donnerons en fait au programme le nom d'exportation de chacune des fonctions, ce qui lui permettra de faire les
chargements nécessaires lors du déclenchement de l'événement OnClick.
Nous allons donc créer une fonction qui se chargera de fournir ces informations au programme. Par souci de commodité la fonction renverra
un tableau de records listant les couples Item/Fonction, ce qui est la meilleure manière de tout renvoyer d'un seul coup.
Nous allons donc définir deux types : le type TItem, un record de deux éléments et le type TItemsList, un tableau dynamique de
TItem.
Afin de pouvoir aisément réutiliser ces types dans notre programme, nous allons les déclarer dans une unité à part. Créez une nouvelle unité
nommée par exemple "PlugUtils.pas" et définissez-y nos deux types, dans la partie interface :
| Types TItem et TItemsList |
Type
TItem = record
Nom : ShortString;
Fonction: ShortString;
end;
TItemsList = Array of TItem; |
Il nous faut quelque part stocker la liste des items du menu à créer. Revenez à l'unité de la DLL, et déclarez un tableau constant
qui contiendra les Caption des items de notre plugin. Pour l'exemple, nous en créerons trois :
| Captions des sous-items |
const
ItemsCaptions: Array[0..2]Of ShortString = ('Item1', 'Item2', 'Item3'); |
Cette liste établie, il reste à pouvoir la communiquer au programme hôte de notre plugin. Nous allons donc créer une fonction
GetItemsList qui renverra une tableau de type TItemsList contenant chacun des éléments de notre tableau ItemsCaptions
associé au nom de la fonction qui lui correspond. Afin de ne pas procéder au cas par cas, nous nommerons toutes les fonctions de la
même manière, en préfixant le nom de l'item de la chaîne "Exec".
Voici le code de la fonction GetItemsList :
| Fonction de renseignement des sous-items à créer |
function GetItemsList: TItemsList; stdcall;
var
i: Integer;
begin
SetLength(Result, Length(ItemsCaptions));
For i := Low(ItemsCaptions) to High(ItemsCaptions) do
begin
Result[i].Nom := ItemsCaptions[i];
Result[i].Fonction := 'Exec' + ItemsCaptions[i];
end;
end; |
Le fonctionnement de cette fonction est tel qu'il a été décrit :
pour chaque item de ItemsCaptions, on renseigne un nouvel élément du tableau de retour en lui donnant le Caption
de l'item et la fonction qui lui est associée dans la DLL.
La dernière étape est maintenant de créer ces fonctions, afin qu'un click sur l'un des items puisse être effectif.
Nous ne serons pas trop originaux ici : chaque fonction ne fera qu'afficher un message texte avec un bouton "OK". Pour ne pas
alourdir notre DLL, nous utiliserons la fonction MessageBox de l'unité "Windows.pas" plutôt que ShowMessage.
Nous avons donc trois fonctions à créer, dont voici les implémentations :
| Fonctions des trois sous-items |
procedure ExecItem1(Sender: TObject); stdcall;
begin
Windows.MessageBox(0, 'Vous avez cliqué sur le premier item !!', 'ExecItem1', MB_OK);
end;
procedure ExecItem2(Sender: TObject); stdcall;
begin
Windows.MessageBox(0, 'Vous avez cliqué sur le second item !!', 'ExecItem2', MB_OK);
end;
procedure ExecItem3(Sender: TObject); stdcall;
begin
Windows.MessageBox(0, 'Vous avez cliqué sur le troisième item !!', 'ExecItem3', MB_OK);
end; |
Le paramètre de ces procédures n'est présent que pour des raisons de compatibilité avec le type TNotifyEvent, qui
est le type d'événement de base, et plus particulièrement le type de l'événement OnClick auquel nous associerons
nos procédures.
Pour finir, il reste une chose essentielle pour que la boucle soit bouclée : l'exportation de toutes ces fonctions.
Vous avez déjà vu comment cela se passait, le code suivant n'a donc pas besoin d'explication supplémentaire :
| Exportation des fonctions du plugin |
exports
GetName index 0 name 'GetName',
GetItemsList index 1 name 'GetItemsList',
ExecItem1 index 2 name 'ExecItem1',
ExecItem2 index 3 name 'ExecItem2',
ExecItem3 index 4 name 'ExecItem3'; |
A noter que l'exportation avec des noms corrects est ici très importante, puisque c'est grâce à eux que le programme identifiera
et utilisera correctement le plugin.
Justement, voyons maintenant comment cela se passe du côté de l'application. Compilez la DLL et copiez la dans un sous-dossier "plugins"
du répertoire qui recevra les sources de notre nouveau programme.
III-2-b. Le programme
Commençons par un aperçu de l'interface :
Le menu "Fichier" ne contient qu'un item "Quitter".
La ListBox est là pour afficher des informations sur l'avancement du chargement des plugins.
Ce chargement sera fait dès le lancement de l'application, ainsi c'est dans l'événement OnCreate de notre fiche que nous
allons procéder à l'inspection du dossier "plugins" et à l'ajout des différents items à notre menu.
Lors du chargement, le handle de chacune des DLL liées sera ajouté dans un tableau dynamique afin d'être accessible par la suite.
Ce tableau est déclaré de manière globale dans l'unité :
| Déclaration du tableau de Handles |
var
Form1: TForm1;
Plugins: Array Of THandle; |
Chaque item conservera les informations qui lui sont propres. Afin de ne pas surcharger le code de tableaux en tous genres, nous stockerons
le nom de la fonction lui servant d'événement OnClick dans sa propriété Hint, et l'index du handle de la DLL où trouver cette
fonction dans sa propriété Tag.
A chacun des items, à leur création, nous assignerons un même événement OnClick qui se chargera d'utiliser ces deux informations pour
lier et appeler la fonction adéquate.
L'événement OnCreate ne contient que très peu de code en lui-même. En effet à cause du fonctionnement de FindFirst et FindNext,
le code de liaison des DLL et d'ajout des items est décentralisé dans une procédure AjoutMenu pour éviter que de longues portions de code
ne soient répétées inutilement. Cette procédure est imbriquée dans la procédure OnCreate, afin de pouvoir réutiliser les variables de celle-ci.
Voici dans un premier temps l'événement OnCreate sans la procédure AjoutMenu :
| Evénement OnCreate |
procedure TForm1.FormCreate(Sender: TObject);
var
SR: TSearchRec;
begin
If FindFirst(ExtractFilePath(ParamStr(0)) + '\plugins\*.dll', faAnyFile, SR) = 0 then
begin
AjoutMenu(0);
while FindNext(SR) = 0 do
begin
AjoutMenu(High(Plugins) + 1);
end;
FindClose(SR);
end;
end; |
Le fonctionnement avec une boucle while sur FindNext conditionnée par le résultat de FindFirst ne doit pas être nouveau pour vous ; c'est
la méthode fournie dans l'aide de Delphi sur ces deux fonctions de recherche.
Ainsi si un premier fichier est trouvé, nous commençons par l'ajouter au menu. Le paramètre de AjoutMenu, comme nous le verrons, spécifie l'index où
stocker le handle de cette nouvelle DLL dans le tableau Plugins. La valeur 0 correspond donc bien à la première DLL trouvée.
Ensuite à chaque fois qu'une nouvelle DLL est trouvée, nous l'ajoutons au menu en spécifiant comme index la taille actuelle de Plugins incrémentée de 1.
La procédure AjoutMenu se charge avec cette information et les autres variables de OnCreate de lier la DLL au programme et d'en utiliser les fonctions
pour ajouter les items et sous-items au menu.
En voici le code, qu'il faut insérer après la déclaration des variables de OnCreate :
| Procédure AjoutMenu |
procedure AjoutMenu(Index: Integer);
var
i: Integer;
Item, SousItem: TMenuItem;
ItemsList: TItemsList;
PluginName : function: ShortString; stdcall;
GetItemsList: function: TItemsList; stdcall;
begin
SetLength(Plugins, Index + 1);
Plugins[Index] := 0;
PluginName := LierFonction(ExtractFilePath(ParamStr(0)) + '\plugins\' + SR.Name, Plugins[Index], 'GetName');
If not Assigned(PluginName) then
begin
If Plugins[Index] <> 0 then
FreeLibrary(Plugins[Index]);
SetLength(Plugins, Index);
lstDLL.Items.Add('La DLL "' + SR.Name + '" n''est pas un plugin valide.')
end
else
begin
Item := TMenuItem.Create(Menu);
Item.Caption := PluginName;
Menu.Items.Add(Item);
lstDLL.Items.Add('Nouveau plugin "' + PluginName + '" dans la DLL "' + SR.Name +'"');
GetItemsList := LierFonction('', Plugins[Index], 'GetItemsList');
If not Assigned(GetItemsList) then
begin
lstDLL.Items.Add('Fonction "GetItemsList" manquante dans la DLL "' + SR.Name + '"');
Exit;
end;
SetLength(ItemsList, 0);
ItemsList := GetItemsList;
for i := Low(ItemsList) to High(ItemsList) do
begin
SousItem := TMenuItem.Create(Menu);
SousItem.Caption := ItemsList[i].Nom;
SousItem.Hint := ItemsList[i].Fonction;
SousItem.Tag := Index;
SousItem.OnClick := ExecItem;
Item.Add(SousItem);
lstDLL.Items.Add('---> Ajout de l''item "' + SousItem.Caption + '"');
end;
end;
end; |
Tout d'abord afin de pouvoir utiliser le type
TItemsList, ajoutez dans la clause
uses l'unité "PlugUtils.pas" que nous avons
créée lors de l'implémentation de la DLL.
AjoutMenu est peut-être la fonction la plus complexe de cet article, mais elle surtout est plus "massive" que compliquée.
Tout d'abord
Plugins voit sa taille incrémentée d'un élément, pour recevoir le handle de la DLL, et l'élément nouvellement
créé est initialisé à 0 pour les besoins de la fonction
LierFonction. Pour rappel nous avons vu cette fonction dans le troisième
paragraphe de la seconde partie de cet article. Vous pouvez éventuellement
le consulter pour bien comprendre le déroulement du
reste du code. Il faut bien sûr l'ajouter au code de notre programme.
Nous appelons donc
LierFonction pour en récupérer le résultat dans la variable
PluginName. Dans le meilleur des cas,
elle pointera alors sur la fonction
GetName exportée par la DLL que nous souhaitons ajouter. Sinon, elle contiendra
nil, ce qui nous
indiquera que la DLL que nous tentons d'ajouter n'est pas un plugin valide. C'est ce distinguo que nous faisons juste ensuite pour éviter
toute violation d'accès.
Si
PluginName contient
nil, la liaison avec la DLL est libérée et la taille de
Plugins est décrémentée afin de supprimer
l'item qui était réservé pour cette dernière.
Dans le cas contraire, on continue en créant un nouvel item dans le menu dont le
Caption est le résultat de l'appel à
PluginName. Ensuite
on procède avec
GetItemsList comme pour la liaison avec
GetName, et chaque sous-item est ajouté au menu grâce à la liste récupérée dans
ItemsList.
C'est à cet instant que l'on stocke les informations concernant l'action de chacun de ces items, et que l'on associe à leur événement
OnClick la
procédure
ExecItem. Elle constitue l'événement
OnClick "central", qui se chargera d'appeler en fonction du
Sender la fonction
qu'il faut dans la bonne DLL.
Elle utilisera pour cela notre fonction
LierFonction en lui passant en paramètres les informations auparavant stockées dans les propriétés
Hint et
Tag de l'item.
| OnClick des sous-items |
procedure TForm1.ExecItem(Sender: TObject);
var
Item : TMenuItem;
Execution: procedure(Sender: TObject); stdcall;
begin
If not (Sender is TMenuItem) then Exit;
Item := TMenuItem(Sender);
Execution := LierFonction('', Plugins[Item.Tag], Item.Hint);
Execution(Sender);
end; |
Pour l'appel de Lierfonction, notez que puisque Plugins[Item.Tag] est un handle valide de DLL, il n'y a pas besoin de fournir
le chemin d'un fichier DLL à charger.
N'oubliez pas de déclarer ExecItem dans la partie Public de TForm1 pour qu'elle puisse être utilisé comme événement des Items
Le code de chargement des plugins est maintenant terminé. Il ne reste qu'à libérer les DLL à la fermeture du programme pour parfaire le tout.
Il suffit pour cela de gérer l'événement OnClose de la fiche, en implémentant le code suivant :
| Libération des DLL |
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var i: Integer;
begin
for i := Low(Plugins) to High(Plugins) do
FreeLibrary(Plugins[i]);
end; |
Le programme est maintenant bel et bien terminé. Vous pouvez le tester en dupliquant autant de fois que vous le désirez la DLL que nous avons compilée
précédemment, les menus s'ajouteront automatiquement et pour chaque item vous aurez le message correspondant.
Cette méthode de liaison dynamique est donc très puissante, car elle offre une grande flexibilité à tout programme qui l'utilise et permet ainsi
de gérer au mieux toute sorte de modules, de plugins etc.
Comme vous l'avez peut-être remarqué il y a toutefois certaines choses auxquelles il faut faire attention pour éviter diverses problèmes.
C'est ce que nous allons traiter dans la dernière partie de cet article.
IV. Réflexes à prendre, astuces, ...
IV-1. Partager des types entre application et DLL
Comme dans le dernier programme que nous venons de développer, il est parfois nécessaire de créer des types spécifiques pour échanger
des données entre une DLL et l'application qui l'exploite.
Dans notre exemple il n'y en avait que deux, TItem et TItemsList, mais dans de vrais projets cela peut prendre une autre dimension.
Il est alors fortement conseillé, et c'est ce que nous avons fait ici, de déclarer tous ces types (et même éventuellement des fonctions communes)
dans une unité à part, pour ne pas répéter les déclarations entre les unités du programme et de la DLL.
Cette précaution permet par ailleurs de modifier beaucoup plus facilement ces types, et ainsi d'éviter les problèmes qui pourraient arriver dans le
cas d'un oubli de répercussion d'une modification entre deux fichiers, par exemple.
IV-2. Veiller aux types de variables utilisés
Ce n'est pas une nouvelle, les DLL sont faites pour être utilisées par plusieurs programmes de manière totalement transparente.
Seulement, plusieurs programmes peut signifier différents langages de programmation, et les choses se compliquent alors.
En effet, les types de données de base peuvent avoir entre les langages des caractéristiques bien différentes, comme notamment la place
prise en mémoire par une variable d'un type donné.
Si vos DLL sont destinées à être exploitées par des programmes qui ne seront pas nécessairement développés avec Delphi, il vaut donc mieux
utiliser des types génériques, non spécifiques à Delphi. Il faudra donc pour cela préférer les types définis par Windows, ou au minimum vérifier
que le type de variable que vous vous apprêtez à utiliser correspond bien au type attendu par le programme qui exploitera votre DLL.
Ainsi, faites attention lorsque vous utilisez des types propres à Delphi. Par exemple, le type integer est défini comme un nombre entier signé
de quatre octets. Cependant, dans des versions futures et grâce à l'évolution des architectures des processeurs, ce type pourrait très bien
devenir un entier signé de huit octets. Dans ce cas, on pourrait alors avoir de nombreuses violations d'accès ou au mieux des valeurs
incohérentes.
Les types fondamentaux dont les tailles ne sont pas censées changer sont les suivants :
- Shortint : 8 bits signé
- Smallint : 16 bits signé
- Longint : 32 bits signé
- Int64 : 64 bits signé
- Byte : 8 bits non signé
- Word : 16 bits non signé
- Longword : 32 bits non signé
Dans le même registre, attardons nous sur le type string. Vous vous souvenez sans doute de l'avertissement de Borland.
Pour donner plus de précisions, "borlndmm.dll" est une DLL qui sert à remplacer le gestionnaire de mémoire de Windows par défaut.
Cela parce que le type string n'est pas du tout un type standard, et que pour le gérer Borland a donc eu besoin de fournir un
moyen personnalisé. Cependant leur solution est plutôt contraignante, car elle limite le fonctionnement de ces dernières à des applications
Delphi qui utilisent l'unité "ShareMem.pas".
Il est donc préférable, pour s'affranchir de cette contrainte, de n'utiliser dans les paramètres des fonctions de vos DLL que des types
pChar ou ShortString, comme nous l'avons fait dans tous les codes de cet article.
Notez bien qu'il ne vous est pas du tout interdit d'utiliser le type string au sein de votre DLL. L'important est de ne pas l'utiliser
dans les échanges entre votre DLL et le programme qui l'emploie.
 |
Si vous souhaitez une alternative à ShareMem et l'utilisation de Borlndmm.dll, il existe FastSharemem.
Cette unité a pour but de remplacer Sharemem.pas en bien des points : allocations de mémoire plus rapides, unité plus légère
et surtout pas de DLL à déployer en plus de votre projet.
Merci à Romain Toutain pour cette information !
|
IV-3. Les conventions d'appel
Les conventions d'appel règlent le passage des paramètres des fonctions et procédures.
Il existe cinq conventions différentes :
- Pascal
- Register
- Stdcall
- Cdecl
- Safecall
Les différences qu'elles apportent sur le passage des paramètres à une fonction ou une procédure se jouent à trois niveaux :
- L'ordre dans lequel sont empilés les paramètres en mémoire
- A qui revient la responsabilité (entre l'appelant et l'appelée) de libérer la mémoire occupée par les paramètres
- Le passage ou non des paramètres par les registres processeur
La convention register est la seule à passer les paramètres par les registres processeur. Dans le cas où il y a
plus de trois paramètres, le quatrième et les suivants seront empilés en mémoire dans l'ordre de leur déclaration.
Cette convention, qui est celle utilisée par défaut lorsque les optimisations sont activées, remplace la convention
pascal. Celle-ci n'existe encore que par souci de compatibilité. Elle est la convention par défaut au cas où les
optimisations ne sont pas activées. Dans les deux cas, c'est à la routine appelée que revient la tâche de libérer la mémoire
allouée aux paramètres.
La convention stdcall (Standard Call) est la convention principalement utilisée par Windows, dans toutes ses DLL et donc notamment
dans les DLL contenant les fonctions de l'API. Cela en fait la convention la plus "standard" des cinq, et c'est pourquoi
nous l'utilisons pour communiquer entre nos DLL et nos programmes.
Lors d'un appel à une routine déclarée avec stdcall, les paramètres sont empilés en mémoire dans l'ordre inverse
de leur déclaration, et c'est la routine qui doit libérer la mémoire allouée pour ces derniers.
La convention cdecl est la convention des programmes et librairies écrits en C et C++. Elle est précisée par
Borland comme étant moins efficace que stdcall. Avec cette convention, les paramètres sont également empilés en
mémoire dans l'ordre inverse de leur déclaration, mais c'est au programme appelant qu'il revient la responsabilité de
libérer la mémoire qui leur est réservée.
Enfin, la convention safecall est identique par ses caractéristiques à stdcall. Elle est utilisée dans le
cadre des objets Automation et sert à mettre en place les notifications d'erreurs COM interprocessus, selon l'aide
en ligne de Delphi.
Pour conclure sur les conventions d'appel, la principale chose que vous devez garder à l'esprit lorsque vous développez
et/ou exploitez des DLL, c'est qu'il est impératif que les fonctions soient déclarées et appelées avec des conventions
d'appel strictement identiques. Sans cela, vous risquez bon nombre d'erreurs, plantages ou résultats hasardeux.
Si vous souhaitez une illustration de ce qu'on peut appeler "résultat hasardeux", essayez avec le programme Somme de la
première partie de déclarer la fonction importée en register par exemple, en gardant la DLL d'origine, puis
faites effectuer au programme la somme "0 + 0" !
IV-4. Déboguer une DLL
Comme pour tout programme, il arrive que le code d'une DLL pose quelques problèmes et il est alors nécessaire de la
déboguer. Cela dit, ce n'est pas aussi automatique qu'avec une application car une DLL n'est pas exécutable et a donc
besoin d'un "hôte" pour pouvoir être chargée en mémoire et déboguée.
Pour avoir un tel hôte, il vous faut donc tout d'abord une application compilée qui utilise le code de la DLL que vous
souhaitez déboguer.
Cette application prête, ouvrez le projet de votre DLL dans Delphi et accédez à la fenêtre "Paramètres" dans le menu "Exécuter" :

Paramètres d'exécution
Ici, renseignez le chemin de votre application dans le champ "Application hôte" et d'éventuels paramètres pour celle-ci. Si
vous voulez utilisez un répertoire de travail différent de celui de votre application, vous pouvez également l'indiquer.
Une fois cette fenêtre validée, vous pourrez lancer la commande "Exécuter" et utiliser les fonctionnalités de débogage dans
votre DLL.
L'onglet "Distant" permet d'utiliser le débogage à distance grâce au serveur de débogage à distance de Borland. Cette option
ne sera pas abordée ici.
Conclusion
Remerciements
Merci beaucoup à
Delphiprog,
Laurent Dardenne,
LLB,
Mac LAK
et
Nono40 pour leurs remarques et leur aide.
Merci également à
N*M*B qui m'a signalé une erreur dans les fichiers à télécharger et une optimisation de mon code.
Références


Les sources présentées sur cette page sont libres de droits,
et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation
constitue une oeuvre intellectuelle protégée par les droits d'auteurs. Copyright ©
2005 Olivier Lance. Aucune reproduction,
même partielle, ne peut être faite de ce site et de l'ensemble de son contenu :
textes, documents, images, etc sans l'autorisation expresse de l'auteur.
Sinon vous encourez selon la loi jusqu'à 3 ans de prison et jusqu'à 300 000 E
de dommages et intérêts.
Cette page est déposée à la
SACD.