Les chaînes de caractères dans les DLL

Jean-Luc Mellet

Delphi et le langage C utilisent des conventions différentes en ce qui concerne la gestion des chaînes de caractères. J'ai récemment buté sur le problème lors de la construction d'une DLL devant utiliser des variables de type string. Suite à cela, et sachant pertinemment que je ne suis pas le seul dans ce cas, j'ai décidé d'écrire ce tutoriel pour éviter à d'autres de perdre du temps en tests et recherches.

Je n'entrerai pas dans des détails techniques inutiles, mais je vais plutôt vous présenter le problème d'une façon simple. Pour cela, je vais reprendre mon problème de base qui était la création d'une fonction de saisie de mot de passe (fonction basique s'il en est !). Pour réaliser cette fonction, j'avais deux options. La première était de créer un composant Delphi à partir d'un composant TForm. La seconde passait par la création d'une DLL utilisable avec un autre langage de programmation tel que C ou VB. Détaillons la création de la DLL.

- Titre : Les chaînes de caractères dans les DLL

- Auteur : Jean-Luc Mellet

- Parution : 12 février 2003

- Mise à jour : 25 décembre 2019

- Licence : Copyright ® 2019 Jean-Luc Mellet. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Création de la DLL

- II. Utilisation de la DLL

I - Création de la DLL

Pour commencer, créons le corps de la DLL par le menu Fichier/Nouveau/Autres, en terminant par un double-clic sur l'icône Expert dll. Vous remarquerez immédiatement le long commentaire inséré après la première ligne du code créé. Que nous explique-t-il ?

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

L'avertissement est très clair. Pour utiliser des variables de type String long dans une DLL créée avec Delphi, on doit passer par le gestionnaire de mémoire partagée BORLNDMM.DLL. ATTENTION : il est bien précisé que cela ne concerne QUE les String longs spécifiques de Delphi. Les PChar et les ShortString ne sont pas concernés.

Vous me répondrez: « Pourquoi pas ? » Et bien tout simplement pour des problèmes de compatibilité et de déploiement de logiciels. Toujours limiter au possible le nombre de fichiers à déployer chez le client est une bonne devise. Cela évite de retourner au bureau récupérer sur une disquette une DLL oubliée dans le programme d'installation. Il est donc prudent et judicieux d'utiliser des PChar ou des Shortstring comme on nous l'indique dans l'avertissement. Le problème, c'est si l'on passe directement un PChar comme paramètres, Delphi va se plaindre. Pourquoi ?

Tout d'abord, qu'est-ce qu'un PChar ? Voyons ce que dit l'aide Delphi !

Un PChar est un pointeur sur une chaîne à zéro terminal de caractères de type Char. Chacun des trois types caractère dispose d'un type de pointeur prédéfini : un PChar est un pointeur sur une chaîne à zéro terminal de caractères 8 bits. Un PAnsiChar est un pointeur sur une chaîne à zéro terminal de caractères 8 bits. Un PWideChar est un pointeur sur une chaîne à zéro terminal de caractères 16 bits. PChar est, avec les chaînes courtes, l'un des types chaîne qui existaient à l'origine dans le Pascal Objet. Il a été créé tout d'abord comme type compatible avec le langage C et l'API Windows.

Note : un PChar étant terminé par un caractère #0, n'oubliez pas que cela interdit la présence de ce caractère dans la chaîne, ce qui vous obligerait alors à utiliser le type String. Mais comme l'affichage passe par l'API Windows ne gérant que les PChar, le premier caractère #0 sera considéré comme fin de chaîne. (Merci à Merlin et RDM(Epita) pour cette précision.)

Delphi nous offre une compatibilité entre les string et les PChar. On peut ainsi écrire :

MonPChar := MaVariableSting;

Mais ça, ce n'est valable que pour Delphi. Un PChar étant un pointeur sur une chaîne, pour les autres langages, ça n'est ni plus ni moins qu'une adresse, pas une chaîne de caractères. La fin de cette chaîne est déterminée par la présence d'un caractère null, aussi appelé zéro terminal. En définissant un PChar, on définit (en quelque sorte), un début et une FIN. Je mets volontairement le mot FIN en majuscules, car c'est là le point le plus important de l'affaire.

Avec Delphi, on définit une variable de type string, autrement dit, une chaîne longue.
Pour les string longs, Delphi gère un offset négatif qui contient la longueur de la chaîne et un compteur de références qui servent à la gestion dynamique de la chaîne. De plus, Delphi ajoute toujours un #0 après le dernier char pour simplifier la compatibilité avec les PChar. (Merci à Merlin pour cette nouvelle précision.)
Quelle que soit la longueur de la chaîne que nous allons assigner à la variable, Delphi se chargera de l'allocation mémoire nécessaire pour contenir cette chaîne.
Par exemple :

