최근 본인은 이벤트를 감시하기 위해 훅을 사용하는 어플리케이션을 개발하고 있었다. 훅은 정말로 많은 작업에 있어서 유용하지만, 델파이에서는 직접적으로 지원되지 않기 때문에, 다음과 같이 Win32 API 호출을 사용해야 한다.
var
ThisHook : hHook;
.....
ThisHook := SetWindowsHookEx (WH_CALLWNDPROC, HookProc, HInstance, GetCurrentThreadID);
이것은 WH_CALLWNDPORC 타입의 훅을 현재 어플리케이션의 인스턴스와 쓰레드에 걸어준다. 훅이 발생할 때 HookProc를 호출하는데, 그것의 다음과 같이 선언되어 있다.
function HookProc (nCode: Integer; wParam, lParam: Longint): Longint; stdcall;
begin
//.... my code goes here
Result := CallNextHookEx (ThisHook, nCode, wParam, lParam);
//.... or goes in here
end;
여러분들은 볼랜드의 FORMS.PAS내에서 사용되는 이 호출들의 예를 발견할 수 있을 것이다. 또한 SetWindowsHookEx를 WIN32.HLP 파일에서 발견할 수 있으며 사용한 가능한 훅의 타입들에 대해서 볼 수 있을 것이다. 그러나, 이 예제에서 보듯이 훅을 사용하기 위해서는 전역 변수와 독립적으로 떨어져 나와 있는 함수를 가져야 한다는 것을 알아챘을 것이다. 이것은 동일한 어플리케이션내에서 여래개의 훅을 걸 때나, FORMS.PAS내의 경우와 같이 전역변수를 사용하지 않고 클래스내의 메소드나 변수를 참조하고 싶을 경우에는 문제가 된다.
동일한 문제가 클래스의 내부에 '윈도우 메세지 프로시져'를 작성하기를 원할 때 발생한다. 그러나, 볼랜드사는 클래스내의 '메소드'가 '윈도우 메세지 프로시져' 가 될 수 있도록 MakeObjectInstance(그리고 거기에 대응하는 FreeObjectInstace)를 제공해 주었다. 그래서, 본인은 동일한 기법으로 훅 함수로 메소드를 사용할 수 있지 않을까 하고 짐작했다. 그 결과가 되는 코드는 MakeObjectInstance를 위한 볼랜드가 제공했던 코드를 사용했지만 훅에 적절하도록 손을 봤다. 볼랜드의 원래 코드(Forms.Pas내에 있는 코드)에 약간의 변경을 가한 코드를 다음과 같이 제시한다.
unit
HookInst;
interface
uses
Windows;
type
THookCall = packed record
Code : integer;
WParam : WPARAM;
LParam : LPARAM;
Result : LResult
end;
THookMethod = procedure (var HookCall: THookCall) of object;
function MakeHookInstance (Method: THookMethod): pointer;
procedure FreeHookInstance (ObjectInstance: pointer);
implementation
const
InstanceCount = 313; // set so that sizeof (TInstanceBlock) < PageSize
type
PObjectInstance = ^TObjectInstance;
TObjectInstance = packed record
Code: Byte;
Offset: Integer;
case Integer of
0: (Next: PObjectInstance);
1: (Method: THookMethod);
end;
type
PInstanceBlock = ^TInstanceBlock;
TInstanceBlock = packed record
Next: PInstanceBlock;
Code: array[1..2] of Byte;
WndProcPtr: Pointer;
Instances: array[0..InstanceCount] of TObjectInstance;
end;
var
InstBlockList : PInstanceBlock = nil;
InstFreeList : PObjectInstance = nil;
function StdHookProc (Code, WParam: WPARAM; LParam: LPARAM): LResult; stdcall; assembler;
asm
XOR EAX,EAX
PUSH EAX
PUSH LParam
PUSH WParam
PUSH Code
MOV EDX,ESP
MOV EAX,[ECX].Longint[4]
CALL [ECX].Pointer
ADD ESP,12
POP EAX
end;
{ Allocate a hook method instance }
function CalcJmpOffset(Src, Dest: Pointer): Longint;
begin
Result := Longint(Dest) - (Longint(Src) + 5);
end;
function MakeHookInstance(Method: THookMethod): Pointer;
const
BlockCode: array [1..2] of Byte = ($59, $E9);
PageSize = 4096;
var
Block: PInstanceBlock;
Instance: PObjectInstance;
begin
if InstFreeList = nil then
begin
Block := VirtualAlloc (nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
Block^.Next := InstBlockList;
Move(BlockCode, Block^.Code, SizeOf(BlockCode));
Block^.WndProcPtr := Pointer(CalcJmpOffset(@Block^.Code[2], @StdHookProc));
Instance := @Block^.Instances;
repeat
Instance^.Code := $E8;
Instance^.Offset := CalcJmpOffset(Instance, @Block^.Code);
Instance^.Next := InstFreeList;
InstFreeList := Instance;
Inc(Longint(Instance), SizeOf(TObjectInstance));
until Longint(Instance) - Longint(Block) >= SizeOf(TInstanceBlock);
InstBlockList := Block
end;
Result := InstFreeList;
Instance := InstFreeList;
InstFreeList := Instance^.Next;
Instance^.Method := Method
end;
{ Free a hook method instance }
procedure FreeHookInstance (ObjectInstance: Pointer);
begin
if ObjectInstance <> nil then
begin
PObjectInstance(ObjectInstance)^.Next := InstFreeList;
InstFreeList := ObjectInstance
end
end;
end.
유닛의 문서화가 잘되지 못해 유감이다. 본인도 어떤 원리로 작동하고 있는지는 이해했지만, 이 코드를 완벽하게 이해하지는 못했다. 하지만 제대로 동작을 한다.
어떻게 사용할 것인가?
여러분이 클래스내에서 WH_CALLWNDPROC를 사용하기를 원한다고 하자. 여러분의 훅 함수, 포인터 변수(메소드에 대한 포인터가 될 것임), 그리고 훅 핸들(훅 이벤트를 올바르게 연결할 수 있기 위해서임)을 선언하라.
type
TMyClass = class (TWhat)
....
private
ThisHook : hHook;
HookProc : pointer;
....
function MyHookProc (var HookCall: THookCall);
....
end;
그리고, 클래스의 생성자에서 클래스 메소드에 대한 포인터를 만들고, API 함수를 호출하여 훅을 구동시킨다.
HookProc := MakeHookInstance (MyHookProc);
ThisHook := SetWindowsHookEx (WH_CALLWNDPROC, HookProc, HInstance, GetCurrentThreadID);
그리고, 다음단계로 훅 함수를 선언해야 하는데, 요것은 클래스의 메소드가 된다. :)
function TMyClass.MyHookProc (var HookCall: THookCall);
begin
....
with HookCall do
Result := CallNextHookEx (ThisHook, Code, wParam, lParam);
....
end;
마지막으로 소멸자에서 훅에 대한 해제 작업을 한다.
UnhookWindowsHookEx (ThisHook);
FreeHookInstance (HookProc);
그래서, 결론을 내리면...
전역 변수는 없다! 독립적으로 떨어져 나와 있는 훅 함수도 없다! 훅 함수는 쉽게 클래스 메소드를 참조할 수 있다.
Tags: 윈도우즈
|