I. Introduction

Qu'est-ce qu'une DLL me direz-vous ? Une dll est un fichier contenant des ressources et à la capacité de mettre à disposition ces ressources pour les programmeurs et donc pour les applications. Une DLL peut contenir des fonctions (exemple des fonctions mathématiques), une classe, des composants ou d'autre chose comme des icônes.
Pour ce qui est de son utilité, on peut distinguer trois cas d'utilisation :

  • Le principal est qu'elle permet de partager ses ressources avec tout le monde. Vous pouvez distribuer ou récupérer une dll et ensuite utiliser ses ressources. Par exemple si vous créez ou récupérez une DLL contenant des fonctions mathématiques, vous n'avez plus besoin de les coder dans votre application, il vous suffit de les importer. On peut dés lors les utiliser comme celles que l'on trouve dans Delphi comme sqrt.
  • le second est qu'elle permet de décomposer son application en module afin d'améliorer la maintenance et le déploiement de votre application. Si vous modifiez une partie du code dans une dll, il vous suffit de redistribuer uniquement cette dll modifiée pour mettre à jour l'application. Cela permet aussi d'organiser votre code par fonctionnalité. Je dois dire que l'intérêt de ce point est un peu flou. Il est quasiment inexistant pour les petites applications, il suffit de faire plusieurs unités et on peut aussi utiliser l'héritage, je ne parle pas ici de l'héritage des classes quoique mais de celui des unités (Pour rappel : on peut inclure des unités dans son projet en spécifiant qu'ils ne sont qu'une copie de l'original ou un lien, ce qui implique qu'elles se mettent à jour si l'original est modifié). Reste le problème de la compilation qui concerne tout le projet au contraire de la DLL où seul la dll concernée est à recompiler.
  • le troisième est que l'on peut charger dynamiquement une DLL. Quand une DLL est chargée dynamiquement dans une application, elle ne réside en mémoire que lorsqu'elle est utilisée (appelé) et ensuite la mémoire est libérée (la dll est déchargée).

II. Écriture des DLLs & Utilisation

Dans cette partie, nous allons voir comment écrire et utiliser une DLL de fonction puis une DLL de Classe et pour terminer une DLL de composant.

II-A. DLL de fonction

Pour ceux qui préfèrent la théorie avant la pratique, consultez les chapitres suivant.

Pour faire simple, utilisons l'expert DLL, Fichier -> nouveau -> Expert DLL.

Sinon, il vous faudra supprimer dans l'unité projet.prj le mot Program pour le remplacer par Library et supprimer la déclaration de l'unité Forms dans la clause Uses.
Supprimez aussi tout ce qui se trouve entre begin et end et supprimer la forme du projet, pour cet exemple, elle ne servira pas.

Nous allons commencer par la réalisation d'une DLL de fonction mathématique, libre à vous d'y ajouter d'autre fonction par la suite. Enregistrez le projet sous le nom LibMaths.

Notre première fonction sera la fonction factorielle. Bref rappel de math, la factorielle de 1 vaut 0 et la factorielle de 0 vaut 1. On note l'opération factorielle : n! où n est un nombre entier positif et ! l'opérateur. Le produit factoriel de n vaut n multiplier par le produit factoriel de n-1 (pour les esprits vifs que vous êtes, cette définition vous fait penser à la récursivité). Exemple : 3! = 3*2! = 3*2*1! soit 3*2*1 donc 6. et 4! = 4*3! = 4*6 soit 24. Pour ce qui ne connaissent pas la récursivité, pas d'inquiétude car ce n'est pas le sujet du cours, l'essentiel est de comprendre le principe.

  • Pour partir sur de bonnes bases, votre projet doit ressembler à ceci :
 
Sélectionnez
library LibMaths;

uses
    Windows;

begin

end.
  • Nous allons coder notre fonction factorielle entre la clause uses et le begin principal, soit :
 
Sélectionnez
function Factorielle(n : integer): integer;
begin
    // On aurait pu traiter le cas 0 et 1 séparément mais cela fait un test de plus
    if n = 0 then Result := 1
    else Result := n * Factorielle(n-1);
end;

Vérifiez bien que la fonction puisse s'arrêter quand vous faites de la récursivité sinon vous créez une boucle infinie.

  • Maintenant, nous allons exporter notre fonction pour qu'elle puisse être utilisée par d'autre application. Entre le end de la fonction et le begin du projet, saisissez :
 
Sélectionnez
exports
    Factorielle;

Le code source complet doit ressembler à ceci. Mais attention, il ne s'agit pas d'un exécutable, vous ne pouvez que le compiler. Pour ce faire, Tapez CTRL+F9 ou Projet -> Compiler LibMaths.

Source de la dll LibMaths
Sélectionnez
library LibMaths;

uses

    Windows;

function Factorielle(n : integer): integer;