1. var

2. S: String;

3. begin

4. S := 'Une chaîne créée';

5. end;

Le programmeur n'a pas à se soucier d'allouer une taille mémoire pour stocker cette chaîne. Delphi le fait pour nous. Il crée automatiquement un pointeur sur un emplacement mémoire (l'adresse de la suite de caractères représentant la chaîne) suivi des caractères assignés. On obtient alors un tableau de caractères que l'on peut représenter ainsi :

1. Car[0] = adresse mémoire du tableau

2. Car[1] = 'U'

3. Car[2] = 'n'

4. ...

5. Car[16] = 'e'

Delphi gère aussi les modifications de taille. Ce qui veut dire que si je modifie la chaîne, pour la remplacer par une chaîne plus longue, Delphi va gérer dynamiquement cette augmentation de taille pour contenir la nouvelle suite de caractères. Ce que ne permet pas le C (par exemple). Pour créer une procédure 'propre' dans une DLL, il sera nécessaire de contrôler la taille de la zone mémoire utilisée. Pour cela, l'utilisation d'un type array of char est tout indiquée.

Revenons à notre problème de base qui est de passer des paramètres string de tailles variables à notre DLL. Cette DLL, que va-t-elle faire ? Elle va recevoir deux valeurs « chaînes » qu'elle devra modifier pour renvoyer au programme appelant un nom d'utilisateur avec le mot de passe que celui-ci aura saisi. Voilà le code de la DLL !

1. library LoginPerso;

3. uses

4. windows,

5. controls,

6. UFrmLogin in 'UFrmLogin.pas' {FrmMotPasse};

8. {$R *.res}

9. function GetPassWord(UserName, PassWord: PChar; SizeUser, SizePass: Cardinal): boolean; stdcall;

10. begin

11. with TFrmMotPasse.CreateWithParams(UserName, PassWord, SizeUser, SizePass, nil)  do

12. begin

13. ShowModal;

14. result := ModalResult = mrOk;

15. free;

16. end;

17. end;

19. exports

20. GetPassWord;

22. begin

23. end.

Comme vous le voyez dans la déclaration de la fonction, Username et Password sont des PChar. Deux autres paramètres sont nécessaires pour une création « propre », il s'agit de SizeUser et SizePass qui sont les capacités en caractères pour Username et Password. Cette DLL utilise elle-même un TForm pour permettre la saisie des infos. Je ne donnerai pas le détail de la construction de cette forme, car j'y utilise des composants que vous n'aurez peut-être pas. Pour l'exemple, vous pouvez créer simplement un formulaire de saisie à votre goût. Le plus important n'est pas l'interface utilisateur, mais le code utilisé. Pour information, voici une capture d'écran de ce que j'ai créé.

Le code de cette unité est des plus simple.

1. private

2. FUserName, FPassWord: PChar;

3. public

4. constructor CreateWithParams(Username, PassWord: PChar; SizeUser, SizePass: DWORD; AOwner: TComponent);

6. {Ça, c'est pour la partie Interface. La partie implémentation n'a que deux procédures. La première

7. est une surcharge du constructor auquel je rajoute des paramètres.

8. }

9. constructor TFrmMotPasse.CreateWithParams(Username, PassWord: PChar;

10. SizeUser, SizePass: Cardinal; AOwner: TComponent);

11. begin

12. inherited Create(AOwner);

13. FUserName := UserName;

14. FPassWord := Password;

15. with EdtUser do begin

16. maxlength := SizeUser - 1;

17. Text := FUserName;

18. end;

19. with EdtPass do begin

20. maxlength := SizePass - 1;

21. Text := FPassWord;

22. end;

23. end;

On commence par stocker les « pointeurs » PChar dans des variables Private pour pouvoir les retrouver à la fermeture de la fenêtre de saisie des informations. Ensuite, on définit la longueur maxi des zones de saisie correspondantes en fonction des paramètres reçus, et on initialise les TEdit avec les valeurs de Username et Password. À chaque longueur, je retranche 1 pour tenir compte du zéro terminal en sortie de procédure. Ceci n'est qu'une convention. Habituellement, les développeurs C et les API fonctionnent en supposant que la longueur maxi passée tient compte du caractère terminal. Tout n'est qu'une question de commentaire dans la procédure.

La deuxième et dernière code l'évènement OnClick du bouton OK

1. procedure TFrmMotPasse.btnOkClick(Sender: TObject);

2. begin

3. if EdtUser.Text = '' then

4. begin

5. MessageBox(handle, 'Aucun utilisateur saisi !', 'Avertissement', MB_OK or MB_ICONWARNING);

6. modalresult := mrCancel;

7. end

8. else

9. begin

10. StrPLCopy(FUserName, EdtUser.Text, EdtUser.MaxLength);

11. StrPLCopy(FPassWord, EdtPass.Text, EdtPass.MaxLength);

12. Modalresult := mrOk;

13. end;

14. end;

StrPLCopy permet de transférer le contenu d'une chaîne dans un PChar, tout en tenant compte d'une longueur de chaîne maxi. Ainsi, on est sûr de ne pas « déborder » la zone mémoire réservée au PChar. Comme FUserName est égal à notre UserName passé en paramètre, en transférant le texte dans FUserName, c'est comme si on le transférait directement à UserName dans la fonction de la DLL. En remontant encore d'un niveau, UserName dans la DLL est le même « pointeur » que UserName dans le code qui appelle la DLL. Ainsi, nous renseignons directement le PChar de départ. On n'a pas transféré de chaînes de caractères, mais on a tout simplement rempli une zone mémoire définie dans le code qui appelle la DLL. Et le tour est joué !

II - Utilisation de la DLL

L'utilisation de la DLL est très simple. Dans votre code Delphi, soit vous ajoutez une déclaration external à votre code, soit vous appelez la DLL de fonction dynamique.

1. // function GetPassWord(UserName, PassWord: PChar;

2. SizeUser, SizePass: DWORD): boolean; stdcall; external 'LoginPerso.dll';

