none
COM Interop: SafeArrayTypeMismatchException when passing safearray from c++ -> c# RRS feed

  • Question

  • Hi There,

    I am trying to pass an array of (idispatch pointers to managed) objects from a native c++ client to a managed c# component.

    (note: iDisp below is returned successfully from a call to the managed side before the code below. IDisp points to a managed object from class 'A')

    CComSafeArray<VARIANT> sa(2);
    sa[0]=iDisp;
    sa[1]=iDisp;
    
    HRESULT hr = myObject->PassSet(sa.Detach());


    If in my .NET component I use the following signature, everything is fine and I enter the call

    public void PassSet(object[] values)
    {
    
       A myObject = values[0] as A;
    
    //myObject is valid
    
    }



    if however I change the signature to:

    public void PassSet(A[] values)
    {
    
    }
    
    I get a SafeArrayTypeMismatchException on the native side and the managed code is not reached.

    Tried to add some marshalling attributes on the c# side, without success however.

    Can anybody shed some light on this ? Thanks in advance!

    Tuesday, February 15, 2011 3:48 PM

Answers

  • Hello .3,

     

    1. Custom marshaling is one way to accomplish using A[] as the parameter type for the PassSet() method while allowing the client code to continue to use SAFEARRAY.

    1.1 That is, we want the PassSet() method to be defined in C# as :

    public void PassSet(A[] values);

    while at the same time, the C++ client code be written as :

    HRESULT hrRetTemp = myObject -> PassSet((long)pSafeArray);

    * I'll later explain why we must cast "pSafeArray" to a long.

    1.2 The idea is to implement and use a C# class which is derived from ICustomMarshaler.

    1.3 I The details are listed below. Note that the basics of custom marshaling and the ICustomMarshaler interface will not be covered. For more details please refer to MSDN.

     

    2. In order to use a Custom Marshaler for the PassSet() method, the method must be decorated with additional attributes :

    public void PassSet([MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(ObjectToAConverter))] A[] values);

    2.1 Here I have indicated that the A[] "values" parameter is to be marshaled in (from unmanaged code) using an ICustomMarshaler object of type "ObjectToAConverter"

    2.2 Because the A[] "values" parameter is an [In] parameter, the marshaling is only one way (from unmanaged code to managed code). Hence only the ICustomMarshaler::MarshalNativeToManaged() method needs to contain substantial code.

    2.3 The Custom Marshaler class is listed below :

        public class ObjectToAConverter : ICustomMarshaler
        {
            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayLock(IntPtr pSafeArray);

            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayUnlock(IntPtr pSafeArray);

            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayGetDim(IntPtr pSafeArray);

            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayGetLBound(IntPtr pSafeArray, UInt32 nDim, out Int32 plLbound);

            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayGetUBound(IntPtr pSafeArray, UInt32 nDim, out Int32 plUbound);

            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayAccessData(IntPtr pSafeArray, out IntPtr ppvData);

            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayUnaccessData(IntPtr pSafeArray);

            static ObjectToAConverter marshaler;

            public object MarshalNativeToManaged(IntPtr pNativeData)
            {
                if (pNativeData == IntPtr.Zero) return null;

                A[] A_array = null;

                SafeArrayLock(pNativeData);

                try
                {
                    uint uiDim = SafeArrayGetDim(pNativeData);

                    if (uiDim != 1)
                    {
                        // Dimensions must be exactly one.
                        return null;
                    }

                    Int32 plLbound = 0;
                    Int32 plUbound = 0;

                    SafeArrayGetLBound(pNativeData, 1, out plLbound);
                    SafeArrayGetUBound(pNativeData, 1, out plUbound);

                    A_array = new A[plUbound - plLbound + 1];

                    IntPtr pSafeArrayData = IntPtr.Zero;

                    SafeArrayAccessData(pNativeData, out pSafeArrayData);

                    Object[] obj_array = Marshal.GetObjectsForNativeVariants(pSafeArrayData, A_array.Length);

                    for (int i = 0; i < A_array.Length; i++)
                    {
                        A_array[i] = (A)obj_array[i];
                    }

                    SafeArrayUnaccessData(pNativeData);
                    pSafeArrayData = IntPtr.Zero;
                }
                finally
                {
                    SafeArrayUnlock(pNativeData);
                }

                return A_array;
            }

            public IntPtr MarshalManagedToNative(object ManagedObj)
            {
                // Nothing to do
                return IntPtr.Zero;
            }

            public void CleanUpNativeData(IntPtr pNativeData)
            {
                // Nothing to do
            }

            public void CleanUpManagedData(object ManagedObj)
            {
                // Nothing to do
            }

            public int GetNativeDataSize()
            {
                return -1;
            }

            public static ICustomMarshaler GetInstance(string cookie)
            {
                // Always return the same instance
                if (marshaler == null)
                {
                    marshaler = new ObjectToAConverter();
                }

                return marshaler;
            }
        }

    2.4 As mentioned, only the MarshalNativeToManaged() method needs to be non-trivial. This method is called by the CLR immediately after the unmanaged C++ code has called the PassSet() method but before the C# managed code for PassSet() is actually invoked. This is where we have a chance to take whatever data the unmanaged code has given and transform it to an array of "A" objects.

    2.5 The parameter for the MarshalNativeToManaged() method is an IntPtr. It points to the actual SAFEARRAY object passed from C++. We then use the usual SAFEARRAY APIs available from Oleaut32.dll to perform locking of the SAFEARRAY, getting dimension, lower and upper bound information. These information helps us to prepare an array of "A" objects (i.e. "A_array") of the same array length as that contained in the SAFEARRAY.

    2.6 We then use the SafeArrayAccessData() API to obtain a pointer to the actual array of VARIANT objects which is contained inside the SAFEARRAY. The returned pointer is stored inside an IntPtr named "pSafeArrayData".

    2.7 We then use the Marshal.GetObjectsForNativeVariants() method together with "pSafeArrayData" to transform the array of VARIANT objects into an array of equivalent C# Objects. This is the most crucial point of the MarshalNativeToManaged() method.

    2.8 After that comes a "for" loop in which each element of the object array is cast into a corresponding "A" object of the "A_array". When this loop is done, we are ready to hand over "A_array" to the PassSet() method. But before that, SafeArrayUnaccessData() and SafeArrayUnlock() are called as part of cleanup.

    2.9 When the C# PassSet() method is invoked, the "values" array is an actual array of "A" objects and so we may do the following :

    A myObject = values[0];

     

    3. Some final explanations and implementation advise.

    3.1 Concerning the need for the C++ unmanaged code to have to cast the SAFEARRAY to a long as it calls the PassSet() method :

    HRESULT hrRetTemp = myObject -> PassSet((long)pSafeArray);

    For this example the COM interface is generated from C# code which has been given COM attributes :

        [ComVisible(true)]
        [ProgId("CSImpl02.A")]
        [ClassInterface(ClassInterfaceType.AutoDual)]
        public class A
        {
               ...
               ...
               ...
        }

    I have also defined a class named "B" in which the PassSet() method is defined :

        [ComVisible(true)]
        [ProgId("CSImpl02.B")]
        [ClassInterface(ClassInterfaceType.AutoDual)]
        public class B
        {
               ...
               ...
               ...
               public void PassSet([MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(ObjectToAConverter))] A[] values)
               {
                            ...
                            ...
                            ...
               }
        }

    The COM method signature for PassSet() is generated by Regasm.exe :

    HRESULT PassSet([in] long values);

    Instead of "SAFEARRAY(VARIANT)", the "values" parameter is set as of type "long". This is likely because of the use of Custom Marshling. This will have negative impact if COM apartments and COM interface marshaling (different from the .NET custom marshaling that we've been discussing) come into play. For this example, as long as the unmanaged code and the C# objects all live in the same apartment, there will be no problem.

    For long term implementation which includes deployment, where multiple threads and COM apartments are involved, we need to look into maintaining the use of the SAFEARRAY type for the COM method signature of the PassSet() method. My advise is that we defined an independent COM interface (in which the PassSet() method is defined) and then run Tlbimp.exe on it to produce an interop assembly. The parameter for the PassSet() method will be explicitly set to a SAFEARRAY (instead of long). The C# "B" class then inherits from this COM interface together with the Custom Marshaling attributes.

    I do anticipate the use of ildasm.exe and ilasm.exe to disassemble the interop assembly, do some necessary adjustments to it and then re-assemble it. Give me some time to do research on this.

     

    3.2 The next thing is that you may have noticed that in the unmanaged C++ code, I have used a pointer to a SAFEARRAY "pSafeArray" as the parameter for PassSet() :

    HRESULT hrRetTemp = myObject -> PassSet((long)pSafeArray);

    The full code is actually :

       CComSafeArray<VARIANT> sa(2);
       sa[0]=iDisp;
       sa[1]=iDisp;
         
       SAFEARRAY* pSafeArray = sa.Detach();
      
       HRESULT hrRetTemp = myObject -> PassSet((long)(pSafeArray));
      
       ::SafeArrayDestroy(pSafeArray);
       pSafeArray = NULL;

    It is better to hold the returned LPSAFEARRAY (from the Detach() method) in a SAFEARRAY pointer. This is so that the original SAFEARRAY contained inside "sa" may be destroyed later using ::SafeArrayDestroy().

    If the call was simply :

    HRESULT hr = myObject->PassSet(sa.Detach());

    then the SAFEARRAY inside "sa" would be left dangling (after the call to PassSet()) without any way to access and destroy it. We thus have a memory leak for each IDispatch pointer contained inside "sa".

     

    I hope you will find the above custom marshaling technique useful. Best of luck.

    - Bio.

     

    • Marked as answer by .3 Thursday, February 17, 2011 10:10 AM
    Wednesday, February 16, 2011 6:41 PM

All replies

  • I think what you want is automatic QueryInterface() here.  I am guessing that is the problem: You can't get away with automatic QI.  What is the data type of iDisp?  I am thinking it is IDispatch because of its name.  Besides you can only pass IDispatch or IUnknown to VARIANT.  So you want to mask a SafeArray of VARIANTs containing IDispatch pointers and access them like objects of type A.  Not possible without calling QI() on each pointer.  This is what I believe is the problem.
    MCP
    Tuesday, February 15, 2011 8:44 PM
  • Thanks for your reply. Indeed the type if iDisp is IDispatch. So you're saying this is not possible and can only be accomplished by casting each member of the object[] on the C# side ?

    Tuesday, February 15, 2011 10:21 PM
  • Hi.  I would say so, yes.  I don't do much of COM interop with .Net, but I think it would appear so.

    Strictly speaking and if I remember correctly, a SAFEARRAY can be of type A (or its public interface, that is), as long as you provide the marshaling code.  Try it out and see if it works.  If you cannot make it work, then use object[].

    OR:  Find a way of getting an automatic QueryInterface, but it appears that you have looked already.

    Summarizing:  It is logical to me that you get the mismatch exception because your have IDispatch pointers and not pointers to IA (or whatever public interface the A class uses).  Stick with object[] as it is compatible with your IDispatch pointers.


    MCP
    Wednesday, February 16, 2011 2:03 AM
  • Hello .3,

     

    1. The original signature of the PassSet() method :

    public void PassSet(object[] values);

    produces the following equivalent COM method :

    HRESULT PassSet([in] SAFEARRAY(VARIANT) values);

    i.e., the type of each element of the "values" SAFEARRAY is expected to be of VT_VARIANT type.

     

    2. By changing the signature of the PassSet() method to :

    public void PassSet(A[] values);

    The equivalent COM method signature becomes :

    HRESULT PassSet([in] SAFEARRAY(A*) values);

    i.e., the type of each element of the "values" SAFEARRAY is expected to be a pointer to the "A" interface.

    Two problems can arise out of this :

    2.1. A SAFEARRAY can only hold VT_* types (i.e. types which can be stored inside a VARIANT struct). And we know that there is no VT_* type which directly corresponds with "A". This may not have seemed a problem in C++ code because the "values" parameter for the PassSet() method is expected to be just a pointer to a SAFEARRAY. Hence you may still create a SAFEARRAY of VARIANTs each containing an IDispatch pointer.

    2.2 A more serious problem arises when control reaches the CLR Interop Marshaler. At this time, the input SAFEARRAY parameter will be queried for the type (VARTYPE) of its contained elements. The VT_DISPATCH is discovered. This is different from the "A" interface type which is expected by the Interop Marshaler. Hence the SafeArrayTypeMismatchException.

     

    3. Let me illustrate with an example which will produce the same results.

    3.1 Let's say you had defined a C# method as follows :

    public UInt32 PassArrayOfUInt32(UInt32[] values);

    Then the equivalent COM method is :

    unsigned long PassArrayOfUInt32([in] SAFEARRAY(unsigned long) values);

    3.2 Then in your C++ code, if you had coded as follows :

    CComSafeArray<unsigned long, VT_UI4> sa_UI4(2);
    unsigned long ulResult = 0;

    sa_UI4[0] = 1;
    sa_UI4[1] = 2;

    HRESULT hr = myObject -> PassArrayOfUInt32(sa_UI4.Detach(), &ulResult);

    Things will go fine.

    3.3 But if you had changed the template parameter VT_UI4 to VT_UI2, you will get the SafeArrayTypeMismatchException exception.

     

    4. One powerful way to accomplish this (i.e. maintaining A[] as the parameter type for PassSet() and to allow the C++ client code to use SAFEARRAY) would be to use Custom Marshaling. I'll cover this in another post because it will take up space.

     

    - Bio.

     

    Wednesday, February 16, 2011 8:58 AM
  • Thanks for your extensive explanation, makes sense. Bio\WebJose, thanks for your time.

     

    Wednesday, February 16, 2011 10:15 AM
  • Hello .3,

     

    1. Custom marshaling is one way to accomplish using A[] as the parameter type for the PassSet() method while allowing the client code to continue to use SAFEARRAY.

    1.1 That is, we want the PassSet() method to be defined in C# as :

    public void PassSet(A[] values);

    while at the same time, the C++ client code be written as :

    HRESULT hrRetTemp = myObject -> PassSet((long)pSafeArray);

    * I'll later explain why we must cast "pSafeArray" to a long.

    1.2 The idea is to implement and use a C# class which is derived from ICustomMarshaler.

    1.3 I The details are listed below. Note that the basics of custom marshaling and the ICustomMarshaler interface will not be covered. For more details please refer to MSDN.

     

    2. In order to use a Custom Marshaler for the PassSet() method, the method must be decorated with additional attributes :

    public void PassSet([MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(ObjectToAConverter))] A[] values);

    2.1 Here I have indicated that the A[] "values" parameter is to be marshaled in (from unmanaged code) using an ICustomMarshaler object of type "ObjectToAConverter"

    2.2 Because the A[] "values" parameter is an [In] parameter, the marshaling is only one way (from unmanaged code to managed code). Hence only the ICustomMarshaler::MarshalNativeToManaged() method needs to contain substantial code.

    2.3 The Custom Marshaler class is listed below :

        public class ObjectToAConverter : ICustomMarshaler
        {
            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayLock(IntPtr pSafeArray);

            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayUnlock(IntPtr pSafeArray);

            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayGetDim(IntPtr pSafeArray);

            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayGetLBound(IntPtr pSafeArray, UInt32 nDim, out Int32 plLbound);

            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayGetUBound(IntPtr pSafeArray, UInt32 nDim, out Int32 plUbound);

            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayAccessData(IntPtr pSafeArray, out IntPtr ppvData);

            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayUnaccessData(IntPtr pSafeArray);

            static ObjectToAConverter marshaler;

            public object MarshalNativeToManaged(IntPtr pNativeData)
            {
                if (pNativeData == IntPtr.Zero) return null;

                A[] A_array = null;

                SafeArrayLock(pNativeData);

                try
                {
                    uint uiDim = SafeArrayGetDim(pNativeData);

                    if (uiDim != 1)
                    {
                        // Dimensions must be exactly one.
                        return null;
                    }

                    Int32 plLbound = 0;
                    Int32 plUbound = 0;

                    SafeArrayGetLBound(pNativeData, 1, out plLbound);
                    SafeArrayGetUBound(pNativeData, 1, out plUbound);

                    A_array = new A[plUbound - plLbound + 1];

                    IntPtr pSafeArrayData = IntPtr.Zero;

                    SafeArrayAccessData(pNativeData, out pSafeArrayData);

                    Object[] obj_array = Marshal.GetObjectsForNativeVariants(pSafeArrayData, A_array.Length);

                    for (int i = 0; i < A_array.Length; i++)
                    {
                        A_array[i] = (A)obj_array[i];
                    }

                    SafeArrayUnaccessData(pNativeData);
                    pSafeArrayData = IntPtr.Zero;
                }
                finally
                {
                    SafeArrayUnlock(pNativeData);
                }

                return A_array;
            }

            public IntPtr MarshalManagedToNative(object ManagedObj)
            {
                // Nothing to do
                return IntPtr.Zero;
            }

            public void CleanUpNativeData(IntPtr pNativeData)
            {
                // Nothing to do
            }

            public void CleanUpManagedData(object ManagedObj)
            {
                // Nothing to do
            }

            public int GetNativeDataSize()
            {
                return -1;
            }

            public static ICustomMarshaler GetInstance(string cookie)
            {
                // Always return the same instance
                if (marshaler == null)
                {
                    marshaler = new ObjectToAConverter();
                }

                return marshaler;
            }
        }

    2.4 As mentioned, only the MarshalNativeToManaged() method needs to be non-trivial. This method is called by the CLR immediately after the unmanaged C++ code has called the PassSet() method but before the C# managed code for PassSet() is actually invoked. This is where we have a chance to take whatever data the unmanaged code has given and transform it to an array of "A" objects.

    2.5 The parameter for the MarshalNativeToManaged() method is an IntPtr. It points to the actual SAFEARRAY object passed from C++. We then use the usual SAFEARRAY APIs available from Oleaut32.dll to perform locking of the SAFEARRAY, getting dimension, lower and upper bound information. These information helps us to prepare an array of "A" objects (i.e. "A_array") of the same array length as that contained in the SAFEARRAY.

    2.6 We then use the SafeArrayAccessData() API to obtain a pointer to the actual array of VARIANT objects which is contained inside the SAFEARRAY. The returned pointer is stored inside an IntPtr named "pSafeArrayData".

    2.7 We then use the Marshal.GetObjectsForNativeVariants() method together with "pSafeArrayData" to transform the array of VARIANT objects into an array of equivalent C# Objects. This is the most crucial point of the MarshalNativeToManaged() method.

    2.8 After that comes a "for" loop in which each element of the object array is cast into a corresponding "A" object of the "A_array". When this loop is done, we are ready to hand over "A_array" to the PassSet() method. But before that, SafeArrayUnaccessData() and SafeArrayUnlock() are called as part of cleanup.

    2.9 When the C# PassSet() method is invoked, the "values" array is an actual array of "A" objects and so we may do the following :

    A myObject = values[0];

     

    3. Some final explanations and implementation advise.

    3.1 Concerning the need for the C++ unmanaged code to have to cast the SAFEARRAY to a long as it calls the PassSet() method :

    HRESULT hrRetTemp = myObject -> PassSet((long)pSafeArray);

    For this example the COM interface is generated from C# code which has been given COM attributes :

        [ComVisible(true)]
        [ProgId("CSImpl02.A")]
        [ClassInterface(ClassInterfaceType.AutoDual)]
        public class A
        {
               ...
               ...
               ...
        }

    I have also defined a class named "B" in which the PassSet() method is defined :

        [ComVisible(true)]
        [ProgId("CSImpl02.B")]
        [ClassInterface(ClassInterfaceType.AutoDual)]
        public class B
        {
               ...
               ...
               ...
               public void PassSet([MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(ObjectToAConverter))] A[] values)
               {
                            ...
                            ...
                            ...
               }
        }

    The COM method signature for PassSet() is generated by Regasm.exe :

    HRESULT PassSet([in] long values);

    Instead of "SAFEARRAY(VARIANT)", the "values" parameter is set as of type "long". This is likely because of the use of Custom Marshling. This will have negative impact if COM apartments and COM interface marshaling (different from the .NET custom marshaling that we've been discussing) come into play. For this example, as long as the unmanaged code and the C# objects all live in the same apartment, there will be no problem.

    For long term implementation which includes deployment, where multiple threads and COM apartments are involved, we need to look into maintaining the use of the SAFEARRAY type for the COM method signature of the PassSet() method. My advise is that we defined an independent COM interface (in which the PassSet() method is defined) and then run Tlbimp.exe on it to produce an interop assembly. The parameter for the PassSet() method will be explicitly set to a SAFEARRAY (instead of long). The C# "B" class then inherits from this COM interface together with the Custom Marshaling attributes.

    I do anticipate the use of ildasm.exe and ilasm.exe to disassemble the interop assembly, do some necessary adjustments to it and then re-assemble it. Give me some time to do research on this.

     

    3.2 The next thing is that you may have noticed that in the unmanaged C++ code, I have used a pointer to a SAFEARRAY "pSafeArray" as the parameter for PassSet() :

    HRESULT hrRetTemp = myObject -> PassSet((long)pSafeArray);

    The full code is actually :

       CComSafeArray<VARIANT> sa(2);
       sa[0]=iDisp;
       sa[1]=iDisp;
         
       SAFEARRAY* pSafeArray = sa.Detach();
      
       HRESULT hrRetTemp = myObject -> PassSet((long)(pSafeArray));
      
       ::SafeArrayDestroy(pSafeArray);
       pSafeArray = NULL;

    It is better to hold the returned LPSAFEARRAY (from the Detach() method) in a SAFEARRAY pointer. This is so that the original SAFEARRAY contained inside "sa" may be destroyed later using ::SafeArrayDestroy().

    If the call was simply :

    HRESULT hr = myObject->PassSet(sa.Detach());

    then the SAFEARRAY inside "sa" would be left dangling (after the call to PassSet()) without any way to access and destroy it. We thus have a memory leak for each IDispatch pointer contained inside "sa".

     

    I hope you will find the above custom marshaling technique useful. Best of luck.

    - Bio.

     

    • Marked as answer by .3 Thursday, February 17, 2011 10:10 AM
    Wednesday, February 16, 2011 6:41 PM