begin

    if n = 0 then Result := 1
    else Result := n * Factorielle(n-1);

end;

exports
    Factorielle;

begin

end.

Nous allons maintenant nous attaquer à la programmation d'une application utilisant cette DLL. Pour cela enregistrer votre projet si ce n'est déjà fait et commencez un nouveau projet.

Dans la Form ajouter un composant EditBox pour la saisie de l'utilisateur et un Label ou EditBox pour afficher le résultat plus d'autres labels pour documenter votre application. Ajoutez aussi deux boutons l'un pour calculer et l'autre pour fermer. Pour le bouton Fermer, vous pouvez utiliser un BitBt (onglet : Supplément) et positionner sa propriété Kind à bkClose.

  • Entre implementation et les directives de compilation, importez la fonction. L'importation se fait en reprenant la déclaration de la fonction soit ici : function Factorielle(n : integer): integer; Attention à la casse (il faut le f majuscule) ajouter le mot reservé external 'nom du fichier de la dll'; sans l'extension.
 
Sélectionnez
implementation
 function Factorielle(n : integer): integer; external 'LibMaths';
{$R *.DFM}
  • On peut maintenant utiliser cette fonction, dans l'événement onclick du bouton Calculer, ajouter le code suivant :
    Remarque : Il n'y a pas de test sur la saisie de l'utilisateur dans un souci de clarté !
 
Sélectionnez
procedure TForm1.btTestClick(Sender: TObject);

var
    n : integer;

begin
    n := StrToInt(Edit1.Text);
    lbResultat.Caption := IntToStr(Factorielle(n));
end;
  • Exécutez et testez votre application. Notez que le fichier DLL doit se trouver dans le même répertoire que l'application l'utilisant ou dans le répertoire Window ou Window/Systeme (à éviter cependant sauf cas particuliers comme les DLL de windows :) ).
Le code source complet ci-dessous.
Sélectionnez
unit PrincipalFrm;

interface

uses
    Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
    StdCtrls, Buttons, Mask;

type
    TForm1 = class(TForm)
        BitBtn1: TBitBtn;
        Label1: TLabel;
        btTest: TButton;
        lbResultat: TLabel;
        Edit1: TEdit;

        procedure btTestClick(Sender: TObject);

    private
        { Déclarations privées }

    public
        { Déclarations publiques }

    end;

var
    Form1: TForm1;

implementation

function Factorielle(n : integer): integer; external 'LibMaths';

{$R *.DFM}

procedure TForm1.btTestClick(Sender: TObject);

var
    n : integer;

begin
    n := StrToInt(Edit1.Text);
    lbResultat.Caption := IntToStr(Factorielle(n));
end;

end.

II-B. Théorie & subtilités

Si vous avez lu les chapitres précédents, le premier mot-clé nouveau fut library. Ce mot indique au compilateur qu'il ne s'agit pas d'un programme principal (contenant main) et donc sans point d'entrée de type winmain. Ensuite la partie entre begin et end final correspond, comme pour les projets, à la partie initialisation. Dans cette section, vous pouvez initialiser les variables globales. Voilà pour les détails.

exports : permet de lister les fonctions à rendre visible/accessible depuis l'extérieur.
On peut y ajouter des options :

  • name : renomme la fonction exportée, par exemple quand le nom de la fonction existe déjà ailleurs.

    exports sqrt name 'sqrtv2';
    Exporte la fonction sqrt sous le nom sqrtv2 car sqrt existe déjà, c'est sqrtv2 qu'il faudra utiliser dans les applications utilisant cette DLL. Attention à la casse.

  • index : il spécifie un index pour la clause exports. Toutes les fonctions sont indexées, si on change l'index d'une fonction les index des fonctions suivantes reprennent à partir de celui-ci. N'est plus utilisé en environnement 32 bits (win98 et +) car le gain de performance est négligeable.

    exports sqrtv2 index 5;
    La fonction sqrtv2 sera accessible par l'indice 5.

Ces options sont cumulables, on peut exporter une fonction par nom et par index (sqrt name 'sqrtv2' index 5;).
Les noms des fonctions exportées sont sensibles à la casse (c'est-à-dire qu'il y a une différence entre majuscule et minuscule).

Convention d'appel:
La convention d'appel permet de spécifier le type de gestion du passage des paramètres passés à une fonction ou procédure d'une DLL. Elles sont pour l'instant au nombre de quatre :

  • stdcall : C'est la convention par défaut mais il est bon de le préciser quand même (en cas d'évolution du langage). Cette convention permet d'exporter ces fonctions pour une majorité des langages. Elle est apparue avec les systèmes 32 bits.
  • register : permet de placer les paramètres dans les registres donc d'optimiser leur vitesse d'accès.
  • pascal : utilise une convention propre au Pascal.
  • cdecl : convention du langage C.

Évitez d'utiliser les deux dernières conventions avant d'approfondir leur utilisation.

