When opening a midi in or out port (device) you can pass a delegate that will be called when an event takes place on the port. Events such as receiving midi and opening and closing a port. This delegate instance should be kept alive (not Gargabe Collected) during the time the port is open and the midi function can call back on it.
Most solutions you see is that the instance of the callback delegate is stored as a member in the port class itself. This ties the lifetime of the delegate to the lifetime of the port class. This seems like a reasonable solution, right?
But what if the client of your port class doesn't play by the rules? Suppose the application that uses your port class forgets to close (or dispose) the port class. As soon as the client releases all references to the port class it will be elligable for garbage collection and so is the callback delegate. When the midiXxx API now tries to callback on the delegate you provide during the Open call, you'll get an exception that says someone was trying to call a garbage collected delegate.
Microsoft has added a new feature in the .NET 2.0 framework called SafeHandles. A safe handle is like an object wrapper around a native handle and is guarenteed to finalize (normal classes are not). The SafeHandle also provides protection against some security attacks (recycling of handles). The native midi handle that is returned from a midi-open call is usually stored as Int32 or IntPtr inside the port class. If a client forgets to call the close method on your port class the port stays open and cannot be used again. All the (managed) code that has access to the (native) port is collected eventually and your app starts to misbehave.
So lets walk through the solution for both these problems: keeping the delegate alive and ensuring the midi handles are closed.
My solution for keeping the delegate alive is simple. We cache one instance in a static variable. Static variables pretty much live untill the AppDomain dies. This introduces a new problem though: Multiple instances of the port class are now called back on the same delegate. Luckily we can supply 'user data' that is passed with each call to the callback function. We pass in the this reference of the port class itself.
Lets get to the code. First we declare the P/Invoke methods for the MIDI out port.
static class NativeMethods
{
public const int CallbackFunction = 0x30000;
public delegate void MidiOutCallback(IntPtr midiHandle, int message,
IntPtr userData, int param1, int param2);
[DllImport("winmm.dll")]
public static extern int midiOutClose(IntPtr midiHandle);
[DllImport("winmm.dll")]
public static extern int midiOutOpen(out MidiSafeHandle midiHandle, int deviceId, MidiOutCallback callback, IntPtr userData, int flags);
}
Notice that I specify the MidiSafeHandle type to be returned from the midiOutOpen function. The P/Invoke plumbing knows about SafeHandles and will create one. The MidiSafeHandle is derived from the Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid base class. You should have seperate SafeHandle implementations for the different flavors of midi handles.
class MidiSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public MidiSafeHandle()
: base(true)
{ }
protected override bool ReleaseHandle()
{
bool success = (NativeMethods.midiOutClose(this.handle) == 0);
handle = IntPtr.Zero;
return success;
}
}
Notice that the native midi handle is closed in the override of the ReleaseHandle method.
Next lets look at the MidiOutPort class. The first part is shown below:
class MidiOutPort : IDisposable
{
private GCHandle _gcHandle;
private MidiSafeHandle _midiHandle;
public MidiOutPort()
{
_gcHandle = GCHandle.Alloc(this, GCHandleType.Weak);
}
public void Open(int device)
{
int result = NativeMethods.midiOutOpen(
out _midiHandle, device, _callback, GCHandle.ToIntPtr(_gcHandle),
NativeMethods.CallbackFunction);
}
public void Close()
{
if (_midiHandle != null)
{
_midiHandle.Close();
}
}
The constructor allocates a GCHandle that contains a weak reference to our MidiOurPort instance. This GCHandle is used to pass a reference to our instance to the native midiOutOpen method that calls our delegate with it. A weak reference will not keep the MidiOutPort instance alive, so we do not interfere with garbage collection and the lifetime of the object.
The Open method opens the midi out port and returns a valid MidiSafeHandle on success. Notice we pass in the static delegate reference (_callback) and a reference to our own instance (_gcHandle). Opening the port will call the callback with an MOM_OPEN message.
The Close method just closes the MidiSafeHandle which in turn will use the midiOutClose API to actually close the device. Closing the port will call the callback with an MOM_CLOSE message.
private void OnMessage(int message, int param1, int param2)
{
Console.WriteLine(String.Format("OnMessage Message:{0} Param1:{1} Param2:{2}",
message, param1, param2), "MidiOutPort");
}
private static NativeMethods.MidiOutCallback _callback = OnCallback;
private static void OnCallback(IntPtr midiInHandle, int message, IntPtr userData, int param1, int param2)
{
try
{
GCHandle handle = GCHandle.FromIntPtr(userData);
if (handle.Target != null)
{
((MidiOutPort)handle.Target).OnMessage(message, param1, param2);
}
}
catch (Exception e)
{
// TODO: handle exception
Console.WriteLine(e);
}
}
}
We see the static _callback member being initialized with a delegate to the static OnCallback method. This method will receive all callbacks for all open midi (out) ports. It uses the userData to retrieve a reference to our GCHandle we created in the constructor of our class. Then it examines the Target property that should contain the reference to our MidiOutPort class unless it has been collected. If its not collected it calls the OnMessage (instance) method with the message data (message, param1 and param2). Now we're back in our MidiOutPort instance and we can do the message processing.
Notice that the MidiOutPort class does not implement a finalizer! It does implement IDisposable (not shown in code) but relies on the MidiSafeHandle to clean up the unmanaged resources.
To try our code simply instantiate the MidiOutPort class and call Open and Close. Build this into a console application so you can see the Console.Write calls.
MidiOutPort midiPort = new MidiOutPort();
midiPort.Open(0);
midiPort.Close();
Now comment out the call to the Close method. You will see that upon exiting the process, the MidiSafeHandle does its job and closes the handle, which causes a callback, which might or might not reach the original MidiOutPort instance (depending if the instance is already garbage collected).
This solutions is the most stable solution to this problem I managed to come up with so far. The solution is applicable to any P/Invoke interop using callbacks, I just used the Midi Out Port as a concrete example.
If you have any thoughts, suggestions or problems trying it out for yourself, please leave a comment.
1 comments:
Thank you Marc for this valuable contribution.
It would be great to have your introduction opened for a bigger audience.
What about formulating it in Ruby, which also allows delegation and access to the WinApi?
Fridemar.com
Post a Comment