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".

Image non disponible

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
Sélectionnez

library Project1;

{ Remarque importante concernant la gestion de mémoire de DLL : ShareMem doit
  être la première unité de la clause USES de votre bibliothèque ET de votre projet
  (sélectionnez Projet-Voir source) si votre DLL exporte des procédures ou des
  fonctions qui passent des chaînes en tant que paramètres ou résultats de fonction.
  Cela s'applique à toutes les chaînes passées de et vers votre DLL --même celles
  qui sont imbriquées dans des enregistrements et classes. ShareMem est l'unité
  d'interface pour le gestionnaire de mémoire partagée BORLNDMM.DLL, qui doit
  être déployé avec vos DLL. Pour éviter d'utiliser BORLNDMM.DLL, passez les
  informations de chaînes avec des paramètres PChar ou ShortString. }

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
Sélectionnez

library DLL1;

{ Remarque importante concernant la gestion de mémoire de DLL .... }

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
Sélectionnez

library DLL1;

{ Remarque importante concernant la gestion de mémoire de DLL .... }

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
Sélectionnez

library DLL1;

{ Remarque importante concernant la gestion de mémoire de DLL .... }

uses
  SysUtils,
  Classes;

{$R *.res}


function Somme(A, B: Integer): Integer; stdcall;
begin
  Result := A + B;
end;

exports
  Somme; //Sans précision, le nom d'exportation sera "Somme" et l'index 1

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
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
Sélectionnez

....
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é :

 
Sélectionnez

 //Importation en spécifiant le nom de la fonction
function Somme(A, B: Integer): Integer; stdcall; external NomDLL name 'Somme'; 

//Importation en spécifiant son index
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 = ?"
Sélectionnez

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é :

Image non disponible

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
Sélectionnez

...

implementation

...

function LierFonction(DLL: String; var HandleDLL: THandle; NomFct: String; IndexFct: Integer = -1): Pointer;
begin
  //Valeurs de retour par défaut
  Result := nil;
  HandleDLL := 0;

  //Chargement de la DLL. On récupère son handle
  HandleDLL := LoadLibrary(pAnsiChar(DLL));

  //Si le chargement a échoué, on sort
  If HandleDLL = 0 then
    Exit;

  //On récupère l'adresse de la fonction voulue, que l'on renvoie
  If IndexFct < 0 then //Si l'index est négatif on utilise le nom sous forme de chaîne
    Result := GetProcAddress(HandleDLL, pAnsiChar(NomFct))
  else                  
    //Sinon, on utilise l'index comme identifiant
    Result := GetProcAddress(HandleDLL, pAnsiChar(IndexFct));
end;

procedure TForm1.btnSommeClick(Sender: TObject);
var HandleDLL: THandle; //Pour stocker le handle de la DLL chargée
    Somme    : function(A, B: LongInt): LongInt; stdcall; //Notre fonction, sous forme de variable
begin
  //On récupère le pointeur sur notre fonction nommée 'Somme' au sein de DLL1.dll
  Somme := LierFonction('DLL1.dll', HandleDLL, 'Somme');

  //Si Somme a bien reçu un pointeur valide sur une fonction
  If assigned(Somme) then
  try
    //L'appel de la fonction se passe comme avec une fonction ordinaire
    ShowMessageFmt('%d + %d = %d', [A.Value, B.Value, Somme(.Value, B.Value)]);
  finally
//Ne pas oublier de libérer la DLL lorsqu'elle n'est plus nécessaire
    FreeLibrary(HandleDLL);
  end
  else
    //Sinon, on affiche un message d'erreur : appeler Somme provoquerait une violation d'accès
    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.

Image non disponible

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
Sélectionnez

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
Sélectionnez

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
Sélectionnez

function Sauver(Fichier, Contenu: pAnsiChar; Methode: TCryptMethod; Valeur: Integer): Boolean; stdcall;
var
  Stream: TFileStream;
  p     : pChar;
  Data  : Char;