Pour déclarer une convention d'appel :
fonction sqrtv2(liste de paramètre et leur type)[:retour si function]; stdcall;

Exemple avec notre dll :

  • Dans le projet dll, on ajoute la convention d'appel à la fin de la déclaration de la fonction. Remarquez le mot export (sans s) qui se place après la convention d'appel et notez qu'il n'est pas obligatoire.
 
Sélectionnez
function Factorielle(n : integer): integer; stdcall; export;
begin
    if n = 0 then Result := 1
    else Result := n * Factorielle(n-1);
end;
  • Dans le projet utilisant la dll, on rajoute la même convention d'appel
 
Sélectionnez
implementation

function Factorielle(n : integer): integer; stdcall; external 'LibMaths';

{$R *.DFM}

Importation :

  • Importation par nom : on reprend la déclaration de la Dll et on ajoute external 'nom du fichier de la dll' sans l'extension
    function sqrtv2(n : integer): integer; stdcall; external 'NomDLL';
    On peut aussi changer le nom de la fonction sous lequel on veut la manipuler dans l'application ex :
    function sqrt(n : integer): integer; stdcall; external 'NomDLL' name 'sqrtv2'
    sqrtv2 est le nom de la fonction dans la DLL et sqrt le nom de la fonction dans l'application.
    Attention, pas de ';' entre les clauses external et name.
  • Importation par index : pas grand-chose à dire donc un exemple :
    function sqrt(n : integer): integer; stdcall; external 'NomDLL' index 10;

Manipulation des chaînes Longues :
Si vous avez utilisez l'Expert Dll, vous avez pu remarquer un commentaire vous mettant en garde sur l'utilisation des chaînes longues. Cette mise en garde ne concerne que la manipulation des chaînes comme paramètre, c'est-à-dire entrant ou sortant de la dll (en interne, pas de problème). En effet, Delphi gère les chaînes longues d'une façon qui est incompatible avec les autres langages. Vous avez dés lors deux solutions soit utiliser l'unité ShareMem et inclure la dll : BORLNDMM.DLL avec votre application, soit utiliser les types PChar ou ShortString (voir le chapitre DLL & Chaîne de caractère) en lieu et place du type String. Attention même les chaînes qui sont dans des enregistrements ou des classes sont concernées !

Voir le chapitre Chargement Statique/Dynamique pour les importations avancées.

II-B-1. DLL de classe

Les DLL permettent aussi d'exporter des classes à condition de comprendre quelques subtilités liées aux classes. Commençons par définir une classe dans un projet DLL (Nouveau-> expert dll). Aajoutez une nouvelle unité (Nouveau->Unité) qui contiendra notre classe et commencons sa définition.

 
Sélectionnez
unit MaClassDLLUnt;

interface

type
    TMaClass = class

    private
        Test : integer;

    public
        function GetTest : integer; virtual; stdcall; export;
        procedure SetTest(NewValeur : integer); virtual; stdcall; export;

    end;

implementation

function TMaClass.GetTest:integer; stdcall; export;
begin
    // Renvoie la valeur de la variable Test
    Result := Test;
end;

procedure TMaClass.SetTest(NewValeur : integer); stdcall; export;
begin
    // Change la valeur de la variable Test en NewValeur
    Test := NewValeur;
end;

end.
  • J'ai déjà indiqué les fonctions que je voulais exporter mais les déclarer dans la partie exports ne serait pas suffisant. Nous manipulons ici une classe et non un ensemble de fonction, c'est donc la classe qu'il nous faut exporter. Voici comment procéder :
 
Sélectionnez
function CreeInstanceMaClass : TMaClass; export; // Pas de virtual ici !

begin
    Result := TMaClass.Create;
end;
  • Dans la section exports, on exporte seulement cette fonction. On exporte la classe depuis le fichier source à savoir celui avec le mot library, c'est-à-dire le projet courant
Le code complet de la page principale
Sélectionnez
library Dll_ClassPrj;
uses
    SysUtils,
    Classes,
    MaClassDLLUnt in 'MaClassDLLUnt.pas';

{$R *.res}

exports
    CreeInstanceMaClass; // Seul la fonction permettant d'instancier (créer) un objet de la classe est exporté !

begin

end.

L'unité contenant la définition de la classe

 
Sélectionnez
unit MaClassDLLUnt;
interface

type
    TMaClass = class

    private
        Test : integer;

    public
        function GetTest : integer; virtual; stdcall; export;
        procedure SetTest(NewValeur : integer); virtual; stdcall; export;

    end;

    function CreeInstanceMaClass() : TMaClass; stdcall; export; // Pas de virtual ici !

implementation

function TMaClass.GetTest:integer; stdcall; export;
begin
    // Renvoie la valeur de la variable Test
    Result := Test;
end;

