The mechanism for keeping track of window hooks was very
different in 16-bit Windows.
The functions involved were SetWindowsHook
,
UnhookWindowsHook
and DefHookProc
.
The first two functions still exist today, but the third one has
been replaced with a macro:
// 16-bit prototype DWORD WINAPI DefHookProc(int nCode, WPARAM wParam, LPARAM lParam, HHOOK FAR *phk); // 32-bit macro #define DefHookProc(nCode, wParam, lParam, phhk)\ CallNextHookEx(*phhk, nCode, wParam, lParam)
Disclaimer: All code below is “reconstructed from memory”. The spirit of the code is intact, but the precise details may be off.
To install a windows hook in 16-bit Windows, you started by
calling SetWindowsHook
:
HHOOK g_hhkPrev; g_hhkPrev = SetWindowsHook(WH_WHATEVER, MyHookProc);
The return value from SetWindowsHook
must be saved
in a global variable, which we gave the somewhat provocative
name g_hhkPrev
.
The hook procedure itself went something like this:
// In Win16, hook procedures returned a DWORD, not an LRESULT. DWORD CALLBACK MyHookProc(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0) { ... } return DefHookProc(nCode, wParam, lParam, &g_hhkPrev); }
And then when you were finished, you removed the hook by
calling UnhookWindowsHook
:
UnhookWindowsHook(WH_WHATEVER, MyhookProc); g_hhkPrev = NULL;
Internally, the chain of hook functions was managed as a linked list, but instead of using some internal data structure to keep track of the hooks, the linked list was managed inside the HHOOK variables themselves.
The internal implementation of SetWindowsHook
was simply this:
// This array is initialized with a bunch // of "do nothing" hook procedures. HOOKPROC g_rgHook[NUMHOOKS]; HHOOK WINAPI SetWindowsHook(int nType, HOOKPROC pfnHookProc) { HHOOK hhkPrev = (HHOOK)g_rgHook[nType]; g_rgHook[nType] = pfnHookProc; return hhkPrev; }
Installing a hook merely set your hook procedure as the head of the hook chain, and it returned the previous head. Invoking a hook was a simple matter of calling the hook at the head of the chain:
DWORD CallHook(int nType, int nCode, WPARAM wParam, LPARAM lParam) { return g_rgHook[nType](nCode, wParam, lParam); }
Each hook procedure did its work and then sent the call
down the hook chain by calling DefHookProc
,
passing the HHOOK
by address.
DWORD WINAPI DefHookProc(int nCode, WPARAM wParam, LPARAM lParam, HHOOK FAR *phk) { HOOKPROC pfnNext = (HOOKPROC)*phk; if (nCode >=0) { return pfnNext(nCode, wParam, lParam); } ... more to come ... }
As you can see, it’s all blindingly simple:
Invoking a hook calls the first hook procedure,
which then calls DefHookProc
, which
knows that a HHOOK
is just a HOOKPROC
, and it forwards the call down
the chain by merely calling the next hook procedure directly.
The real magic happens when somebody wants to unhook.
Recall that the rule for hook procedures is that a negative
hook code should be passed straight to DefHookProc
(or in modern times, CallNextHookEx
).
This convention allows the hook system to use negative codes
to manage its own internal bookkeeping.
In this case, we’re using -1
as the
“unhook this hook procedure” code.
BOOL WINAPI UnhookWindowsHook(int nType, HOOKPROC pfnHookProc) { return DefHookProc(-1, 0, (LPARAM)pfnHookProc, (HHOOK FAR*)&g_rgHook[nType]); }
And then the real magic begins:
DWORD WINAPI DefHookProc(int nCode, WPARAM wParam, LPARAM lParam, HHOOK FAR *phk) { HOOKPROC pfnNext = (HOOKPROC)*phk; if (nCode >=0) { return pfnNext(nCode, wParam, lParam); } switch (nCode) { case -1: // trying to unhook a node if (pfnNext == (HOOKPROC)lParam) { // found it *phk = (HHOOK)pfnNext(-2, 0, 0); return TRUE; } // else keep looking return pfnNext(nCode, wParam, lParam); case -2: // report the next hook procedure return (DWORD)*phk; } return 0; }
And there you have it, the entire window hook system in two dozen lines of code. You have to give 16-bit Windows credit for being small.
Let’s walk through hook installation, dispatch, and removal to see
how this all works.
Suppose there is one WH_KEYBOARD
hook in the system.
Our variables are therefore set up like this:
// In USER g_rgHook[WH_KEYBOARD] = Hook1; // In HOOK1.DLL HHOOK g_hhkPrev1 = DoNothingHookProc; DWORD CALLBACK Hook1(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0) { ... work ... } return DefHookProc(nCode, wParam, lParam, &g_hhkPrev1); }
Now suppose you want to install a new hook, Hook2
.
// In HOOK2.DLL HHOOK g_hhkPrev2; g_hhkPrev = SetWindowsHook(WH_KEYBOARD, Hook2);
The SetWindowsHook
function just puts your function
in as the new “head” hook function and returns the old one.
// In USER g_rgHook[WH_KEYBOARD] = Hook2; // In HOOK2.DLL HHOOK g_hhkPrev2 = Hook1; DWORD CALLBACK Hook2(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0) { ... work ... } return DefHookProc(nCode, wParam, lParam, &g_hhkPrev2); } // In HOOK1.DLL HHOOK g_hhkPrev1 = DoNothingHookProc; DWORD CALLBACK Hook1(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0) { ... work ... } return DefHookProc(nCode, wParam, lParam, &g_hhkPrev1); }
Now suppose the window manager decides it’s time to fire the
WH_KEYBOARD
hook.
It starts with CallHook
which calls
g_rgHook[WH_KEYBOARD]
that takes us to
Hook2
.
That hook function does its work, then calls
DefHookProc(..., &g_hhkPrev2)
,
which dispatches the hook to g_hhkPrev2 == Hook1
.
Similarly, the hook travels through Hook1
,
then
DefHookProc(..., &g_hhkPrev1)
,
where it finally reaches the DoNothingHookProc
which does nothing and ends the hook chain.
Now suppose that HOOK1.DLL
decides to uninstall its
hook. It therefore calls
UnhookWindowsHook(WH_KEYBOARD, Hook1)
.
This starts off the hook chain with the internal hook code -1
and &g_rgHook[WH_KEYBOARD]
as the first hook pointer.
This activates the case -1
in DefHookProc
code
path, which dereferences its phk
parameter and obtains
g_rgHook[WH_KEYBOARD] == Hook2
.
Since this is not equal to Hook1
, the call forwards down
the chain to Hook2
.
Like a good hook function, Hook2
reacts to the negative
hook code by handing the call directly to
DefHookProc(-1, ..., &g_hhkPrev2)
.
This time, *phk == g_hhkPrev2 == Hook1
,
so the test succeeds and we dispatch the hook down the chain
with a new internal code of -2
, which means,
“Tell me what the next hook procedure is”.
This dispatch calls Hook1
which (since the notification
code is negative) immediately passes the call to
DefHookProc(-2, ..., &g_hhkPrev1)
.
This now triggers the case -2
code path, which
just returns *phk == g_hhkPrev1 == DoNothingHookProc
.
This value is returned to the DefHookProc(-1, ...)
which stores the result into *phk == g_hhkPrev2
;
the result is that you have g_hhkPrev2 = DoNothingHookProc
.
Finally, DefHookProc
returns TRUE
to indicate that the hook was successfully uninstalled.
This value is then returned out from all the nested function calls
to the original caller of UnhookWindowsHook
.
Observe that at the end of this unhook exercise, we get the desired result:
// In USER g_rgHook[WH_KEYBOARD] = Hook2; // unchanged // In HOOK2.DLL g_hhkPrev2 = DoNothingHookProc; // updated! DWORD CALLBACK Hook2(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0) { ... work ... } return DefHookProc(nCode, wParam, lParam, &g_hhkPrev2); }
And Hook1
is out of the hook chain, as we desired.
This really isn’t all that complicated.
All we did was delete a node from a linked list.
It’s just that this particular linked list cannot be traversed
by just dereferencing pointers.
Instead, we have to issue a function call and ask the
recursive function to perform the work on the “next” node for us.
That’s what the negative nCode
values are for.
Every time I work through this exercise, I am impressed by how compactly 16-bit Windows was written. In just two dozen lines of code, we managed a linked list of function calls, including a dispatching system as well as arbitrary deletion from the middle of the linked list, and all without any memory allocation.
(And because I know people are going to try to change the topic: Remember, I’m talking about 16-bit Windows, not 32-bit window hooks.)
Next time, we’ll look at one way people abused this simple system.
0 comments