Developpez.com - Delphi
X

Choisissez d'abord la catégorieensuite la rubrique :

Les chaînes de caractères dans les dlls

Le 12/02/2003 (mise à jour)

Par Jean-Luc Mellet (Alphomega)

Delphi et le langage C utilisent des conventions différentes en ce qui concernent 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 recherche.

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 (fontion basique s'il en est !). Pour réaliser cette fonction, j'avais 2 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.

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édiatemment 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 en 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 logiciel. 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 chaine. (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 majuscule, 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:

var
  S: String;
begin
  S := 'Une chaîne créée';
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 pointer sur un emplacement mémoire ( l'adresse de la suite de caractères représentant la chaîne) suivi des caratères assignés. On obtient alors un tableau de caractères que l'on peut représenter ainsi:

Car[0] = adresse mémoire du tableau
Car[1] = 'U'
Car[2] = 'n'
...
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 controler 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 taille variable à notre dll. Cette dll, que va t-elle faire ? Elle va recevoir 2 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 !

library LoginPerso;

uses
  windows,
  controls,
  UFrmLogin in 'UFrmLogin.pas' {FrmMotPasse};

{$R *.res}
function GetPassWord(UserName, PassWord: PChar; SizeUser, SizePass: Cardinal): boolean; stdcall;
begin
 with TFrmMotPasse.CreateWithParams(UserName, PassWord, SizeUser, SizePass, nil)  do
  begin
   ShowModal;
   result := ModalResult = mrOk;
   free;
  end;
end;

exports
 GetPassWord;

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

private
  FUserName, FPassWord: PChar;
public
  constructor CreateWithParams(Username, PassWord: PChar; SizeUser, SizePass: DWORD; AOwner: TComponent);
				  
{Ca, c'est pour la partie Interface. La partie implementation n'a que 2 procédures. La première 
 est une surcharge du constructor auquel je rajoute des paramètres.
}
constructor TFrmMotPasse.CreateWithParams(Username, PassWord: PChar;
  SizeUser, SizePass: Cardinal; AOwner: TComponent);
begin
 inherited Create(AOwner);
 FUserName := UserName;
 FPassWord := Password;
 with EdtUser do begin
  maxlength := SizeUser - 1;
  Text := FUserName;
 end;
 with EdtPass do begin
  maxlength := SizePass - 1;
  Text := FPassWord;
 end;
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. A chaque longueur, je retranche 1 pour tenir compte du zéro terminal en sortie de procédure. Ceci n'est qu'une conention. Habituellement, les developpeurs 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édures.

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

procedure TFrmMotPasse.btnOkClick(Sender: TObject);
begin
 if EdtUser.Text = '' then
  begin
   MessageBox(handle, 'Aucun utilisateur saisi !', 'Avertissement', MB_OK or MB_ICONWARNING);
   modalresult := mrCancel;
  end
 else
  begin
   StrPLCopy(FUserName, EdtUser.Text, EdtUser.MaxLength);
   StrPLCopy(FPassWord, EdtPass.Text, EdtPass.MaxLength);
   Modalresult := mrOk;
  end;
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 sur 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 function 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é !

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.

Generated with HyperDelphi

// function GetPassWord(UserName, PassWord: PChar; 
SizeUser, SizePass: DWORD): boolean; stdcall; external 'LoginPerso.dll';

{Méthode d'appel dynamique}
procedure TForm1.Button1Click(Sender: TObject);
type
  TMyProc = function(UserName, PassWord: PChar; SizeUser, SizePass: DWORD):
    boolean; stdcall;
var
  U, P: array[0..49] of Char;
  Handle: THandle;
  MyProc: TMyProc;
begin

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

  if Handle <> 0 then
  begin
    try 
	  @MyProc := GetProcAddress(Handle, 'GetPassWord');
      if @MyProc <> nil then
      begin
        U := 'Nom_User';
        P := 'Pass_User';
        if MyProc(U, P, sizeof(U), sizeof(P)) then
          ShowMessage(U + ':' + P);
      end;
	Finally  
      FreeLibrary(Handle); //Assure le déchargement de la dll
	end;  
  end
  else
    ShowMessage('Impossible de charger la DLL');
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.

Generated with HyperDelphi

function DossierWindows: string;
var
  WinDir: array[0..MAX_PATH] of char;
begin
  GetWindowsDirectory(Windir, MAX_PATH);
  result := StrPas(Windir);
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 4 lignes à notre code, mais il parait bon de présenter l'autre possibilité (merci à RDM(Epita) pour son rappel).

Generated with HyperDelphi

procedure TForm1.Button1Click(Sender: TObject);
var
  U, P: PChar;
begin

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

  if Hdle <> 0 then
  begin
    try
      @MyProc := GetProcAddress(Hdle, 'GetPassWord');
      if @MyProc <> nil then
      begin
        try
		  U := StrAlloc(50);
          P := StrAlloc(50);
          StrPLCopy(U, 'Username', length('Username'));
          StrPLCopy(P, 'Password', length('Password'));
          if MyProc(U, P, 50, 50) then
            ShowMessage(U + ':' + P);
		finally	
          StrDispose(U);
          StrDispose(P);
		end;  
      end;
    finally
      FreeLibrary(Hdle);
    end;
  end
  else
    ShowMessage('Impossible de charger la DLL');
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


Responsables bénévoles de la rubrique Delphi : Gilles Vasseur - Alcatîz -