Custom Marshaling – Part 2
In Part 1 I posted a sort of “set up” for today’s post. I presented a data structure (WAVEFORMATEX) that doesn’t fit into the “automatic” marshaling paradigm due to it’s dynamic size. The solution is to create a custom marshaling class that implements ICustomMarshaler and perform the marshaling from managed to native (and vice versa) yourself. There is a bit of good news, in this particular instance. Last time I showed the example of the ADPCMWAVEFORMAT struct which extends WAVEFORMATEX, but the good news is that any extension to the WAVEFORMATEX struct can simply be represented in memory by the WAVEFORMATEX struct plus cbSize number of bytes of data. This typically applies when reading the struct out of a file (and you don’t want to test the codec Id against every possibility), since when writing to a file you will typically know what format you’re writing and can deal with it in whatever way you see fit.
The first thing to know about creating a custom marshaler is that your class must declare a static method called GetInstance that returns an instance of the class. I find it officially “not-cool” that the only way to know you need to do this is by reading the docs (maybe that tells you more about me than the API), but it basically implements the singleton pattern allowing you full control over the number of objects created. I’m not clear yet what the cookie parameter specifies–so far it’s always been an empty string in my testing. At any rate, here is a stubbed out class that implements ICustomMarshaler, and one at a time I’ll go through the required methods:
public sealed class WAVEFORMATEXMarshaler : ICustomMarshaler { private static WAVEFORMATEXMarshaler marshaler = null; public static ICustomMarshaler GetInstance(string cookie) { if (marshaler == null) { marshaler = new WAVEFORMATEXMarshaler(); } return marshaler; } #region ICustomMarshaler Members public object MarshalNativeToManaged(System.IntPtr pNativeData) { } public System.IntPtr MarshalManagedToNative(object ManagedObj) { } public void CleanUpManagedData(object ManagedObj) { } public int GetNativeDataSize() { } public void CleanUpNativeData(System.IntPtr pNativeData) { } #endregion }
I’ll start with the easy ones. Note that in the code below, I have a WAVEFORMATEX_tag struct that is wrapped by a WAVEFORMATEX class that provides friendly properties and helper methods, etc.
In my case, an implementation for CleanUpManagedData is not required since there isn’t any managed data that requires cleaning up. GetNativeDataSize() isn’t actually called by the runtime (that I can determine), but just in case it is, I return the size of a non-extended (PCM) WAVEFORMATEX structure. GetNativeDataSize(WAVEFORMATEX fmt) is a helper method that I added to calculate the amount of native memory required to allocate. Finally, CleanUpNativeData needs to free any memory allocated during marshaling.
public void CleanUpManagedData(object ManagedObj) { // No action required } public int GetNativeDataSize(WAVEFORMATEX fmt) { int size = Marshal.SizeOf(typeof(WAVEFORMATEX_tag)); // Calculate the unmanaged size of the struct size += fmt.Size; // Add the size of the extra data return size; } public int GetNativeDataSize() { // Assume a standard WAVEFORMATEX (no extra bytes) return Marshal.SizeOf(typeof(WAVEFORMATEX_tag)); // Calculate the unmanaged size of the struct } public void CleanUpNativeData(System.IntPtr pNativeData) { Marshal.FreeHGlobal(pNativeData); }
Getting to the real meat of custom marshaling, we have the two methods which do the actual work: MarshalNativeToManaged and MarshalManagedToNative. If you have trouble figuring out what each one is intended to do, uh, you might want to reconsider a few things. Anyway (ahem), MarshalNativeToManaged supplies you with an IntPtr which points to the native data structure. Whenever we’re marshaling, we always want to take advantage of any built-in helper methods we can (in this case, Marshal.PtrToStructure). After pulling out the WAVEFORMATEX structure, we need to pull out any extra data, such as the coefficient information in ADPCMWAVEFORMAT. The implementation of this method isn’t rocket science, so I’ll leave it to you to interpret it. MarshalManagedToNative involves a little more complexity-only because we have to allocate memory on the native heap before we can write to it. Once we’ve allocated the memory, we can use the helper methods to marshal the struct, then manually add the additional data at the end. Again, this ain’t rocket science.
public object MarshalNativeToManaged(System.IntPtr pNativeData) { WAVEFORMATEX wfx = new WAVEFORMATEX(); wfx.waveFormat = (WAVEFORMATEX_tag)Marshal.PtrToStructure(pNativeData, typeof(WAVEFORMATEX_tag)); // If there is extra data, marshal it if (wfx.Size > 0) { // Move pointer forward pNativeData = new IntPtr(pNativeData.ToInt32() + Marshal.SizeOf(typeof(WAVEFORMATEX_tag))); // Read extra data wfx.data = new byte[wfx.Size]; Marshal.Copy(pNativeData, wfx.data, 0, wfx.Size); } return wfx; } public System.IntPtr MarshalManagedToNative(object ManagedObj) { WAVEFORMATEX wfx = null; if (!(ManagedObj is WAVEFORMATEX)) { throw new ArgumentException("Specified object is not a WAVEFORMATEX object.", "ManagedObj"); } else { wfx = (WAVEFORMATEX)ManagedObj; } IntPtr ptr = Marshal.AllocHGlobal(this.GetNativeDataSize(wfx)); if (ptr == IntPtr.Zero) { throw new Exception("Unable to allocate memory to marshal ADPCMWAVEFORMAT."); } // Write out WAVEFORMATEX structure Marshal.StructureToPtr(wfx.waveFormat, ptr, false); // Write extra data (move the ptr "up") if (wfx.Size > 0) { IntPtr dataPtr = new IntPtr(ptr.ToInt32() + Marshal.SizeOf(typeof(WAVEFORMATEX_tag))); Marshal.Copy(wfx.data, 0, dataPtr, Math.Min(wfx.Size, wfx.data.Length)); } return ptr; }
Finally, we come to the method prototype which uses the MarshalAs attribute to associate our custom marshaling class with specific method parameters (if we don’t do this, then why the heck did we write a custom marshaler??). First we specify that we are using a custom marshaler (UnmanagedType.CustomMarshaler), and then we provide the class name of our marshaling class. Easy, no?
[DllImport("Msacm32.dll")] public static extern uint acmStreamOpen(ref IntPtr phas, int had, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef=typeof(WAVEFORMATEXMarshaler))] WAVEFORMATEX pwfxSrc, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef=typeof(WAVEFORMATEXMarshaler))] WAVEFORMATEX pwfxDst, IntPtr pwfltr, IntPtr dwCallback, IntPtr dwInstance, uint fdwOpen);
That’s all she wrote, folks! It’s really not that difficult at all, but it took a while to find the information to guide me on this. That’s why I’m posting this here-hopefully it can help someone else!
As always, if anybody has any comments, please leave them-I love feedback!