begin

  Result := True;

  //On copie le pointeur du contenu à enregistrer
  p := Contenu;

  //Création du stream de fichier
  Stream := TFileStream.Create(Fichier, fmCreate);
  try
    try
      //Tant qu'on n'a pas atteint la fin du texte à enregistrer
      while p^ <> #0 do
      begin

        //Cryptage du caractère en fonction de la méthode choisie
        case Methode of
          cmXOR  : Data := xorChar(p^, Valeur);
          cmCesar: Data := cesChar(p^, Valeur);
        end;

        //Puis écriture dans le fichier et on avance d'un caractère
        Stream.Write(Data, 1);
        inc(p);
      end;
      //Ecriture du caractère de fin de chaîne
      Data := #0;
      Stream.Write(Data, 1);
    except
      //S'il y a eu une erreur on renvoie False
      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é :

 
Sélectionnez

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
Sélectionnez

function Charger(Fichier, Destination: pAnsiChar; Methode: TCryptMethod; Valeur: Integer): Integer; stdcall;
var
  Stream: TFileStream;
  p     : pChar;
begin

  Result := 0;

  //Ouverture du fichier à charger
  Stream := TFileStream.Create(Fichier, fmOpenRead);

  If Destination = nil then
  begin
    //Si Destination = nil c'est la taille du buffer que l'on renvoie
    Result := Stream.Size;
    Stream.Free;
    Exit;
  end
  else
  try
    //Sinon on copie le fichier tel quel dans le buffer de destination
    Stream.Seek(0, soFromBeginning);
    Stream.Read(Destination^, Stream.Size);

    //Puis tant qu'on n'est pas à la fin du texte, on remplace chaque caractère
    //par sa valeur décryptée 
    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
Sélectionnez

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 :

Interface du programme

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
Sélectionnez

interface

...

type
  TCryptMethod = (cmXOR = 0, cmCesar = 1);

  //Déclarations statiques des fonctions de chargement/enregistrement du texte
  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'
Sélectionnez

procedure TForm1.Button2Click(Sender: TObject);
var
  Fichier: String;
begin

  //Choix de la destination du fichier à créer
  If not PromptForFileName(Fichier, 'Fichier texte (*.txt)|*.txt', '', '', '', True) then
    Exit;

  //Puis on appelle la fonction en prévenant d'une éventuelle erreur de sauvegarde
  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'
Sélectionnez

procedure TForm1.Button1Click(Sender: TObject);
var
  Fichier: String;
  Contenu: pAnsiChar;
  Taille : Integer;
begin

  //Demande du fichier à charger
  If not PromptForFileName(Fichier, 'Fichier texte (*.txt)|*.txt') then
    Exit;

  //On récupère la taille du buffer à créer par un premier appel avec Destination = nil
  Taille := Charger(pAnsiChar(Fichier), nil, TCryptMethod(cbxMethode.ItemIndex), CRYPT_VAL);

  //Allocation du buffer qui recevra le texte à charger
  GetMem(Contenu, Taille);

  //Ce buffer créé, on le passe à la fonction Charger pour y récupérer le texte de notre fichier
  Charger(pAnsiChar(Fichier), Contenu, TCryptMethod(cbxMethode.ItemIndex), CRYPT_VAL);
  //Ajout du texte au Memo
  memTexte.Lines.SetText(Contenu);

  //Libération du buffer 
  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
Sélectionnez

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
Sélectionnez

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
Sélectionnez

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
Sélectionnez

function GetItemsList: TItemsList; stdcall;
var
  i: Integer;
begin
  //On ajuste la taille du tableau
  SetLength(Result, Length(ItemsCaptions));

  For i := Low(ItemsCaptions) to High(ItemsCaptions) do
  begin
    //Nom de l'item
    Result[i].Nom      := ItemsCaptions[i];
    //Identifiant de la procédure associée à l'item
    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
Sélectionnez

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
Sélectionnez

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 :

Programme Liaison Dynamique

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
Sélectionnez

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
Sélectionnez

procedure TForm1.FormCreate(Sender: TObject);
var
  SR: TSearchRec;
  
  {* AjoutMenu sera insérée ici *}
  
begin
  //Recherche des fichiers DLL présents dans le répertoire "/plugins"
  If FindFirst(ExtractFilePath(ParamStr(0)) + '\plugins\*.dll', faAnyFile, SR) = 0 then
  begin
    //Ajout de la première DLL
    AjoutMenu(0);

    //Traitement des autres DLL éventuellement présentes
    while FindNext(SR) = 0 do
    begin
      //Une nouvelle DLL = un élément de plus
      AjoutMenu(High(Plugins) + 1);
    end;
    
    //Libération des ressources allouées pour la recherche 
    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