procedure TMaClass.SetTest(NewValeur : integer); stdcall; export;
begin
    // Change la valeur de la variable Test en NewValeur
    Test := NewValeur;
end;

function CreeInstanceMaClass() : TMaClass; stdcall; export; // Pas de virtual ici !
begin
    Result := TMaClass.Create;
end;

end.

Téléchargez le code source de la Dll

Voilà pour la DLL, nous allons maintenant créer une application l'utilisant. Contrairement à ce qui avait été dit auparavant export ne sera pas la dernière déclaration dans l'importation des fonctions. Il s'agit d'abstract qui terminera nos déclarations. Pourquoi ? Et bien parce que ! Mais plus sérieusement, pour pouvoir utiliser une classe il faut que l'application connaisse sa définition. C'est pourquoi nous avons déclaré toutes nos méthodes (le nom des fonctions quand elles sont dans une classe) virtuelles (Virtual) et que nous utilisons le mot-clé abstract qui permet de mettre la définition sans implémenter les méthodes (Pour plus de précision voir les cours sur les classes, comme celui de Image non disponible Frédéric BEAULIEU).

Dans un nouveau projet, voici pour l'apparence :

Image non disponible
DLL utilisation de classe

Ensuite, repérez la fin de la déclaration de la Form et ajoutez la définition de la classe contenue dans la DLL. Il suffit de recopier sa déclaration et d'ajouter abstrac à la fin de chaque méthode exportée. Après les directives de compilation ({$R *.DFM}), importez la fonction qui permet de créer une instance de la classe.
Voici le code complet :

 
Sélectionnez
unit UtilDll_ClassUnt;

interface

uses
    Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
    StdCtrls;

type
    TForm1 = class(TForm)
        edNouvValeur: TEdit;
        Label1: TLabel;
        lbValeur: TLabel;
        btManipClasse: TButton;
        Label2: TLabel;
        procedure btManipClasseClick(Sender: TObject);

    private
        { Déclarations privées }

    public
        { Déclarations publiques }

    end;

type
    TMaClass = class

    private
        // On n'importe pas la varaible Testn car l'utilisateur ne devrait jamais
        // manipuler directement cet valeur, voir les classes
        // pour comprendre la philosophie sous-jacente.
        // pas de déclaration de : Test : integer;
    public
        function GetTest : integer; virtual; stdcall; export; abstract;
        procedure SetTest(NewValeur : integer); virtual; stdcall; export; abstract;

    end;

var

    Form1: TForm1;

implementation

{$R *.DFM}

function CreeInstanceMaClass() : TMaClass; stdcall; external 'Dll_ClassPrj'; // Pas d'abstract ici, ce n'est pas une méthode !

procedure TForm1.btManipClasseClick(Sender: TObject);
var
    MaClass : TMaClass;

begin
    // La classe ne fait pas grand chose
    // mais permet de rester concentrer sur le sujet : les DLL
    // Crée une instance de la classe
    MaClass := CreeInstanceMaClass();

    // Affecte une nouvelle valeur à la variable Test
    MaClass.SetTest(StrToInt(edNouvValeur.Text));

    // Recupère la valeur de la variable Test puis l'affiche
    lbValeur.Caption := IntToStr(MaClass.GetBidon);

    // Libère la classe
    MaClass.Free;
end;

end.

Téléchargez le code source de la partie utilisation ici

II-C. DLL de composant

Nous allons réaliser une petite calculette sans prétention. Le but est d'apprendre à utiliser des composants Delphi dans une DLL et de voir quelques écueils.

Commencez un nouveau projet DLL et ajoutez une fiche. Enregistrer le projet (PMadll) et l'unité (UMadll). Dans la fiche ajoutez les composants et changez la propriété BorderStyle de la fiche à bsDialog, de façon à obtenir ce résultat :

Image non disponible
DLL composant : Etape 1

Le bouton égal est obtenu en mettant le signe '=' dans Caption et le signe '+' par un label avec une fonte différente :)
Dans la clause Uses ajoutez l'unité Forms avant l'unité UMadll.

  • Ajoutez le code suivant dans l'événement OnClick du bouton '=' :
 
Sélectionnez
procedure TMaForm.Button1Click(Sender: TObject);

begin
    Label3.Caption := FloatToStr(StrToFloat(Edit1.Text) + StrToFloat(Edit2.Text));
end;
  • J'ai renommé Form1 en MaForm pour la suite !
  • Retournez sur l'unité Projet, Il nous faut donner le moyen à l'application de créer la Form car vous remarquerez qu'il n'y a pas de création automatique contrairement aux exécutables.
 
Sélectionnez
procedure Creer_Form; stdcall; export;

begin
    // Crée une instance de Form
    DecimalSeparator := '.'  // change le séparateur décimal
    Application.CreateForm(TMaForm, MaForm);
    MaForm.Label3.Caption := '0';
end;

