CLR SPY and Customer Debug Probes: The Collected Delegate Probe

The most common mistake made when passing a delegate to unmanaged code (marshaled as a function pointer) is to allow the delegate to be garbage collected before unmanaged code is finished using it.  This can happen because unmanaged code is invisible to the CLR garbage collector; it has no way to know when a delegate might still be in use by unmanaged clients.  Once the delegate is not being referenced by any managed code, it could be collected at any time, causing the function pointer to become invalid.

The reason this issue so greatly affects delegates is that function pointers, unlike COM objects, have no scheme to manage lifetime.  If you pass a managed object to unmanaged code, the CLR increments the reference count of its COM-Callable Wrapper (CCW) and ensures that the original object won't get collected until this count reaches zero (resulting from IUnknown::Release calls from unmanaged clients).  When passing a delegate to unmanaged code as a function pointer, however, the CLR does not keep it alive by default because there's no standard way for unmanaged code to notify the CLR that it's finished with the pointer. The CLR would have to keep delegates alive forever once they are passed to unmanaged code, and this would clearly not be acceptable.

Therefore, you must take explicit action to ensure that managed code maintains a reference to any such delegate for the duration in which the corresponding function pointer may be used.  The Collected Delegate probe makes you aware of cases where you neglect to do so.

The following Visual Basic .NET code passes a delegate to unmanaged code in order to handle control signals that would ordinarily end the process (such as Ctrl+C, Ctrl+Break, etc).  It fails to keep the delegate for the Callback method alive, however, since it instantiates it in-line as part of the call to SetConsoleCtrlHandler.

  Imports System

  Class HandleControlSignals

  Declare Function SetConsoleCtrlHandler Lib "kernel32.dll" _

    (ByVal HandlerRoutine As ConsoleCtrlDelegate, _

  ByVal Add As Boolean) As Boolean

  Delegate Function ConsoleCtrlDelegate( _

  ByVal dwControlType As Integer) As Boolean

  Public Shared Sub Main ()

  ' Add the Callback method to the list of handlers

  SetConsoleCtrlHandler(AddressOf Callback, True)

  ' ERROR: At this point, the delegate passed to

  ' SetConsoleCtrlHandler can be collected at any time!

  Console.WriteLine("Press 'Q' (followed by Enter) to quit.")

  While (Console.ReadLine() <> "Q")

  End While

  ' Restore normal processing

  SetConsoleCtrlHandler(Nothing, False)

  End Sub

  Public Shared Function Callback( _

  ByVal dwControlType As Integer) As Boolean

  Console.WriteLine("[HANDLED]")

  Callback = True

  End Function

  End Class

If you run this code as-is and press Ctrl+C to provoke the callback, chances are you won't run into the bug because the garbage collector probably won't run unless your system is running out of memory.  But if you add the following code to force a garbage collection right after the call to SetConsoleCtrlHandler, you'll likely get an ExecutionEngineException if you press Ctrl+C:

  GC.Collect()

  GC.WaitForPendingFinalizers()

Now if you enable the Collected Delegate probe, you'll get the following message (and the opportunity to inject a debug break) before the exception is thrown:

  Unmanaged callback to garbage collected delegate: ConsoleCtrlDelegate

So how do you fix this problem?  There are several approaches to keeping the delegate alive, such as using the GC.KeepAlive method appropriately, or storing the delegate as a static field of the class.

Although the benefit of this probe can only be seen after the appropriate garbage collection occurs, using this probe in combination with the Object Not Kept Alive probe and/or the Buffer Overrun probe forces these failures to happen reliably at the earliest possible time without writing code to explicitly provoke the garbage collector.  That's because these probes, which I'll discuss next, force collections for you.

Note that in order for this probe to determine that unmanaged code is calling back on a collected delegate, the CLR holds onto some extra memory indefinitely.  The stub that the unmanaged function pointer is associated with remains intact so the probe can intercept the call.  Therefore, enabling this probe means that you might see memory gradually "leak" over time, so this probe may not be appropriate for certain types of applications.