Sélectionnez

  procedure AjoutMenu(Index: Integer);
  var
    i: Integer;
    Item, SousItem: TMenuItem;
    ItemsList: TItemsList;
    //Deux fonctions à récupérer dans la DLL :
    PluginName  : function: ShortString; stdcall;
    GetItemsList: function: TItemsList;  stdcall;
  begin
    //Agrandissement du tableau de DLL
    SetLength(Plugins, Index + 1);
    //On n'a pas encore le handle de notre DLL chargée
    Plugins[Index] := 0;
    //Ce qui va être fait en liant la fonction GetName
    PluginName := LierFonction(ExtractFilePath(ParamStr(0)) + '\plugins\' + SR.Name, Plugins[Index], 'GetName');

    //Si la fonction n'est pas assignée, ce n'est pas un plugin
    If not Assigned(PluginName) then
    begin
      //Libération de la DLL si besoin
      If Plugins[Index] <> 0 then
        FreeLibrary(Plugins[Index]);
      //On remet le tableau de DLL à la bonne taille
      SetLength(Plugins, Index);
      //Information dans la ListBox
      lstDLL.Items.Add('La DLL "' + SR.Name + '" n''est pas un plugin valide.')
    end
    else
    begin
      //Création du nouveau menu, avec le nom du plugin
      Item := TMenuItem.Create(Menu);
      Item.Caption := PluginName;
      Menu.Items.Add(Item);

      //Ajout à la ListBox du nouveau plugin trouvé
      lstDLL.Items.Add('Nouveau plugin "' + PluginName + '" dans la DLL "' + SR.Name +'"');

      //Liaison de la fonction GetItemsList
      GetItemsList := LierFonction('', Plugins[Index], 'GetItemsList');

      //S'il y a eu un problème de chargement il ne faut pas continuer
      If not Assigned(GetItemsList) then
      begin
        lstDLL.Items.Add('Fonction "GetItemsList" manquante dans la DLL "' + SR.Name + '"');
        Exit;
      end;

      //On récupère maintenant les Items qui formeront le menu
      SetLength(ItemsList, 0);
      ItemsList := GetItemsList;

      //Et on les ajoute un à un au menu créé pour la DLL
      for i := Low(ItemsList) to High(ItemsList) do
      begin
        SousItem := TMenuItem.Create(Menu);
        SousItem.Caption := ItemsList[i].Nom;
        //On stocke dans Hint le nom de la fonction à appeler pour l'exécution de l'item
        SousItem.Hint    := ItemsList[i].Fonction;
        //Et dans Tag l'index de la DLL contenant la fonction de notre item
        SousItem.Tag     := Index;
        //La procédure qui gère l'événement OnClick
        SousItem.OnClick := ExecItem;
        //Ajout du sous-item au menu
        Item.Add(SousItem);
        //Information de cet ajout
        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
Sélectionnez

procedure TForm1.ExecItem(Sender: TObject);
var
  Item     : TMenuItem;
  Execution: procedure(Sender: TObject); stdcall;
begin
  //Si l'appelant n'est pas un TMenuItem on sort
  If not (Sender is TMenuItem) then Exit;

  //On récupère la fonction associée à l'item dans la DLL
  Item      := TMenuItem(Sender);
  Execution := LierFonction('', Plugins[Item.Tag], Item.Hint);

  //Et on appelle cette fonction
  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
Sélectionnez

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
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

C'est maintenant la fin de cet article. J'espère qu'il vous a permis de bien cerner comment se créent et s'emploient les DLL avec Delphi, et que les exemples de la troisième partie vous ont convaincu de leur efficacité à dynamiser vos applications.

Je suis ouvert à toute remarque, suggestion ou critique constructive quant à cet article. Il vous suffit de m'envoyer un message privé par le biais du forum.

Vous pouvez télécharger les sources des exemples de la troisième partie à ces adresses :
Exemple de la méthode statique
Exemple de la méthode dynamique

Si ces liens ne fonctionnent pas chez vous, utilisez ces miroirs :
Exemple de la méthode statique
Exemple de la méthode dynamique

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