4. {Méthode d'appel dynamique}

5. procedure TForm1.Button1Click(Sender: TObject);

6. type

7. TMyProc = function(UserName, PassWord: PChar; SizeUser, SizePass: DWORD):

8. boolean; stdcall;

9. var

10. U, P: array[0..49] of Char;

11. Handle: THandle;

12. MyProc: TMyProc;

13. begin

15. Handle := loadlibrary('LoginPerso.dll');

17. if Handle <> 0 then

18. begin

19. try

20. @MyProc := GetProcAddress(Handle, 'GetPassWord');

21. if @MyProc <> nil then

22. begin

23. U := 'Nom_User';

24. P := 'Pass_User';

25. if MyProc(U, P, sizeof(U), sizeof(P)) then

26. ShowMessage(U + ':' + P);

27. end;

28. Finally

29. FreeLibrary(Handle); //Assure le déchargement de la dll

30. end;

31. end

32. else

33. ShowMessage('Impossible de charger la DLL');

34. end;

La méthode de chargement statique est plus discutable, car le code est chargé par le programme même si la fonction ne doit pas être utilisée lors de l'utilisation finale.

Tout repose sur la compatibilité entre les PChar et les array of char. Chaque DLL Windows que vous utilisez et qui demande un paramètre PChar doit en fait passer un paramètre array of char de taille définie avec le maximum que pourra contenir le PChar dans la DLL. Par exemple, l'API GetWindowsDirectory vous demande un paramètre PChar que vous devez passer sous forme de tableau avec une taille définie pour obtenir un résultat correct. La taille est également passée en paramètre avec MAX_PATH.

1. function DossierWindows: string;

2. var

3. WinDir: array[0..MAX_PATH] of char;

4. begin

5. GetWindowsDirectory(Windir, MAX_PATH);

6. result := StrPas(Windir);

7. end;

La fonction de notre DLL agit de la même manière. On lui passe un array de char de taille 50. Dans notre DLL, nous avons limité la taille de saisie à 50 (-1 pour le null de fin) caractères. Aucun débordement possible !

Il est également possible d'utiliser une autre version de notre code. StrAlloc et StrDispose permettent l'allocation de mémoire pour les PChar que nous pouvons alors utiliser comme type de départ. Par rapport à l'utilisation d'un array of char, cela nous oblige à rajouter quatre lignes à notre code, mais il paraît bon de présenter l'autre possibilité (merci à RDM(Epita) pour son rappel).

1. procedure TForm1.Button1Click(Sender: TObject);

2. var

3. U, P: PChar;

4. begin

6. Hdle := loadlibrary('DllLogin.dll');

8. if Hdle <> 0 then

9. begin

10. try

11. @MyProc := GetProcAddress(Hdle, 'GetPassWord');

12. if @MyProc <> nil then

13. begin

14. try

15. U := StrAlloc(50);

16. P := StrAlloc(50);

17. StrPLCopy(U, 'Username', length('Username'));

18. StrPLCopy(P, 'Password', length('Password'));

19. if MyProc(U, P, 50, 50) then

20. ShowMessage(U + ':' + P);

21. finally

22. StrDispose(U);

23. StrDispose(P);

24. end;

25. end;

26. finally

27. FreeLibrary(Hdle);

28. end;

29. end

30. else

31. ShowMessage('Impossible de charger la DLL');

32. end;

Voilà, j'espère que ces quelques explications vous aideront dans votre travail !!

Bonne programmation et à bientôt !

Code source .zip
Code source .cab