Comme nous créons la Form, nous allons nous donner le moyen de la libérer

 
Sélectionnez
procedure Free_Form; stdcall; export;

begin
    // Libère la Form
    MaForm.Free;
end;

Nous allons maintenant ajouter une fonction retournant la somme calculée.

 
Sélectionnez
function fncsomme():Double; stdcall; export;
begin
    with MaForm do
    begin
        // Affiche la forme de maniére modale (attend sa fermeture)
        ShowModal;
        // Renvoi le résultat du calcul
        Result := StrToFloat(Label3.Caption);
    end;
end;
  • et ajouter une version procédure qui ne sera utilisée que dans le chapitre autre langages (plus avant dans ce cours).
 
Sélectionnez
procedure proSomme(var R: Double); stdcall; export;
begin
    with MaForm do
    begin
        // Affiche la forme de maniére modale (attend sa fermeture)
        ShowModal;
        // Modifie la variable R (passage par adresse)
        R := StrToFloat(Label3.Caption);
    end;
end;
  • Pour terminer notre DLL exportons nos fonctions.
 
Sélectionnez
exports
   Creer_Form, Free_Form,
   fncsomme, proSomme;

Réalisons maintenant une application qui utilisera cette DLL. Enregistrez votre projet DLL si ce n'est déjà fait et commencez un nouveau projet application. Nous allons faire simple et nous contenter d'un bouton pour appeler la calculette et d'un label pour afficher le résultat, nous mettrons aussi la fiche en bsdialog.

Image non disponible
Dll composant Etape2

L'apparence étant faite, passons au codage. Il nous faut importer les fonctions permettant de manipuler la DLL. Donc nous allons importer les fonctions qui permettent de créer la form, de la libérer et bien sûr d'utiliser la calculette.
Juste après les directives de compilation ( {$R *.DFM} ) ajoutez :

 
Sélectionnez
procedure Creer_Form; stdcall; external 'pMadll.dll';

procedure Free_Form; stdcall; external 'pMadll.dll';

function Total:Double; stdcall; external 'pMadll.dll' name 'fncsomme';
  • Au niveau de la Function, vous aurez remarqué que j'ai renommé la fonction Total
  • Dans l'événement OnClick du Bouton 'Appel de la Calculette', saisissez le code suivant (fmUseDll est le nom de ma Form) :
 
Sélectionnez
procedure TfmUseDLL.btAppelCalculetteClick(Sender: TObject);
var
    R : Double;

begin
    Creer_Form;
    R := Total;
    lbResultat.Caption := FloatToStr(R);
    // Ne pas oublier de libérer la form à la fin
    Free_Form;
end;
  • Enregistrez votre projet et vérifiez que la DLL se trouve dans le même répertoire et que la casse des noms de fonctions importés soit correcte.
    Lancez votre application, vous disposez maintenant d'une DLL proposant une calculette :)

II-D. DLL & les chaînes de caractères

Si votre dll doit utiliser des paramètres de type chaînes longues (String), vous allez vous heurter à un problème. Heureusement deux solutions s'offrent à vous.

  • La plus simple, utiliser l'unité ShareMem en la déclarant dans la clause uses avant toutes les autres déclarations. Vous pouvez alors utiliser les chaînes longues comme bon vous semble. Cette méthode bien que simple à un gros inconvénient, pour que votre application puisse fonctionner, il lui faut alors une autre dll. Le problème et qu'il ne faut pas oublier de fournir cette DLL sous peine d'empêcher l'exécution de votre application. le fichier DLL à inclure : BORLNDMM.DLL.
  • La plus indépendante, utilisez les type PChars ou ShortStrings. En ce qui concerne les ShortStrings, ils se manipulent comme le type String si ce n'est le nombre limité de caractère qu'une chaîne de ce type peut contenir.

Attention même les chaînes qui sont dans des enregistrements ou des classes sont concernées !

Je ne parlerais ici que des chaînes de caractères de type PChars et de leur utilisation avec les DLL, le reste ne posant pas de difficulté. Tout d'abord un PChar est une chaîne un peu spéciale (avec un nom pareil, c'est évident que ça va être bizarre). Un Pchar est un pointeur sur une chaîne de caractère (tableau de Char) terminée par un indicateur de fin. Une bonne nouvelle lorsqu'on les manipule sous delphi, ils sont compatibles avec les String.

 
Sélectionnez
 MonPChar := MonString;

Par contre, l'indicateur de fin est le caractère #0. Du coup ce caractère ne doit pas se retrouver dans la chaîne (la chaîne s'arrête dés qu'elle rencontre celui-ci). Cet indicateur de fin est aussi appelé null ou Zéro Terminal.
Le point le plus délicat est l'espace mémoire occupé par ces PChars, contrairement au string, il est fixe. Mais avant d'entrer dans le vif du sujet un exemple d'utilisation vous permettra de fixer les choses :

  • Soit MaFonction : une fonction qui attend une chaîne de caractère de type PChar qui la manipule et la renvoie. Le détail de la fonction n'est pas ce qui nous intéresse.
    function MaFonction(S :PChar; Taille : integer);
    On ne met pas var devant la variable S car c'est un pointeur donc une adresse (Sinon vous passez une adresse d'une adresse O_o )
  • Si nous voulons utiliser cette fonction, on aura :
 
