IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Construire une procédure pointant sur une méthode en Delphi


précédentsommairesuivant

I. Public visé et prérequis

II. La problématique

Beaucoup de procédures des API Windows, ou même de bibliothèques tierces, acceptent un paramètre de call-back dont le type est un pointeur de procédure. Cependant, on aimerait lui transmettre un pointeur de méthode, ne fût-ce que pour avoir des informations de contexte supplémentaires au sein du call-back.

Comme le tutoriel Combiner des procédures et des méthodesCombiner des procédures et des méthodes vous l'explique, il n'est possible de transmettre un pointeur de méthode à la place d'un pointeur de procédure que si la procédure en question est prévue pour : qu'elle possède un paramètre « inutile » en première place. Or dans le cas que nous examinons maintenant, nous n'avons pas la main sur la définition de la procédure de call-back, puisqu'elle appartient soit à Windows, soit à une bibliothèque tierce dont nous n'avons peut-être même pas le code source.

Toute la problématique réside donc en ceci : obtenir un pointeur sur une procédure qui, lorsqu'on l'appelle avec des arguments donnés, appelle elle-même une méthode dont la signature est identique, mais qui évidemment demande un paramètre implicite Self supplémentaire.

III. L'idée de base

Il faut donc que, d'une manière ou d'une autre, nous puissions produire une procédure qui appelle une méthode. Si nous savions à l'avance sur quel objet (global) nous devrions appeler la méthode, on pourrait écrire ceci :

 
Sélectionnez
procedure CallBackProc(Param1: Integer; const Param2: string);
begin
  GlobalObj.CallBackMethod(Param1, Param2);
end;

Seulement voilà ! Nous ne pouvons pas connaître l'objet sur lequel appeler la méthode. Et à la rigueur, on pourrait même ne pas savoir la méthode à appeler !

Mais cette écriture est intéressante, car elle est bien ce que nous voulons. Si ce n'est que nous ne connaîtrons GlobalObj et @CallBackMethod qu'à l'exécution.

La solution ? Construire la routine CallBackProc pendant l'exécution du programme ! Et cela en allouant sur le tas une zone de données que l'on remplira avec du code machine x86, exécutable par le processeur.

IV. Que doit faire exactement CallBackProc ?

Avant de se lancer dans la tâche ardue de construire une routine pendant l'exécution, cernons bien ce qu'a besoin de faire cette routine, que nous avons appelée CallBackProc.

En réalité, elle ne doit pas faire grand-chose. Elle doit seulement ajouter un paramètre (CallBackObj) aux paramètres d'appel existant (sans toucher à ceux-ci), en première position ; puis rediriger vers la méthode à appeler (CallBackMethod).

La redirection se fait avec un simple JMP FAR assembleur. L'ajout du paramètre est plus délicat, et nous devons pour pouvoir le faire bien comprendre comment sont passés les paramètres.

V. Les conventions d'appel : le passage des paramètres

C'est un adage bien connu : l'informaticien est encore plus paresseux que le mathématicien. Je ne vais pas ici réinventer la roue, ni plagier. Je me contenterai donc de vous rediriger vers les sections Conventions d'appel et Paramètres et résultat de la fonction d'un tutoriel de Nono40.

Puisque Self est un type objet ou classe (donc pointeur) et est le premier paramètre, il est passé soit dans EAX (avec register), soit en premier paramètre sur la pile (avec les autres conventions).

VI. Comment utiliser les routines que nous allons développer ?

Pour bien comprendre à quoi peuvent servir les routines magiques que nous allons développer pas à pas dans la suite de ce tutoriel, nous allons d'abord montrer comment les utiliser.

Nous supposons l'existence d'une routine MakeProcOfStdCallMethod, définie comme suit (il s'agit d'une des routines qui vont être écrites) :

 
Sélectionnez
function MakeProcOfStdCallMethod(const Method: TMethod): Pointer;

Cette méthode prend en paramètre une méthode dont la convention d'appel est stdcall, et renvoie une procédure qui redirige sur celle-ci. Le pointeur renvoyé doit être libéré avec FreeMem.

Voici encore une petite routine pratique, qui permet de construire un record TMethod comme Point construit un record TPoint.

 
Sélectionnez
function MakeMethod(Code: Pointer; Data: Pointer = nil): TMethod;
begin
  Result.Code := Code;
  Result.Data := Data;
end;

Pour l'exemple, nous allons énumérer les handles des fenêtres du processus courant. Je choisis cet exemple, car il s'agit d'une question courante, qui a d'ailleurs réponse dans la FAQ Delphi : Comment récupérer les handles des fenêtres d'un processusComment récupérer les handles des fenêtres d'un processus ? ?

La FAQ montre comment exploiter le paramètre LParam du call-back pour avoir des informations de contexte. Je vais montrer ici comment faire sans - ce qui serait obligatoire si le call-back ne proposait pas ce paramètre « à contenu indéterminé ».

Au lieu d'avoir un call-back qui est une routine, nous faisons une méthode de la fenêtre principale :

 
Sélectionnez
type
  TFormMain = class(TForm)
    ...
  private
    function EnumWndCallBack(Handle: HWND; LParam: LPARAM): Boolean; stdcall;
    ...
  end;

function TFormMain.EnumWndCallBack(Handle: HWND; LParam: LPARAM): Boolean;
var
  ProcessID: DWord;
begin
  GetWindowThreadProcessId(Handle, ProcessID);
  if ProcessID = GetCurrentProcessID then
    ListBoxHandleList.Items.Add(IntToStr(Handle));
  Result := True;
end;

Quel est l'avantage d'une méthode par rapport à une routine ? L'avantage, c'est que le paramètre Self contient des informations de contexte. Ici il s'agit de l'instance de la fiche qui est concernée par le call-back. C'est beaucoup plus propre que d'utiliser une variable globale ; et si on travaillait avec des threads, ce serait même impossible à mettre en place autrement.

Nous voulons donc appeler EnumWindows comme suit :

 
Sélectionnez
procedure TFormMain.ButtonEnumWindowsClick(Sender: TObject);
begin
  ListBoxHandleList.Clear;
  EnumWindows(@TFormMain.EnumWndCallBack, 0);
end;

Cependant, cela n'est évidemment pas possible, car EnumWndCallBack est une méthode, pas une routine. Nous allons donc utiliser MakeProcOfStdCallMethod pour avoir une routine :

 
Sélectionnez
procedure TFormMain.ButtonEnumWindowsClick(Sender: TObject);
type
  TEnumWndProc = function(Handle: HWND; LParam: Integer): Boolean stdcall;
var
  EnumWndProc: TEnumWndProc;
begin
  ListBoxHandleList.Clear;
  EnumWndProc := MakeProcOfStdCallMethod(MakeMethod(
    @TFormMain.EnumWndCallBack, Self));
  try
    EnumWindows(@EnumWndProc, 0);
  finally
    FreeMem(@EnumWndProc);
  end;
end;

Et voilà qui est fait. Simple non ? Dans le reste du tutoriel, nous allons montrer comment écrire MakeProcOfStdCallMethod et compagnie (pour les autres conventions d'appel).


précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2007 Sébastien Doeraene. Aucune reproduction, même partielle, ne peut être faite de ce site ni 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.