How to create and use a Custom COM Marshaler in .NET
Microsoft has done a nice job when it designed Component Object Model, also known as COM, even if the system’s complexity was painful for some developers. To fix that, they first created Vb6 (well…no comment) to soften the burden of consuming COM objects, while C++ was still a little bit rough to use (but anything concerning COM was possible !). After a few years, they finally created the .NET framework. As a fair part of the Windows architecture was based on COM and COM+, they had to make the most popular language (C# and VB.NET) of the platform compatible with it. I’m currently working on a system where the .NET code use a lot of COM to communicate with other parts of the system, and I can tell you that even most of the features of COM can be used, sometimes you have to dig deeper to make .NET component compatible with other languages. I saw a few custom marshaler described on the net, but no one done to marshal an array of “anything” (called VARIANT in COM), so I thought it could be interesting to share it with others.
What is a COM marshaler ?
As COM was designed to communicate between codes coming from different language, something has to do the job of converting the bits of every parameter of every function from one language standard to the other. By instance, if a C++ code, where a string is a null-terminated array of char, wants to call a Vb6 function, where a string is a BSTR (also called Pascal string, with the number of char written in the first four bytes), there must be an allocation and an intelligent copy to create one from the other. This is basically what a COM marshaler do, and the default one provided by .NET is enough for most of the usage.
Well, if the default marshaler is ok, why do I have to create another one by myself?
Sometime you want very special processing of your functions arguments. It happened to me with a function passing an array of VARIANT, used to store the arguments of a stored procedure. The core of the issue is that the default marshaler will translate an array of VARIANT into an array of object, and you have more type information with a VARIANT. Why? Because in a VARIANT, you can make the difference between a null string and a null object (the type and the value is stored in different places), and you cannot do this distinction with an object (if an object is null, you lose all type information). Sadly, I had to process in a different ways the null objects and the null string arguments of my stored procedure, so I was stuck: did the ‘null’ that I received was formerly a COM object or a string?
I also had to change the default date (use the Vb6 default date instead of the .NET default one, because every language in the world use different ‘zero’ dates…).
Now show me this custom marshaler !
Here is the code: I commented it fully, so I think further explanation is unnecessary. Sorry for this looooooooong piece of code, but at least all is here and you just have to copy/paste it into some C# file.
class VariantArrayCustomMarshaller : ICustomMarshaler { // so nobody can create a new instance private VariantArrayCustomMarshaller() { } // unique instance that will be used ; multi-threading use may require proper protection of multiple instanciation static readonly VariantArrayCustomMarshaller _marshaler = new VariantArrayCustomMarshaller(); // a SAFEARRAY bound layout [StructLayout(LayoutKind.Sequential, Pack = 4)] public struct SafeArrayBound { [ComAliasName("Microsoft.VisualStudio.OLE.Interop.ULONG")] public uint cElements; [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")] public int lLbound; } #region ICustomMarshaler members public object MarshalNativeToManaged(IntPtr pNativeData) { if (pNativeData == IntPtr.Zero) return null; // check that dimension number is one (this marshaller does not manage multi-dimensionnal SAFEARRAYs) if (SafeArrayGetDim(pNativeData) != 1) throw new ArgumentException("pNativeData must point to a SAFEARRAY with exactly one dimension", "pNativeData"); // get lower bound long lBound = 0; int hr = SafeArrayGetLBound(pNativeData, 1, ref lBound); if (hr != 0) throw Marshal.GetExceptionForHR(hr); // check that lower bound is 0 (this marshaller does not manage non 0 lower bound) if(lBound != 0) throw new ArgumentException("pNativeData must point to a SAFEARRAY with one dimension starting at 0", "pNativeData"); // get upper bound long uBound = 0; hr = SafeArrayGetUBound(pNativeData, 1, ref uBound); if (hr != 0) throw Marshal.GetExceptionForHR(hr); // get the memory size of a single element uint elementSize = SafeArrayGetElemsize(pNativeData); if (elementSize == 0) throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()); // access (and lock) data IntPtr data = IntPtr.Zero; hr = SafeArrayAccessData(pNativeData, ref data); if (hr != 0) throw Marshal.GetExceptionForHR(hr); try { // use the handy System.Runtime.InteropServices.Marshal class object[] returned = Marshal.GetObjectsForNativeVariants(data, (int)(uBound + 1)); // From now the returned object is good // I add my personnal need : I want to translate NULL strings to empty strings, and NULL dates to default // value of OLE dates for (int i = 0; i <= uBound; ++i) { // get the address of the element IntPtr ptr = new IntPtr(data.ToInt32() + elementSize * i); // read the first 16 bits, as the VAR_TYPE is on two bytes VarEnum ve = (VarEnum)Marshal.ReadInt16(ptr); // process if (ve == VarEnum.VT_BSTR && returned[i] == null) { returned[i] = String.Empty; } else if (ve == VarEnum.VT_DATE && returned[i] == null) { returned[i] = DateTime.FromOADate(0); } } return returned; } finally { // SafeArrayAccessData locks the array SafeArrayUnaccessData(pNativeData); } } // data returned by MarshalManagedToNative, so I will be able to clean it in CleanUpNativeData private IntPtr _safeArrayPtr; public IntPtr MarshalManagedToNative(object managedObj) { if( managedObj == null) return IntPtr.Zero; if (!(managedObj is object[])) throw new ArgumentException("Input must be of object[] type", "managedObj"); object[] realObject = (object[]) managedObj; try { // Create the SAFEARRAY // create my unique dimension SafeArrayBound sab; sab.lLbound = 0; sab.cElements = (uint)realObject.Length; IntPtr psab = IntPtr.Zero; try { // Performances would be better with a stackalloc, but then I would have to declare the method unsafe psab = Marshal.AllocHGlobal(Marshal.SizeOf(sab)); Marshal.StructureToPtr(sab, psab, false); // the real SAFEARRAY creation _safeArrayPtr = SafeArrayCreate((ushort) VarEnum.VT_VARIANT, 1, psab); if (_safeArrayPtr == IntPtr.Zero) throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()); } finally { // SafeArrayCreate create a copy of the SafeArrayBound memory, so it is safe and recommended to free it here if (psab != IntPtr.Zero) Marshal.FreeHGlobal(psab); } IntPtr data = IntPtr.Zero; // I use SafeArrayAccessData instead of SafeArrayPutElement to avoid one allocation for each Variant int hr = SafeArrayAccessData(_safeArrayPtr, ref data); if (hr != 0) throw Marshal.GetExceptionForHR(hr); try { // get the memory size of a single element uint elementSize = SafeArrayGetElemsize(_safeArrayPtr); if(elementSize == 0) throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()); // use the handy System.Runtime.InteropServices.Marshal class for each element for (int i = 0; i < realObject.Length; ++i) { // get the address of the element IntPtr ptr = new IntPtr(data.ToInt32() + elementSize*i); Marshal.GetNativeVariantForObject(realObject[i], ptr); } } finally { // SafeArrayAccessData locks the array SafeArrayUnaccessData(_safeArrayPtr); } return _safeArrayPtr; } catch(Exception) { CleanUpNativeData(_safeArrayPtr); _safeArrayPtr = IntPtr.Zero; throw; } } // Clean the data returned by MarshalManagedToNative public void CleanUpNativeData(IntPtr pNativeData) { if (pNativeData != IntPtr.Zero) { int hr = SafeArrayDestroy(pNativeData); if (hr != 0) throw Marshal.GetExceptionForHR(hr); } } // Managed datas will be cleaned by the garbage collector public void CleanUpManagedData(object managedObj) { } // As my marshaller manage only a reference value public int GetNativeDataSize() { return -1; } // return the static instance public static ICustomMarshaler GetInstance(string cookie) { return _marshaler; } #endregion #region pInvoke stuff. For your personnal project, it is better to put it on a separate class [DllImport("oleaut32.dll", EntryPoint = "SafeArrayCreate", SetLastError = true, CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)] static extern IntPtr SafeArrayCreate(ushort vt, uint cDims, IntPtr rgsabound); [DllImport("oleaut32.dll", EntryPoint = "SafeArrayDestroy", SetLastError = true, CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)] static extern int SafeArrayDestroy(IntPtr psa); [DllImport("oleaut32.dll", EntryPoint = "SafeArrayAccessData", SetLastError = true, CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)] static extern int SafeArrayAccessData(IntPtr psa, ref IntPtr ppvData); [DllImport("oleaut32.dll", EntryPoint = "SafeArrayUnaccessData", SetLastError = true, CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)] static extern int SafeArrayUnaccessData(IntPtr psa); [DllImport("oleaut32.dll", EntryPoint = "SafeArrayGetElemsize", SetLastError = true, CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)] static extern uint SafeArrayGetElemsize(IntPtr psa); [DllImport("oleaut32.dll", EntryPoint = "SafeArrayGetDim", SetLastError = true, CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)] static extern uint SafeArrayGetDim(IntPtr psa); [DllImport("oleaut32.dll", EntryPoint = "SafeArrayGetLBound", SetLastError = true, CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)] static extern int SafeArrayGetLBound(IntPtr psa, uint nDim, ref long bound); [DllImport("oleaut32.dll", EntryPoint = "SafeArrayGetUBound", SetLastError = true, CharSet = CharSet.Auto, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)] static extern int SafeArrayGetUBound(IntPtr psa, uint nDim, ref long bound); #endregion }
Of course you have to tell the system to use this marshaler in the parameter of your specific function, here it is:
[ComVisible(true)] public void MyFunction([In, Out, MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef=typeof(VariantArrayCustomMarshaller))] ref object[] arr) { // process }
Here it is, happy marshaling !
jl7i8u Very true! Makes a change to see someone spell it out like that. :)
LC9ekX BION I’m impressed! Cool post!
hI Julien,
I am very much impressed by this article. I am stuck in the same problem. I have Integrated your Custom Marshaller in my code but still I am unable to get the Variant Array. I am getting an exception at SafeArrayGetDim(), and it is basically Access Voilation ” Attempted to read or write protected memory. This is often an indication that other memory is corrupt.”.Can you please help me in this Issue.
Best Regards
Mustafa Muhammad
Hi Mustafa
The argument you’re passing to the marshaller might not be a COM array. Do you have a snippet of the code that calls the function having the [CustomMarshaller] attribute ? Maybe I can find something.
Hi Julien thanx for the reply
Please see the following link of stack overflow where i have posted my question
http://stackoverflow.com/questions/8877510/marshalling-issue-in-c-sharp
code snippet are in it, If you need the code i can send you the code..
Julien,
Can you please add me on skype “mustafa23831”. I want to solve this issue as soon as possible.
best regards
I Mustafa
I’m currently working now so it’s a little bit difficult for me to start a skype session. I’m gonna try to be very reactive on my blog ok ? Right now I need the code source that call the DoSomething function, could you give it to me ?
I would like to ask this on stackoverflow but sadly I don’t have enough point to ask questions.
Hi Julien,
I thing that i want to tell you that this Custom Interface is according to “OPC Standard” . “OPC” Foundation has defined this interface. All the Clients have to make call on standard interface. Can you please send me your email address where i can send you the C# COM server code. And C++ interface file that I have implemented…
As this is custom Interface any one can make its client in c++ and call this method..
I have a project here that would interest you but drive you crazy at the same time:
http://kinectmultipoint.codplex.com. When I get to queueinputreport on the vb.net code I have tried everything to marshal it but I need to marshal a safe array of variants (content is of VT_UI1 on c++ side) to c++ side. I tried object but it does not pass the c++ FADF_FIXEDSIZED and FADF_STATIC bits to c++ when passing the object variable. If you think you are good at making a variant in c# try to see if you call it. I have lync here so you can ask me questions anytime at school too: jeffery.carlson657@topper.wku.edu. that’s my school account but later I will have one for a job.