Sélectionnez
var
    U : array[0..51] of Char; // Déclaration d'une chaîne de caractères

begin
    U := 'affectation d''une valeur';
    MaFonction(U, sizeof(U));
    ... suite des instructions...
end;

Dans le passage de paramètre, on passe un pointeur sur la chaîne de caractères soit U par adresse et la taille de la chaîne. Quoique fasse la fonction MaFonction, elle ne devra pas dépasser cette taille-1 (pensez au caractère de fin de chaîne).
Pour copier une chaîne dans un Pchar en limitant la taille d'arrivée, vous pouvez utiliser la fonction StrPLCopy

 
Sélectionnez
  StrPLCopy(VariablePChar, MonString, TailleMaxi);

On peut très bien se passer des tableaux de caractère en utilisant tout de suite un PChar. Dans ce cas, il faut allouer de l'espace mémoire avant de les utiliser. Le code précédent deviendrait :

 
Sélectionnez
var
    U : PChar; // Déclaration d'un pointeur sur une chaîne de caractères

begin
 U := StrAlloc(50);
 StrLPCopy(U, 'affectation d''une valeur', 49);
 MaFonction(U, 50); // N'utilisez pas la taille de U surtout ! c'est à dire SizeOf(U)
 ... suite des instructions...
end;

Les notions vue ici sont aussi valables pour transmettre d'autres types de tableaux pas seulement des tableaux de caractère. Il suffit de remplacer les pointeurs PChar par un pointeur sur votre tableau.

Exemple avec un tableau de Double.

  • Dans l'application, on déclare le tableau normalement et on ne transmet que le premier élement par adresse. En transmettant le premier élément par adresse, on passe en fait un pointeur sur le tableau. On peut aussi utiliser un vrai pointeur mais pourquoi se fatiguer ?
 
Sélectionnez
var
    Mat : array[0..5] of double;

begin
    for i:=0 to 5 do
        Mat[i] := 0;
    TransmettreMat(Mat[0], 5); // On transmet aussi la taille !
end;
  • Dans la DLL, la fonction attend un paramètre par adresse (le pointeur sur le tableau) et la taille.
 
Sélectionnez
procedure transTableau(var T : Double;taille : integer);stdcall;export;
type
    TTabMat = array[0..6] of double;
    PTabMat = ^TTabMat;

var
    i : integer;
    pMat : PTabMat;

begin
    // fixe le séparateur décimal
    DecimalSeparator := '.';
    // fait pointer pMat sur l'adresse de T donc sur le tableau transmit
    pMat := @T;

    for i:=0 to taille do
        pMat^[i] := pMat^[i] + 1.33;
end;

Vous trouverez un tutoriel dédié à ce sujet ici (Remercier Laurent Dardenne car c'est lui qui m'a fourni le lien).

III. Chargement statique/dynamique

 

Nous avons jusqu'à présent chargé les dll de façon statique. Ceci présente plusieurs avantages, vous savez tout de suite si votre application n'arrive pas à charger votre dll, le code est plus compact et vous ne vous posez pas de question sur le déchargement de la DLL. Par contre la DLL est toute de suite chargée en mémoire et elle ne libèrera cet espace que lorsque l'application sera terminée, si les ressources de la DLL ne sont utilisées que ponctuellement quel gaspillage !
Nous allons apprendre ici à charger les DLL de façon dynamique, c'est-à-dire les charger en mémoire que pour le temps de leur utilisation effective. Un autre avantage est pour la création de plugin.
En effet, on peut réaliser des plugins en utilisant le chargement dynamique de DLL. En fonction du but recherché, on permettra à l'utilisateur de choisir des DLL (et donc des fonctionnalitées) ou alors on lui donnera la possibilité de charger ses DLL (pour apporter de nouvelles fonctionnalitées à votre application). Toutes fois dans le dernier cas, la réalisation est plus ardue, votre code doit pourvoir inclure ces nouvelles fonctions et surtout se protéger contre des fonctions mal écrites.

Voyons tout de suite les nouveaux mots-clé :
D'abord il faut charger la DLL, pour cela on utilise LoadLibrary :

 
Sélectionnez
 Handle := loadlibrary('MonfichierDll.dll');

