§ April 16, 2009 16:35 by
beefarino |
A while back I posted a little puzzle about an exception I was hitting after passing a delegate to a native API call. In a nutshell, my application was passing an anonymous delegate to an unmanaged library call:
Result Code result = ExternalLibraryAPI.Open(
deviceHande,
delegate( IntPtr handle, int deviceId )
{
// ...
}
);
VerifyResult( result );
The call returns immediately, and the unmanaged library would eventually invoke the callback in response to a user pressing a button on a device. After a semi-random period, pressing the button would yield an exception in my application. The questions I asked were:
- What's the exception?
- How do you avoid it?
I've waited a bit to post the answers to see if anyone besides Zach would chime in. Zach correctly identified the nut of the problem - the delegate is being garbage collected because there is no outstanding reference on the anonymous delegate once the unmanaged call returns. With no one referencing the delegate object, the garbage collector is free to reclaim it. When the device button is pushed, the native library invokes the callback, which no longer exists in memory. So, to answer the first question, the specific exception that is raised is a CallbackOnCollectedDelegate:
CallbackOnCollectedDelegate was detected.
Message: A callback was made on a garbage collected delegate of type 'Device.Interop!Device.Interop.ButtonPressCallback::Invoke'. This may cause application crashes, corruption and data loss. When passing delegates to unmanaged code, they must be kept alive by the managed application until it is guaranteed that they will never be called.
The verbage in this exception message answers my second question. To avoid the exception, you need to hold a reference to any delegate you pass to unmanaged code for as long as you expect the delegate to be invoked. You need to use an intermediary reference on the delegate to maintain it's life.
Based on this, any of these examples are doomed to fail eventually, because none of them maintain a reference on the delegate object being passed to the unmanaged library:
ExternalLibraryAPI.Open(
deviceHande,
delegate( IntPtr handle, int deviceId )
{
// ...
}
);
ExternalLibraryAPI.Open(
deviceHande,
new ButtonPressCallback( this.OnButtonPress )
);
ExternalLibraryAPI.Open(
deviceHande,
this.OnButtonPress
);
The correct way to avoid the problem is to hold an explicit reference to the specific delegate instance being passed to unmanaged code:
ButtonPressCallback buttonPressCallback = this.OnButtonPress;
ExternalLibraryAPI.Open(
deviceHande,
buttonPressCallback
);
// hold the reference until we're sure no
// further callbacks will be made on the
// delegate, then we can release the
// reference and allow it to be GC'ed
buttonPressCallback = null;
At first I thought Zach's pinning solution was correct; however, you can only pin blittable types, of which delegates are not, so "pinning a delegate" isn't even possible or necessary. If you're interested, the details of how delegates are marshalled across the managed/unmanaged boundary are quite interesting, as I found out from Chris Brumme's blog:
Along the same lines, managed Delegates can be marshaled to unmanaged code, where they are exposed as unmanaged function pointers. Calls on those pointers will perform an unmanaged to managed transition; a change in calling convention; entry into the correct AppDomain; and any necessary argument marshaling. Clearly the unmanaged function pointer must refer to a fixed address. It would be a disaster if the GC were relocating that! This leads many applications to create a pinning handle for the delegate. This is completely unnecessary. The unmanaged function pointer actually refers to a native code stub that we dynamically generate to perform the transition & marshaling. This stub exists in fixed memory outside of the GC heap.
However, the application is responsible for somehow extending the lifetime of the delegate until no more calls will occur from unmanaged code. The lifetime of the native code stub is directly related to the lifetime of the delegate. Once the delegate is collected, subsequent calls via the unmanaged function pointer will crash or otherwise corrupt the process.
Thanks again Zach, and to everyone who reads my blog!