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,        // hook type
  HOOKPROC lpfn,     // hook procedure
  HINSTANCE hMod,    // handle to application instance
  DWORD dwThreadId   // thread identifier
);

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:

Collapse
LRESULT CALLBACK MouseProc(
  int nCode,        // hook code
  WPARAM wParam,    // message identifier
  LPARAM lParam)    // mouse coordinates
{
    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:
                // do something
                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,      // handle to current hook
  int nCode,      // hook code passed to hook procedure
  WPARAM wParam,  // value passed to hook procedure
  LPARAM lParam   // value passed to hook procedure
);

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:

Collapse
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:
                // do something
                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.

JaeWook Choi