Le handle permet de pointer sur cette DLL (pensez aux objets d'une classe), si le chargement échoue le handle à une valeur nulle.
Ensuite, il faut charger chaque fonction dont on n'aura besoin grâce à GetProcAddress :

 
Sélectionnez
 @MaFonction := GetProcAddress(Handle, 'NomDeLaFonctionDansLaDLL');

NomDeLaFonctionDansLaDLL est le nom de la fonction que l'on veut importer et MaFonction un pointeur sur un processus, ici on récupère son adresse !
Si le chargement de la fonction échoue le pointeur renvoie nil.
Lorsque la Dll n'est plus requise, il faut la décharger par l'appel à FreeLibrary :

 
Sélectionnez
 FreeLibrary(Handle);

Reprenez l'exemple sur la Factorielle (le projet et la dll associée). Nous allons modifier l'application pour qu'elle charge dynamiquement la DLL libMaths.

 
Sélectionnez
unit PrincipalFrm;

interface

uses
    Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
    StdCtrls, Buttons, Mask;

type
    TForm1 = class(TForm)
        BitBtn1: TBitBtn;
        Label1: TLabel;
        btTest: TButton;
        lbResultat: TLabel;
        Edit1: TEdit;
        procedure btTestClick(Sender: TObject);

    private
        { Déclarations privées }
    
    public
        { Déclarations publiques }
    end;

var
    Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.btTestClick(Sender: TObject);
procedure TForm1.btTestClick(Sender: TObject);
type
    // Déclare un type function pour pouvoir manipuler la fonction Factorielle
    TMyProc = function(x : integer):integer;

var
    n : integer;
    Handle: THandle;
    MaFactorielle: TMyProc;

begin
    n := StrToInt(Edit1.Text);
    // Charge une DLL dynamiquement et la libère ensuite
    Handle := loadlibrary('LibMaths.dll'); // Charge la DLL

    if Handle <> 0 then
    begin
        try
            // Charge dynamiquement une fonction de la DLL
            @MaFactorielle := GetProcAddress(Handle, 'Factorielle');
            if @MaFactorielle <> nil then
            begin
                lbResultat.Caption := IntToStr(MaFactorielle(n));
            end;
        Finally
            FreeLibrary(Handle); //Assure le déchargement de la DLL
        end; // Try..Finally
    end
    else
        ShowMessage('Impossible de charger la DLL.');
end;

end.

La déclaration de fonction a disparu et dans l'événement OnClic du bouton, on charge la DLL, la fonction Factorielle et une fois le travail accomplit, on libère la DLL.

Remarque importante : pour le type TMyProc, il faut reprendre la convention d'exportation déclaré dans la DLL.

Libération des ressources :
Remarquez l'imbrication du try..finally : on essaie d'abord de charger la DLL si l'opération réussie (ici traité par un if mais un try..except est tout à fait valable), on rentre dans un bloc protégé (try) même si une exception arrive la ressource sera libérée car elle est dans la section finally. Par contre, ce serait une erreur de mettre LoadLibrary dans le code protégé.

IV. DLL et autres langages de programmation

Dans l'intro, vous avez pu lire que la DLL pouvait être écrite dans un langage et utiliser dans un autre. Je ne vous présenterais pas ici des DLL écrite dans un autre langage et utilisé dans Delphi car cela à peut d'intérêt. Il vous suffit de reprendre ce que l'on a déjà vu. Parlons plutôt du cas où la DLL a été écrite en Delphi et l'application dans un autre langage. Une chose importante, les conventions d'appel dans la DLL doivent être compatibles avec le langage utilisé pour l'application. En général stdcall.

Un autre point très délicat concerne les paramètres manipulés par la DLL, notamment les chaînes longues voir le chapitre DLL & les chaînes de caractères. Mais aussi le composant Menu qui pour une raison qui m'échappe ne fonctionne qu'avec des applications Delphi.

Les précautions d'usage étant établies, essayons de réaliser deux exemples. Le premier utilisera le langage VB et le second HT Basic avec un but identique manipuler la Calculette que nous avons créée dans le chapitre DLL de composant. Que vous n'ayez ni l'un ni l'autre de ces langages n'a pas vraiment d'importance, c'est exemple n'étant que didactique. L'important est de savoir comment importer les ressources contenues dans une DLL compilée avec le langage que vous utilisez.

Reprenons notre DLL calculette et copions-la dans le répertoire où se retrouvera l'application en VB.
Créez un nouveau projet VB et occupons-nous de son apparence en réalisant une fiche dans ce style :

Image non disponible
DLL delphi et appli VB Etape1

Attaquons ensuite le codage, comme pour Delphi, nous allons importer les fonctions nécessaires à la manipulation de la DLL. Ajoutez les lignes suivantes au début de la page de code de la fiche :

 
Sélectionnez
Private Declare Sub CreerFormDll Lib "PMadll.dll" Alias "Creer_Form" ()

Private Declare Sub FreeFormDLL Lib "PMadll.dll" Alias "Free_Form" ()

Private Declare Function DllTotal Lib "PMadll.dll" Alias "fncsomme" () As Double

Idem pour le bouton 'appel de la calculette' :

 
Sélectionnez
Private Sub btAppelCalculette_Click()
    CreerFormDll
    lbResultat.Caption = DllTotal
    ' N'oublions pas de libérer la Form
    FreeFormDLL
End Sub

Exécuter votre application, et voilà une calculette faite en Delphi utiliser par VB.

Ci dessous le code source complet d'une application écrite en HT Basic utilisant la calculette écrite en Delphi. Comme je l'avais déjà dit auparavant, peu importe que vous utilisiez ce langage ou non. Notez par contre la similitude du codage dans les différents langages.

 
Sélectionnez
DLL UNLOAD ALL
! Change le répertoire en cours
MASS STORAGE IS "D:\delphi\Utilisation_de_la_DLL_par_HTB\dll"
! Charge la DLL
DLL LOAD "pMaDll"
! Importe les fonctions et les renomme.
DLL GET "STDCALL VOID pMaDll:Creer_Form" AS "Creerformdll"
DLL GET "STDCALL VOID pMaDll:Free_Form" AS "Freeformdll"
DLL GET "STDCALL VOID pMaDll:proSomme" AS "Dlltotal"

! rechange de répertoire
MASS STORAGE IS "D:\delphi\Utilisation_de_la_DLL_par_HTB"
! Un peu de ménage
CLEAR SCREEN

! Affiche à l'écran toutes les fonctions disponibles dans la dll (pas seulement celle qui sont importées)
LIST DLL

REAL R

! Crée la Form
Creerformdll
! Affiche la Form en Modal (Mécanisme interne de la fonction DllTotal)
Dlltotal(R)
! Libère la form
Freeformdll
! Affiche le résultat à l'écran
PRINT R
! Décharge la DLL et oui en HTB, il faut tout faire soi même ;)
DLL UNLOAD ALL
! Fin
END

