Managing Low-Level Keyboard Hooks in VB .NET, Page 2
Implementing the Keyboard Delegate
To hook the keyboard, we are inserting our method into the address space for the existing low-level handler. This is what we did with interrupt handlers, and we still perform the same basic operation at a moderately higher level of abstraction. As is true with interrupt handlers, we need to hang onto the old handler, and ensure we call it. If we don't call the old handler, we prevent someone else's code from running. This would be rude unless our intention is to prevent someone else's keyboard code from running.
The delegate signature has to play by the same rules as a plain vanilla function pointer. The delegate signature must match an expected signature. Delegates will be invoked with the anticipation and necessity of receiving specific arguments and a return value if one is expected. In our example, the operating system will be calling with two integers and a structure that contains key state information. The caller will be expecting a return value, too. We can name the delegate anything, but as mentioned, the signature must match. The signature of our callback method is defined next.
Public Delegate Function KeyboardHookDelegate( _ ByVal Code As Integer, _ ByVal wParam As Integer, ByRef lParam As KBDLLHOOKSTRUCT) _ As Integer
Decomposed into chunks, we have:
- Public—The Delegate type is public
- Delegate—Defines this method signature as a subclass of the System.Delegate type
- Function—Indicates that the caller will expect a return value
- KeyboardHookDelegate—Is the name of the delegate
- Code—Is the name of the first argument, an Integer, that is passed by value
- wParam—Is a by-value Integer that we don't need in the example but is commonly found in message methods
- lParam—Very important to keyboard hooking; we need a pointer to the keyboard state information. This structure will tell us everything we need to know about the keys being pressed, released, and held. It is important to define this argument ByRef.
- As Integer—Indicates that the caller will be expecting an Integer.
We will actually need a method that very closely matches the signature of the delegate. The only point at which we can deviate is the name of the actual arguments. The callback method can use different names for the arguments, but the order and type of the arguments and the method type—function or subroutine—must match exactly.
Hooking the Keyboard
To hook the keyboard, we need to call the SetWindowsHookEx method. We will need a constant indicating what we want to hook, the idHook argument. We need a method that can be called back, the lpfn argument. A handle of the application doing the hooking, which is our application and the hmod argument, and the thread of the process we want to hook.
When hooking the keyboard in .NET, this part of the revision—from VB6–7 to VB.NET—is the most problematic. To facilitate, I have taken an important excerpt from the complete listing, listing 2. That excerpt is provided in listing 1.
Listing 1: Critical revisions to hooking the keyboard in .NET.
<MarshalAs(UnmanagedType.FunctionPtr)> _ Private callback As KeyboardHookDelegate Public Sub HookKeyboard() callback = New KeyboardHookDelegate(AddressOf KeyboardCallback) KeyboardHandle = SetWindowsHookEx( _ WH_KEYBOARD_LL, callback, _ Marshal.GetHINSTANCE( _ [Assembly].GetExecutingAssembly.GetModules()(0)).ToInt32, 0) Call CheckHooked() End Sub
Delegates are managed objects in .NET. This means that they are garbage collected. A problem occurs when we pass a delegate to the unmanaged code of the user32.dll API. Apparently, the garbage collector doesn't know that the delegate object is in use and after a short interval—roughly 47 seconds in experiments—the delegate is garbage collected. Consequently, when the API method attempts to call the method represented by the delegate back, a null reference exception occurs. To prevent the delegate from getting GC'd, we need to tag a delegate variable with the System.Runtime.InteropServices.MarshalAsAttribute, passing the enumerated value UnmanagedType.FunctionPtr. This tags the delegate argument, preventing it from being GC'd in an untimely fashion.
The first argument to SetWindowsHookEx is WH_KEYBOARD_LL. The second argument is the tagged delegate that contains the address of our local callback method. The third argument is the handle (hWnd) of the application doing the hooking, and passing 0 for the thread id means that we want to hook the keyboard for all threads.
For all of our efforts, if we forget the MarshalAsAttribute, the code fails miserably. You can read more about COMInterop in my new book Visual Basic .NET Power Coding from Addison-Wesley, available July, 2003.