I. Public visé et prérequis▲
Public visé : expert
Prérequis
- Combiner des procédures et des méthodesCombiner des procédures et des méthodes, de Laurent Dardenne
- Assembleur x86, dialecte BASM (Borland ASM) (voir éventuellement le tutoriel Utilisation de l'assembleur en ligne avec DelphiUtilisation de l'assembleur en ligne avec Delphi de Nono40Nono40)
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 :
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) :
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.
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 :
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 :
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 :
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).