V. Compléments d'informations

Suite aux nombreux Emails que j'ai reçus, j'ai décidé d'apporter quelques compléments d'informations, fournissant ainsi à l'ensemble des lecteurs les informations qu'une minorité a reçu. Je tiens à préciser qu'il ne faut hésiter à poser vos questions à la mailling liste car elle est faite pour cela :)

Inclure la form de la DLL dans l'application l'utilisant. (Ne pas afficher la form de la DLL dans la barre des tâches)

Si vous avez réalisez l'exemple ou d'autres DLL contenant des forms, vous avez peut être remarqué la présence dans la barre des tâches d'un nouvel icône portant le nom de la form. Ceci n'est pas forcément ce que vous souhaitez (comme ce fut le cas de la personne qui m'a soumis ce problème), sans compter que ce n'est pas du plus bel effet. Rassurez-vous il existe un moyen simple de ne pas l'afficher. Il suffit de donner à la DLL le handle de votre application (il s'agit d'un identifiant pour ceux que ça intéresse).

Dans la dll, au moment de créer la form principale, il faut changer le handle de l'application pour qu'il pointe sur celui de l'application appelante. Par exemple si on reprend la DLL du chapitre : DLL de composant, il suffit de changer la DLL ainsi :

 
Sélectionnez
procedure Creer_Form(HandlePere: HWND);stdcall;export;
begin
    // Changement du séparateur décimal
    DecimalSeparator := '.';
    
    // Lie la DLL à l'application appelante
    Application.Handle := HandlePere;

    // Crée la form (de la dll)
    Application.CreateForm(TMaForm, MaForm);

    // init
    MaForm.Label3.Caption := '0';
end;

Ajoutez l'unité Windows dans la clause uses pour utiliser les handles, si vous l'oubliez la DLL ne se compilera plus.

Ensuite dans le projet utilisant cette DLL, on transmet le handle de l'application à la DLL.
Pour la déclaration dans l'unité du projet :

 
Sélectionnez
procedure Creer_Form(HandlePere : HWND);stdcall;external 'pMadll.dll';

Et lorsque l'on crée la form de la dll :

 
Sélectionnez
Creer_Form(Application.Handle);

Veuillez noter que ce code fonctionne mais je trouve qu'il me manque des informations au niveau de la libération de la form lorsqu'elle est gérée ainsi. Donc si vous rencontrez des problèmes ne me jeter pas la pierre, je n'y suis pour rien et si vous avez plus d'info, je suis preneur comme toute la communauté.

VI. Conclusion

Petite astuce de débogage :
Pour explorer le code de votre DLL pendant l'exécution, ouvrez votre projet DLL par le menu Exécuter choisissez Paramètres et indiquez une application exploitant votre DLL dans Application hôte. La DLL se comporte alors comme si elle était exécutable, vous pouvez placer des points d'arrêt, faire du pas à pas …

Vous trouverez d'autres tutoriels sur les DLL ici.

Merci à Lionel Saliou et Laurent Dardenne pour leur aide dans la correction.

Sommaire