Windows-API Callback-Funktion als Methode einer Klasse

Abstract

Konvertierung eines Methodenzeigers in einen Funktionszeiger.

Das Problem

Gegeben sei die Situation, dass man sich eine schöne Klasse entworfen hat, in der man eine Windows-API Funktion benutzen will, die aber eine Callback Funktion [1] benötigt. Zum Beispiel EnumWindows [2] zum enumerieren der Fenster. Das Problem ist nun, dass wir diese API-Funktion nicht einfach eine Methode unserer Klasse übergeben können, da ein Funktionszeiger und ein Methodenzeiger inkompatibel zu einander sind.

Ein Methodenzeiger ist letztendlich nichts anderes als ein Zeiger auf ein Record mit zwei Feldern. Ein Feld mit einem Zeiger für den Code und ein Feld mit einem Zeiger auf die Instanz. Wird eine Methode nun aufgerufen, wird sie ganz normal mit Hilfe des ersten Zeigers aufgerufen. Der zweite Zeiger wird als "unsichtbarer" Parameter in die Parameterliste der Methode eingefügt. Dies ist der sogenannte Self-Parameter, über den man auf die Instanz der Klasse zugreifen kann.

Eine mögliche Lösung

Um nun eine Methode für eine bestimmte Instanz über einen "normalen" Funktionszeiger aufrufen zu können, kann man folgendes tun: Man legt einen ausführbaren Speicherbereich an und schreibt in diesen die beiden Zeiger, den Aufruf der Methode und die Return-Anweisung. Den Zeiger auf diesen Speicherbereich kann man nun als Funktionszeiger der Callback-Funktion übergeben.

function TEnumWindows.MakeProcInstance(M: TMethod): Pointer;
begin
  // Ausführbaren Speicher alloziieren fü 15 Byte an Code
  Result := VirtualAlloc(nil, 15, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  asm
    // MOV ECX,
    MOV BYTE PTR [EAX], $B9
    MOV ECX, M.Data
    MOV DWORD PTR [EAX+$1], ECX
    // POP EDX (bisherige Rücksprungadresse nach edx)
    MOV BYTE PTR [EAX+$5], $5A
    // PUSH ECX (self als Parameter 0 anfügen)
    MOV BYTE PTR [EAX+$6], $51
    // PUSH EDX (Rücksprungadresse zurück auf den Stack)
    MOV BYTE PTR [EAX+$7], $52
    // MOV ECX, (Adresse nach ecx laden)
    MOV BYTE PTR [EAX+$8], $B9
    MOV ECX, M.Code
    MOV DWORD PTR [EAX+$9], ECX
    // JMP ECX (Sprung an den ersten abgelegten Befehl und Methode aufrufen)
    MOV BYTE PTR [EAX+$D], $FF
    MOV BYTE PTR [EAX+$E], $E1
    // hier kein Call, ansonsten würde noch eine Rücksprungadresse auf den Stack gelegt
  end;
end;

Die Methode MakeProcInstance schreibt eine neue Funktion in den Speicher, die vorher im Quelltext noch nicht vorhanden war. Aufgabe dieser Funktion ist es den Self-Parameter mit auf den Stack zu legen, so dass er als zusätzlicher "unsichtbarer" Parameter an die Methode über geben wird. Das heißt vorher sah der Stack so aus:

Rücksprungadresse
Parameter 1
Parameter 2
Parameter 3
...

Nach dem Code sieht der Stack dann so aus:

Rücksprungadresse
Parameter 0 = Self
Parameter 1
Parameter 2
Parameter 3
...

MakeProcInstance liefert jetzt die Adresse dieser neu implementierten Funktion zurück. Und diese Funktion macht eben nix weiteres, als self (welches ja in den 15 Bytes mit enthalten ist) als Parameter vorne dran zu hängen und die eigentliche Funktion (siehe Funktionszeiger) aufzurufen. (Danke an sirius für die Erklärung.)

Diese Lösung hat allerdings ein paar Nachteile:

Dies ist aber nicht weiter tragisch, da alle Windows-API Funktionen stdcall als Aufrufkonvention nutzen und als Rückgabewert meist einen Boolean haben.

Obige Methode lässt sich dann in der Klasse wie folgt nutzen (FEnumProc ist ein normaler Zeiger):

procedure TEnumWindows.ListWindows;
var
  Method: TMethod;
begin
  Method.Code := @TEnumWindows.EnumWindows;
  Method.Data := Self;
  FEnumProcInst := MakeProcInstance(Method);
  Windows.EnumWindows(FEnumProcInst, 0);
end;

Im Anhang befindet sich noch mal ein vollständiges Delphi Demo-Projekt.

Weitere Lösungen

In diesen Beiträgen der Delphipraxis hat Nico eine weitere Lösung entwickelt, die auch mit anderen Aufrufkonventionen zurecht kommt:
http://www.delphipraxis.net/post387139.html#387139 [3]
http://www.delphipraxis.net/post387393.html#387393 [4]

Quelle

...Methodenzeiger in Funktionszeiger konvertieren? [5]

Downloads

CallBackMethod.zip Wednesday, 29-Dec-2010 23:45:05 CET 971

2010-12-29T23:44:38 +0100, mail+homepage[at]michael-puff.de