Introduction
Many IE add-ins are using Windows Hook mechanism to monitor message traffic for mouse messages and to perform their intended task related with mouse activities. Since the global hook slows down the system performance drastically, the thread-specific hook will be used in most cases.
IE, on the other hand, is a multi-threaded SDI application. That is, each IE instance will be running in its own thread under the same process address space as long as the new instance of IE is created from the same process address space. (You can still create a new IE in new process address space by double-clicking the IE icon in Desktop again, or run command prompt and type "IExplore.exe").
Now, take a look at MSDN to check the hook API function's prototype:
HHOOK SetWindowsHookEx(
int idHook,
HOOKPROC lpfn,
HINSTANCE hMod,
DWORD dwThreadId
);
You can find that the third parameter of the function is thread identifier and it must be provided, otherwise it will monitor all existing threads running in the same desktop as the calling thread (and this is called as "global Windows hook", right?).
In order to install the mouse hook, we call ::SetWindowHookEx()
API function and provide WH_MOUSE
as hook type, and also provide the address of the global mouse hook procedure which is an application-defined or library-defined callback function, as shown below:
LRESULT CALLBACK MouseProc(
int nCode,
WPARAM wParam,
LPARAM lParam)
{
if (nCode == HC_ACTION)
{
if (lParam)
{
MOUSEHOOKSTRUCT *pMH = reinterpret_cast<MOUSEHOOKSTRUCT *>(lParam);
switch (wParam)
{
case WM_LBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
case WM_LBUTTONDBLCLK:
case WM_MBUTTONDBLCLK:
case WM_RBUTTONDBLCLK:
case WM_MOUSEWHEEL:
break;
default:
break;
}
}
}
return ::CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
To make a window hook chain mechanism work correctly, you should call ::CallNextHookEx()
API in MouseProc
callback function that you provided, before or after processing your own task. Here, you can see that the first parameter of ::CallNextHookEx()
API function is the handle to the current hook, and this is exactly the return value of ::SetWindowsHookEx()
API.
LRESULT CallNextHookEx(
HHOOK hhk,
int nCode,
WPARAM wParam,
LPARAM lParam
);
At this point, you will be able to see the necessity of the global map structure to find out the appropriate thread-specific HHOOK
handle in global MouseProc
callback function. Map is a good solution, and a hash map is even better since its look up cost is known to be O(1)
at best. Therefore, I believe the most of IE add-ins will use map structure in this context.
But, isn't this very similar to WndProc
and MFC's global HWND
map structure situation? Maybe I can use ATL assembly thunking technique to improve performance as did ATL over MFC. Since WM_MOUSEMOVE
message is one of the most frequent window message, even the minimum look up cost isn't that cheap to waste. To infinity and beyond :P
Implementation Note
I think you might be already tired with my bad English, so I will give you references here to help you understand of what the assembly thunk is and how it does its magic.
The core of my thunking implementation is shown below:
mov eax, dword ptr [esp+0Ch] // 8B 44 24 0C
mov [pThis->lParam], eax // A3 [DWORD pThis->lParam]
mov dword ptr [esp+0Ch], [pThis] // C7 44 24 0C [DWORD pThis]
jmp [MouseProc addr] // E9 [DWORD MouseProc addr]
I changed the MouseProc
to accept the pointer to C++ class (CMouseProcHook
class) as a parameter instead of LPARAM
. When BrowserHelperObject
gets connected through the call to IObjectWithSite::SetSite()
, the handle to the hook for the thread-specific IE from ::SetWindowsHookEx()
API function is stored in the C++ object, and it will cache the this
pointer in some well-known place, then install Windows hook with a special 'start-up' mouse hook procedure. In the 'start-up' mouse hook procedure, I crate a sequence of assembly language instructions (the thunk) that replace the LPARAM
parameter of the mouse hook procedure with the physical address of the this
pointer (retrieved from the well-known place), and then jumps to the 'real' mouse hook procedure with the altered stack. In 'real' mouse hook procedure, we grab the C++ class by simply casting the LPARAM
parameter into the CMoudeProcHook
, and get the 'real' LPARAM
as well as the handle to thread-specific hook. The picture below shows how the thunk alters the call stack and then forwards the call.
The LPARAM
of the MouseProc
was found to located at esp
+ 0Ch
, and I was able to double-check this by setting up the breakpoint in the first line of MouseProc
code and enabling Disassembly Debug Windows (ALT+8).
Now, new MouseProc
will be updated as shown below:
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
CMouseProcHook *pThis = reinterpret_cast<CMouseProcHook *>(lParam);
lParam = pThis->GetLPARAM();
if (nCode == HC_ACTION)
{
if (lParam)
{
MOUSEHOOKSTRUCT *pMH = reinterpret_cast<MOUSEHOOKSTRUCT *>(lParam);
switch (wParam)
{
case WM_LBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
case WM_LBUTTONDBLCLK:
case WM_MBUTTONDBLCLK:
case WM_RBUTTONDBLCLK:
case WM_MOUSEWHEEL:
break;
default:
break;
}
}
}
return ::CallNextHookEx(pThis->GetHHOOK(), nCode, wParam, lParam);
}
The last thing I should mention is a map structure used in my codes. The map is only here to avoid a multiple hook installation and to remove an already installed hook procedure from a hook chain. And the rest of the story goes the same as WndProc
thunk case. Refer to source code for details.
Using code
When you compile the source code, it will automatically register the output DLL file in BrowserHelperObject
section of Window Registry. And then you can run an IE, and make a click, or double-click any mouse button on the IE client area, which will simply display the pre-defined message in IE's status bar. When you finish the testing, you can merge the included registry file ("DelBHO.reg") to remove and clean up the registered BrowseHelperObject
entry from the Windows Registry manually.