N/Direct - The .NET Interoperability Resource Center 

FAQ

 

 

 

Most Valuable Professional

My callback works for a while, but eventually throws an exception. What's wrong?
Written: 4/6/2003 Updated: 4/6/2003

If a callback works to begin with, but throws an exception (usually a NullReferenceException) after a while, the reason is probably that the callback delegate has been garbage collected and the function pointer that was passed to unmanaged code has been invalidated. You have to ensure that the delegate is alive for as long as the function pointer is used, by holding a reference to it. This means you can't create the delegate inline, the way it's done in the following code sample.

C#
delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet=CharSet.Auto)]
static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);
[DllImport("user32.dll")]
static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
const int WH_KEYBOARD_LL = 13;
static IntPtr hook;
static void Main()
{
  // Warning - this means trouble!
  hook = SetWindowsHookEx( WH_KEYBOARD_LL, new HookProc(MyLLKbdProc),
                           Marshal.GetHINSTANCE( typeof(Test).Module ), 0 );
  // GC.Collect();
  System.Windows.Forms.MessageBox.Show( "Press OK to cancel hook and exit." );
  UnhookWindowsHookEx( hook );
}
static IntPtr MyLLKbdProc(int nCode, IntPtr wParam, IntPtr lParam)
{
  Console.WriteLine( "message: {0}, vk: {1}", (int)wParam, Marshal.ReadInt32( lParam ) );
  return CallNextHookEx( hook, nCode, wParam, lParam );
}
    

VB.NET
Delegate Function HookProc(ByVal nCode As Integer, ByVal wParam As IntPtr, _
  ByVal lParam As IntPtr) As IntPtr
Declare Auto Function SetWindowsHookEx Lib "user32.dll" (ByVal idHook As Integer, _
  ByVal lpfn As HookProc, ByVal hMod As IntPtr, ByVal dwThreadId As Integer) As IntPtr
Declare Function UnhookWindowsHookEx Lib "user32.dll" (ByVal hhk As IntPtr) As Boolean
Declare Function CallNextHookEx Lib "user32.dll" (ByVal hhk As IntPtr, ByVal nCode As Integer, _
  ByVal wParam As IntPtr, ByVal lParam As IntPtr) As IntPtr
Const WH_KEYBOARD_LL As Integer = 13
Shared hook As IntPtr
Shared Sub Main()
  ' Warning - this means trouble!
  hook = SetWindowsHookEx(WH_KEYBOARD_LL, AddressOf MyLLKbdProc, _
                          Marshal.GetHINSTANCE(GetType(Test).Module), 0)
  ' GC.Collect()
  System.Windows.Forms.MessageBox.Show("Press OK to cancel hook and exit.")
  UnhookWindowsHookEx(hook)
End Sub
Shared Function MyLLKbdProc(ByVal nCode As Integer, ByVal wParam As IntPtr, _
                            ByVal lParam As IntPtr) As IntPtr
  Console.WriteLine("message: {0}, vk: {1}", wParam.ToInt32(), Marshal.ReadInt32(lParam))
  Return CallNextHookEx(hook, nCode, wParam, lParam)
End Function
    

This code installs a low-level keyboard hook to monitor keystrokes until the user closes the message box. The callback function pointer must remain valid until UnhookWindowsHookEx is called, but the way the code is written, it's likely that it will fail before that since no reference to the delegate is held. To speed up the process you can uncomment the GC.Collect call. The solution is to keep a reference to the delegate and ensure that it's valid until after the UnhookWindowsHookEx call.

C#
static void Main()
{
  HookProc hp = new HookProc(MyLLKbdProc);
  hook = SetWindowsHookEx( WH_KEYBOARD_LL, hp, Marshal.GetHINSTANCE( typeof(Test).Module ), 0 );
  // GC.Collect();
  System.Windows.Forms.MessageBox.Show( "Press OK to cancel hook and exit." );
  UnhookWindowsHookEx( hook );
  GC.KeepAlive( hp );
}
    

VB.NET
Shared Sub Main()
  Dim hp As HookProc = AddressOf MyLLKbdProc
  hook = SetWindowsHookEx(WH_KEYBOARD_LL, hp, Marshal.GetHINSTANCE(GetType(Test).Module), 0)
  ' GC.Collect()
  System.Windows.Forms.MessageBox.Show("Press OK to cancel hook and exit.")
  UnhookWindowsHookEx(hook)
  GC.KeepAlive(hp)
End Sub
    

In this case the HookProc delegate can be referenced just with a local variable, since SetWindowsHookEx and UnhookWindowsHookEx are both called directly from Main. A more generic solution would be to reference the delegate using a field variable in the class.