//
you're reading...
.NET Interop, .NET/COM/ActiveX Interop, COM Events, SAFEARRAYs

Passing a Reference to a SAFEARRAY as Parameter to a Managed COM Event Handler Part 3.

1. Introduction.

1.1 This blog is the third segment of the series of articles that discusses the problems and workarounds in relation to passing a reference to a SAFEARRAY as parameter to a dispinterface-based event which is fired from an unmanaged COM server to a managed client.

1.2 In part 2, I presented a workaround which is based on the use of COM’s Connection Point Protocol.

1.3 The workaround in part 2 works well but requires a compromise : the original reference SAFEARRAY parameter has to be wrapped inside a VARIANT that is passed by reference.

1.4 The workaround discussed in this part 3 involves the use of the ICustomQueryInterface interface to enable a custom implementation of the IDispatch interface in C#.

1.5 This works very well and allows for very refined control over the event handling process in managed code.

1.6 In my opinion, this workaround is by far probably the best solution given the fact that it affects only the client code and the original event dispinterface and implementation of the COM server need not be touched.

2. The Workaround and the ICustomQueryInterface Interface.

2.1 In this section I want to provide some background to the latest workaround solution.

2.2 The ICustomQueryInterface interface was first introduced in CLR v4.

2.3 It allows managed code developers to come close to implementing a QueryInterface() method for a managed object exposed to COM.

2.4 By implementing ICustomQueryInterface, a managed class will be able to return virtually any interface implementation to a client which is presumably calling QueryInterface().

2.5 Our solution uses ICustomQueryInterface to return an actual IDispatch interface implementation for the _ITestCOMClassEvents event.

2.6 This IDispatch implementation is coded in C# with the use of various native Windows APIs.

2.7 Some interesting articles on ICustomQueryInterface :

3. New Methods for _ITestCOMClassEvents and ITestCOMClass.

3.1 I have added 2 new methods to _ITestCOMClassEvents in addition to the ones defined earlier :

[
    uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
]
dispinterface _ITestCOMClassEvents
{
properties:
methods:
    [id(1)] HRESULT Event01([in, out] SAFEARRAY(int)* intArray);
    [id(2)] HRESULT Event02([in, out] VARIANT* pvarArray);
    [id(3)] HRESULT Event03([in, out] VARIANT_BOOL* pBool);
    [id(4)] HRESULT Event04([in, out] BSTR* pStr);
};

The two new methods are Event03() and Event04() which each passes a reference VARIANT_BOOL and a reference BSTR parameter respectively.

3.2 I have added these new methods to show the extent of flexibility that one can achieve through customized handling of events in managed code.

3.3 For this part 3, I will concentrate on the use of ICustomQueryInterface on Event01(). I will expound its use in Event02() through Event04() in later parts.

3.4 The following is a new listing for the ITestCOMClass interface :

[
    object,
    uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx),
    dual,
    nonextensible,
    pointer_default(unique)
]
interface ITestCOMClass : IDispatch {
    [id(1)] HRESULT TestMethod01();
    [id(2)] HRESULT TestMethod02();
    [id(3)] HRESULT TestMethod03();
    [id(4)] HRESULT TestMethod04();
};

Each of the new methods are for firing the associated new events.

4. General Approach of Workaround.

4.1 As mentioned earlier, just as it was in part 2, the workarounds are based on the use of COM’s native IConnectionPoint and IConnectionPointContainer interfaces to do the event hookup.

4.2 To this end, we define a class, TestCOMClassEventsListener2, that will perform the Connection Point Protocol hookup and event handling.

4.3 In addition to this, we also want the TestCOMClassEventsListener2 class to implement its own IDispatch interface.

4.4 This is accomplished by making the class implement the ICustomQueryInterface interface.

4.5 A full listing of the class is given below :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TestCOMServerLib;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

namespace ConsoleClient02
{
    // The following definition of IDispatch is based on the code provided in
    // "CustomQueryInterface - IDispatch and Aggregation" (http://clrinterop.codeplex.com/releases/view/32350)
    /// <summary>
    /// This interface is the managed version of the native com IDispatch, it has the same vtable layout
    /// as well as the GUID of the native IDispatch interface.
    /// </summary>
    [ComImport]
    [Guid("00020400-0000-0000-C000-000000000046")]
    [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]    
    public interface IDispatch
    {        
        void GetTypeInfoCount(out uint pctinfo);

        void GetTypeInfo(uint iTInfo, int lcid, out IntPtr info);

        [PreserveSig]
        UInt32 GetIDsOfNames
        (
            ref Guid iid,
            [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr, SizeParamIndex = 2)]
            string[] names,
            int cNames,
            int lcid,
            [Out][MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.I4, SizeParamIndex = 2)]
            int[] rgDispId
        );

        void Invoke(
            int dispId,
            ref Guid riid,
            int lcid,
            ushort wFlags,
            ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams,
            out object result,
            IntPtr pExcepInfo,
            IntPtr puArgErr);
    }

    class TestCOMClassEventsListener2 : IDispatch, ICustomQueryInterface
    {
        public const UInt32 DISP_E_NONAMEDARGS = 0x80020007;

        public TestCOMClassEventsListener2(TestCOMClass testCOMObj)
        {            
            IConnectionPointContainer icpc = (IConnectionPointContainer)testCOMObj;

            // Find the connection point for the
            // _ITestCOMClassEvents source interface.
            Guid guid = typeof(_ITestCOMClassEvents).GUID;
            icpc.FindConnectionPoint(ref guid, out icp);

            // Pass a pointer to the host to the connection point.
            icp.Advise(this, out iCookie);
        }

        ~TestCOMClassEventsListener2()
        {
            if (iCookie != -1)
            {
                icp.Unadvise(iCookie);
                iCookie = -1;
            }
        }

        #region IDispatch implementation
        public void GetTypeInfoCount(out uint pctinfo)
        {
            //NOT IMPLEMENTED
            pctinfo = 0;
        }

        public void GetTypeInfo(uint iTInfo, int lcid, out IntPtr info)
        {
            //NOT IMPLEMENTED
            info = IntPtr.Zero;
        }

        [PreserveSig]
        public UInt32 GetIDsOfNames
        (
            ref Guid iid,
            [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr, SizeParamIndex = 2)]
            string[] names,
            int cNames,
            int lcid,
            [Out][MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.I4, SizeParamIndex = 2)]
            int[] rgDispId
        )
        {
            UInt32 uiRet = 0;

            // ignore lcid, let's assume it is enu
            // Add strict check for parameters Here
            for (int i = 0; i < cNames; i++)
            {
                string name = names[i];

                switch(names[i])
                {
                    case "Event01" :
                        {
                            rgDispId[i] = 1;
                            break;
                        }

                    case "Event02" :
                        {
                            rgDispId[i] = 2;
                            break;
                        }

                    case "Event03" :
                        {
                            rgDispId[i] = 3;
                            break;
                        }

                    case "Event04" :
                        {
                            rgDispId[i] = 4;
                            break;
                        }

                    default:
                        {
                            rgDispId[i] = -1;
                            uiRet = DISP_E_NONAMEDARGS;
                            break;
                        }
                }
            }

            return uiRet;
        }

        public void Invoke
        (
            int dispId,
            ref Guid riid,
            int lcid,
            ushort wFlags,
            ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams,
            out object result,
            IntPtr pExcepInfo,
            IntPtr puArgErr
        )
        {
            result = null;

            switch (dispId)
            {
                case 1:
                    {
                        // Comment out the unwanted event handler.
                        // result = HandleEvent01_v1(ref pDispParams);
                        result = HandleEvent01_v2(ref pDispParams);
                        break;
                    }

                case 2:
                    {
                        result = HandleEvent02(ref pDispParams);
                        break;
                    }

                case 3:
                    {
                        result = HandleEvent03(ref pDispParams);
                        break;
                    }

                case 4 :
                    {
                        result = HandleEvent04(ref pDispParams);
                        break;
                    }
            }

            return;
        }

        #endregion IDispatch implementation

        #region ICustomQueryInterface implementation
        public CustomQueryInterfaceResult GetInterface(ref Guid iid, out IntPtr ppv)
        {
            ppv = IntPtr.Zero;

            if (iid == typeof(_ITestCOMClassEvents).GUID)
            {
                // CustomQueryInterfaceMode.Ignore is used below to notify CLR to bypass the invocation of
                // ICustomQueryInterface.GetInterface to avoid infinite loop during QI.
                //
                // We return a pointer to this object's IDispatch interface to the caller.
                ppv = Marshal.GetComInterfaceForObject(this, typeof(IDispatch), CustomQueryInterfaceMode.Ignore);
                return CustomQueryInterfaceResult.Handled;
            }

            // Let CLR handle the rest of the QI
            return CustomQueryInterfaceResult.NotHandled;
        }
        #endregion

        // This event handler is not suitable for the problem at hand. 
        // It uses Marshal class methods to access the SAFEARRAY
        // from the VARIANT in the DISPPARAMS and store them in a managed array.
        // After manipulating the managed array, we attempt to store the values
        // of the managed array back into the SAFEARRAY in the VARIANT using
        // Marshal.GetNativeVariantForObject().
        //
        // However, this will not work because Marshal.GetNativeVariantForObject()
        // may create a new SAFEARRAY and store it in the VARIANT of type (VT_ARRAY | VT_I4).
        // This is not what the unmanaged COM server expects. The unmanaged COM
        // server expects the VARIANT to be of type (VT_I4 | VT_ARRAY | VT_BYREF).
        //
        // The other problem is that the original SAFEARRAY remains in memory and
        // does not get freed.
        private object HandleEvent01_v1(ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams)
        {
            // We create an array of pointers to the VARIANTs inside the 
            // DISPPARAMS.
            IntPtr[] pVariantArray = new IntPtr[pDispParams.cArgs];

            IntPtr pVariantTemp = pDispParams.rgvarg;
            for (int i = 0; i < pDispParams.cArgs; i++)
            {
                pVariantArray[i] = pVariantTemp;
                pVariantTemp += Marshal.SizeOf(typeof(VariantStructGeneric));
            }

            // We access the reference to the SAFEARRAY in the first VARIANT and 
            // create a managed array from it.
            int[] array = Marshal.GetObjectForNativeVariant<int []>(pVariantArray[0]);

            // We modify the values in the managed array.
            for (int i = 0; i < array.Length; i++)
            {
                array.SetValue((object)(i + 10), i);
            }

            // We then extend the size of the managed array.
            // New elements will each be of value 0.
            int iSize = array.Length;

            Array.Resize<int>(ref array, iSize + 10);

            // This call to Marshal.GetNativeVariantForObject() unfortunately
            // will not make the original VARIANT contain a SAFEARRAY reference.
            // It will make the original VARIANT contain a direct SAFEARRAY.
            Marshal.GetNativeVariantForObject<int []>(array, pVariantArray[0]);

            return null;
        }

        [StructLayout(LayoutKind.Sequential)]
        struct SAFEARRAYBOUND
        {
            public UInt32 cElements;
            public Int32 lLbound;
        };

        [DllImport("OleAut32.dll", CallingConvention = CallingConvention.StdCall)]
        private static extern Int32 SafeArrayRedim([In] IntPtr psa, [In] SAFEARRAYBOUND [] psaboundNew);

        [DllImport("OleAut32.dll", CallingConvention = CallingConvention.StdCall)]
        private static extern Int32 SafeArrayGetUBound([In] IntPtr psa, [In] UInt32 nDim, [Out] out Int32 iUBoundReceiver);

        [DllImport("OleAut32.dll", CallingConvention = CallingConvention.StdCall)]
        private static extern Int32 SafeArrayGetLBound([In] IntPtr psa, [In] UInt32 nDim, [Out] out Int32 iLBoundReceiver);

        // This event handler is the suitable one for the problem at hand.
        // It directly uses unmanaged Windows APIs to manipulate the reference
        // to the SAFEARRAY contained in the VARIANT from the DISPPARAMS.
        //
        private object HandleEvent01_v2(ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams)
        {
            // We create an array of VariantStructGeneric structs that will represent 
            // the VARIANTS as stored inside the DISPPARAMS.
            VariantStructGeneric[] vsg = new VariantStructGeneric[pDispParams.cArgs];

            IntPtr pVariantTemp = pDispParams.rgvarg;
            for (int i = 0; i < pDispParams.cArgs; i++)
            {
                vsg[i] = Marshal.PtrToStructure<VariantStructGeneric>(pVariantTemp);
                pVariantTemp += Marshal.SizeOf(typeof(VariantStructGeneric));
            }

            // We first access the address of the reference to the SAFEARRAY.
            IntPtr ppsa = vsg[0].variant_part.variant_union.pointer_data;
            // We then dereference this address to get to the actual SAFEARRAY.
            IntPtr psa = Marshal.PtrToStructure<IntPtr>(ppsa);

            Int32 iLBound = 0;
            Int32 iUBound = 0;

            // We then increase the size of the SAFEARRAY by 10 more elements.
            SafeArrayGetLBound(psa, 1, out iLBound);
            SafeArrayGetUBound(psa, 1, out iUBound);

            Int32 iSize = iUBound - iLBound + 1;

            SAFEARRAYBOUND[] rgsabound = new SAFEARRAYBOUND[1];

            rgsabound[0].lLbound = 0;
            rgsabound[0].cElements = (uint)(iSize + 10);

            SafeArrayRedim(psa, rgsabound);

            return null;
        }

        // This event handler also uses unmanaged Windows APIs to manipulate 
        // the SAFEARRAY contained in the reference VARIANT contained in the
        // DISPPARAMS.
        private object HandleEvent02(ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams)
        {
            // We create an array of VariantStructGeneric structs that will represent 
            // the VARIANTS as stored inside the DISPPARAMS.
            VariantStructGeneric[] vsg = new VariantStructGeneric[pDispParams.cArgs];

            IntPtr pVariantTemp = pDispParams.rgvarg;
            for (int i = 0; i < pDispParams.cArgs; i++)
            {
                vsg[i] = Marshal.PtrToStructure<VariantStructGeneric>(pVariantTemp);
                pVariantTemp += Marshal.SizeOf(typeof(VariantStructGeneric));
            }

            // We first access the address of the reference to the VARIANT
            // contained in vsg[0].
            IntPtr pVar = vsg[0].variant_part.variant_union.pointer_data;
            // We then create a managed version of the unmanaged VARIANT.
            VariantStructGeneric Var = Marshal.PtrToStructure<VariantStructGeneric>(pVar);

            // Access the pointer to the SAFEARRAY.
            IntPtr psa = Var.variant_part.variant_union.pointer_data;

            Int32 iLBound = 0;
            Int32 iUBound = 0;

            // We then increase the size of the SAFEARRAY by 10 more elements.
            SafeArrayGetLBound(psa, 1, out iLBound);
            SafeArrayGetUBound(psa, 1, out iUBound);

            Int32 iSize = iUBound - iLBound + 1;

            SAFEARRAYBOUND[] rgsabound = new SAFEARRAYBOUND[1];

            rgsabound[0].lLbound = 0;
            rgsabound[0].cElements = (uint)(iSize + 10);

            SafeArrayRedim(psa, rgsabound);

            return null;
        }

        // This event handler directly access the pointer to the VARIANT_BOOL
        // in the VARIANT in the DISPPARAMS.
        private object HandleEvent03(ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams)
        {
            // We create an array of VariantStructGeneric structs that will represent 
            // the VARIANTS as stored inside the DISPPARAMS.
            VariantStructGeneric[] vsg = new VariantStructGeneric[pDispParams.cArgs];

            IntPtr pVariantTemp = pDispParams.rgvarg;
            for (int i = 0; i < pDispParams.cArgs; i++)
            {
                vsg[i] = Marshal.PtrToStructure<VariantStructGeneric>(pVariantTemp);
                pVariantTemp += Marshal.SizeOf(typeof(VariantStructGeneric));
            }

            // Access the pointer to the VARIANT_BOOL which is a 16-bit value.
            IntPtr pBool = vsg[0].variant_part.variant_union.pointer_data;
            short sh = Marshal.PtrToStructure<short>(pBool);

            // Set the VARINAT_BOOL to VARIANT_TRUE.
            sh = -1;

            Marshal.StructureToPtr<short>(sh, pBool, false);

            return null;
        }

        // This event handler directly access the reference to the BSTR
        // in the VARIANT in the DISPPARAMS. Fortunately, there are adequate 
        // Marshal methods to manipulate BSTRs.
        private object HandleEvent04(ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams)
        {
            // We create an array of VariantStructGeneric structs that will represent 
            // the VARIANTS as stored inside the DISPPARAMS.
            VariantStructGeneric[] vsg = new VariantStructGeneric[pDispParams.cArgs];

            IntPtr pVariantTemp = pDispParams.rgvarg;
            for (int i = 0; i < pDispParams.cArgs; i++)
            {
                vsg[i] = Marshal.PtrToStructure<VariantStructGeneric>(pVariantTemp);
                pVariantTemp += Marshal.SizeOf(typeof(VariantStructGeneric));
            }

            // Access the pointer to the BSTR.
            IntPtr pbstr = vsg[0].variant_part.variant_union.pointer_data;
            // Access the BSTR itself.
            IntPtr bstr = Marshal.PtrToStructure<IntPtr>(pbstr);

            // Create a managed string using the BSTR string.
            string str = Marshal.PtrToStringBSTR(bstr);
            // Completely modify the string.
            str = "Bye Bye World !";

            // Free the original BSTR.
            Marshal.FreeBSTR(bstr);
            // Create a brand new BSTR based on the string value 
            // inside str.
            bstr = Marshal.StringToBSTR(str);

            // Make the pointer to the BSTR point to the new BSTR.
            Marshal.StructureToPtr<IntPtr>(bstr, pbstr, false);

            return null;
        }

        private IConnectionPoint icp = null;
        private int iCookie = -1;
    }
}
  • An IDispatch interface declaration is provided in the code above.
  • I based this definition on that provided from : “CustomQueryInterface – IDispatch and Aggregation”.
  • The TestCOMClassEventsListener2 class is derived from both IDispatch and ICustomQueryInterface.
  • For the ICustomQueryInterface::GetInterface() implementation, we filter out the situation where the GUID of _ITestCOMClassEvents is queried.
  • This happens when the IConnectionPoint::Advise() method for the _ITestCOMClassEvents event of TestCOMClass is called.
  • In response to this, Marshal.GetComInterfaceForObject() is used to obtain the IDispatch interface pointer to the current TestCOMClassEventsListener2 instance.
  • This allows the object to return the IDispatch interface implementation provided by the object itself instead of the one provided by the CLR.
  • In effect this returns to the client caller the dispinterface of the _ITestCOMClassEvents event.

4.3 You may have notice that some methods of the IDispatch interface are not implemented by TestCOMClassEventsListener2, i.e. GetTypeInfoCount() and GetTypeInfo(). These are generally not used in event handling.

4.4 The GetIDsOfNames() method, although not called by the COM server to determine the IDs of the events, is nevertheless important to implement in general for dynamic discovery of IDs for event names.

4.5 The Invoke() method factors out the event handling into separate methods : HandleEvent01_v1(), HandleEvent01_v2(), HandleEvent02(), HandleEvent03() and HandleEvent04(). We will go through these in the coming sections.

4.6 Our managed codes have to emulate standard unmanaged techniques in managing dispinterface-based event handlers. Towards this end, we have to engage with the following unmanaged code constructs :

  • The unmanaged VARIANT structure.
  • The unmanaged DISPPARAMS structure.

4.7 Concerning the VARIANT structure, we use a structure named VariantStructGeneric as a managed representation. This structure is taken from “Defining a VARIANT Structure in Managed Code Part 2”. We will use this structure as advised by the article. For more details on this structure, please refer to its link. The full listing of VariantStructGeneric is given below :

using System;
using System.Runtime.InteropServices;

namespace ConsoleClient02
{
    // The VariantStructGeneric structure is taken from 
    // "Defining a VARIANT Structure in Managed Code Part 2"
    // https://limbioliong.wordpress.com/2011/09/19/defining-a-variant-structure-in-managed-code-part-2/
    //
    [StructLayout(LayoutKind.Sequential)]
    public struct CY_internal_struct_01
    {
        public UInt32 Lo;
        public Int32 Hi;
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct CY
    {
        [FieldOffset(0)]
        public CY_internal_struct_01 cy_internal_struct_01;
        [FieldOffset(0)]
        public long int64;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct __tagBRECORD
    {
        public IntPtr pvRecord;
        public IntPtr pRecInfo;
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct VariantUnion
    {
        [FieldOffset(0)]
        public byte ubyte_data;  // This is the BYTE in C++.
        [FieldOffset(0)]
        public sbyte sbyte_data;  // This is the CHAR in C++.
        [FieldOffset(0)]
        public UInt16 uint16_data; // This is the USHORT in C++.
        [FieldOffset(0)]
        public Int16 int16_data;  // This is the SHORT in C++. Also used for the VARIANT_BOOL.
        [FieldOffset(0)]
        public UInt32 uint32_data; // This is the ULONG in C++. Also for the UINT.
        [FieldOffset(0)]
        public Int32 int32_data;  // This is the LONG in C++. Also used for the SCODE, the INT.
        [FieldOffset(0)]
        public ulong ulong_data;  // This is the ULONGLONG in C++.
        [FieldOffset(0)]
        public long long_data;   // This is the LONGLONG in C++.
        [FieldOffset(0)]
        public float float_data;  // This is the FLOAT in C++.
        [FieldOffset(0)]
        public double double_data; // This is the DOUBLE in C++. Also used for the DATE.
        [FieldOffset(0)]
        public IntPtr pointer_data;// Used for BSTR and all other pointer types.
        [FieldOffset(0)]
        public CY cy_data;  // This is the CY structure in C++.
        [FieldOffset(0)]
        public __tagBRECORD record_data; // This is the __tagBRECORD structure in C++.
    };

    [StructLayout(LayoutKind.Sequential)]
    public struct VariantStructure
    {
        public ushort vt;
        public ushort wReserved1;
        public ushort wReserved2;
        public ushort wReserved3;
        public VariantUnion variant_union;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct Decimal_internal_struct_01
    {
        public byte scale;
        public byte sign;
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct Decimal_internal_union_01
    {
        [FieldOffset(0)]
        public Decimal_internal_struct_01 decimal_internal_struct_01;
        [FieldOffset(0)]
        public ushort signscale;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct Decimal_internal_struct_02
    {
        public UInt32 Lo32;
        public UInt32 Mid32;
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct Decimal_internal_union_02
    {
        [FieldOffset(0)]
        public Decimal_internal_struct_02 decimal_internal_struct_02;
        [FieldOffset(0)]
        public ulong Lo64;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct DecimalStructure
    {
        public ushort wReserved;
        public Decimal_internal_union_01 decimal_internal_union_01;
        public UInt32 Hi32;
        public Decimal_internal_union_02 decimal_internal_union_02;
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct VariantStructGeneric
    {
        [FieldOffset(0)]
        public VariantStructure variant_part;
        [FieldOffset(0)]
        public DecimalStructure decimal_part;
    }

}

4.8 And as for the DISPPARAMS structure, we will use the counterpart defined in the System.Runtime.InteropServices.ComTypes namespace.

4.9 The DISPPARAMS structure has a memory layout as follows :

DISPPARAMS

  • The rgvarg field is a pointer to a memory location that contains an array of VARIANT structures.
  • The cArgs field indicate the length of this array.

Hence in managed code, we will access the VARIANTs and transform them into an array of VariantStructGeneric structures as follows :

// We create an array of VariantStructGeneric structs that will represent 
// the VARIANTS as stored inside the DISPPARAMS.
VariantStructGeneric[] vsg = new VariantStructGeneric[pDispParams.cArgs];

IntPtr pVariantTemp = pDispParams.rgvarg;
for (int i = 0; i < pDispParams.cArgs; i++)
{
    vsg[i] = Marshal.PtrToStructure<VariantStructGeneric>(pVariantTemp);
    pVariantTemp += Marshal.SizeOf(typeof(VariantStructGeneric));
}
  • We define an array of VariantStructGeneric and use the cArgs field value to determine the length of the array.
  • We then iterate through the array of VARIANTs and trasnform each into a VariantStructGeneric using Marshal.PtrToStructure().

This is the standard way I access the VARIANTs contained in a DISPPARAMS throughout the code.

5. Handling of Event01 Version 1.

5.1 For Event01(), we have two versions for the handler. As usual, one will work while the other will not.

5.2 I will discuss first the invalid one (HandleEvent01_v1()) here in this section.

5.3 I want to present this unworkable solution due to the use of the Marshal.GetObjectForNativeVariant() and Marshal.GetNativeVariantForObject() methods. These methods look like they are most useful in the current situation and some developers may instinctively want to use them. However, as will be shown below, their use can be misleading.

5.4 The code for the HandleEvent01_v1() method is listed below :

// This event handler is not suitable for the problem at hand. 
// It uses Marshal class methods to access the SAFEARRAY
// from the VARIANT in the DISPPARAMS and store them in a managed array.
// After manipulating the managed array, we attempt to store the values
// of the managed array back into the SAFEARRAY in the VARIANT using
// Marshal.GetNativeVariantForObject().
//
// However, this will not work because Marshal.GetNativeVariantForObject()
// may create a new SAFEARRAY and store it in the VARIANT of type (VT_ARRAY | VT_I4).
// This is not what the unmanaged COM server expects. The unmanaged COM
// server expects the VARIANT to be of type (VT_I4 | VT_ARRAY | VT_BYREF).
//
// The other problem is that the original SAFEARRAY remains in memory and
// does not get freed.
private object HandleEvent01_v1(ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams)
{
    // We create an array of pointers to the VARIANTs inside the 
    // DISPPARAMS.
    IntPtr[] pVariantArray = new IntPtr[pDispParams.cArgs];

    IntPtr pVariantTemp = pDispParams.rgvarg;
    for (int i = 0; i < pDispParams.cArgs; i++)
    {
        pVariantArray[i] = pVariantTemp;
        pVariantTemp += Marshal.SizeOf(typeof(VariantStructGeneric));
    }

    // We access the reference to the SAFEARRAY in the first VARIANT and 
    // create a managed array from it.
    int[] array = Marshal.GetObjectForNativeVariant<int[]>(pVariantArray[0]);

    // We modify the values in the managed array.
    for (int i = 0; i < array.Length; i++)
    {
        array.SetValue((object)(i + 10), i);
    }

    // We then extend the size of the managed array.
    // New elements will each be of value 0.
    int iSize = array.Length;

    Array.Resize<int>(ref array, iSize + 10);

    // This call to Marshal.GetNativeVariantForObject() unfortunately
    // will not make the original VARIANT contain a SAFEARRAY reference.
    // It will make the original VARIANT contain a direct SAFEARRAY.
    Marshal.GetNativeVariantForObject<int[]>(array, pVariantArray[0]);

    return null;
}
  • The code uses the VariantStructGeneric structure briefly.
  • I will mention this structire again later.
  • For now, just note that it is a managed representation of the unmanaged VARIANT structure.

5.5 The problem does not reside completely in the use of the Marshal.GetObjectForNativeVariant() method to obtain a managed array from the incoming VARIANT.

5.6 It is perfectly fine to obtain a managed array from the VARIANT and then to manipulate it using familiar managed coding practices (e.g. changing its element values, changing the array size, etc).

5.7 However, after Marshal.GetObjectForNativeVariant() extracts the SAFEARRAY from the input VARIANT and stores it into a managed array, the managed array is completely independent on the original SAFEARRAY. This is illustrated below :

HandleEvent01_v1_01

5.8 As HandleEvent01_v1() works on the managed array (changing its element values and extending its size), there will be no effect on the original SAFEARRAY :

HandleEvent01_v1_02

5.9 Furthermore, Marshal.GetObjectForNativeVariant() does not call VariantClear() and the input VARIANT. This is by design and is not a bug in any way. There is no logical reason why it should release the resources contained in the VARIANT.

5.10 Later, at the end of HandleEvent01_v1(), Marshal.GetNativeVariantForObject() is used to store the latest values of the managed array into the VARIANT to be returned to the caller. Here, yet another problem occurs : Marshal.GetNativeVariantForObject() will not make the returning VARIANT contain a SAFEARRAY reference (VT_BYREF | VT_ARRAY | VT_I4). It will instead make the VARIANT contain a direct SAFEARRAY (VT_ARRAY | VT_I4) :

HandleEvent01_v1_03

5.11 Note that Marshal.GetNativeVariantForObject() is not at fault here. It has done its expected job. There is just no way to tell it that we want it to store a reference to a SAFEARRAY instead of a direct SAFEARRAY.

5.12 In summary, here are the problems of HandleEvent01_v1() method :

  • The incomplete involvement of the input SAFEARRAY (wrapped inside the input VARIANT) throughout the event handler code.

After the original SAFEARRAY has been transferred into a managed array, it is no longer accessed throughout HandleEvent01_v1(). This is incorrect because it is passed by reference and so any changes intended to be done to it (e.g. changing of values of elements, changing of array size, etc) is supposed to be reflected back the caller on return. Instead of that, the SAFEARRAY is left out of all activities inside the event handler. Note that the COM server is not expected to check the returned VARIANT for any updates to the original SAFEARRAY. The SAFEARRAY is expected to be updated by the time the IDispatch::Invoke() returns (inside Fire_Event01()).

  • The mangling of the original VARIANT (by Marshal.GetNativeVariantForObject()) on return to the caller.

Now, at the end of HandleEvent01_v1(), Marshal.GetNativeVariantForObject() is called with an intention to return an updated SAFEARRAY to the caller. But this will not happen because Marshal.GetNativeVariantForObject() will create a new SAFEARRAY and store it in the VARIANT of type (VT_ARRAY | VT_I4). This is not what the COM server expects. The COM server expects the VARIANT to be of type (VT_I4 | VT_ARRAY | VT_BYREF) with the reference to the original SAFEARRAY. The change to the type of the VARIANT, unfortunately, is permanent. If left unchecked may give rise to potential problems.

  • Possible memory leakage of the returned SAFEARRAY.

With a new SAFEARRAY (separate from the original one passed to the event handler) unexpectedly returned to the caller, this new SAFEARRAY remains in memory without ever being used and without ever being released. A memory leak results.

5.13 I have discussed in detail the problems with Marshal.GetObjectForNativeVariant() and Marshal.GetNativeVariantForObject() here in this section due to its likely use by developers who may not be aware of their unsuitability. Their names certainly do sound useful and appropriate for usage. But as shown under the circumstances here (i.e. when working with a referenced SAFEARRAY), they are not suitable for usage.

6. Handling of Event01 Version 2.

6.1 Here in this section I provide a completely workable version of the Event01() handler. The code is listed below :

// This event handler is the suitable one for the problem at hand.
// It directly uses unmanaged Windows APIs to manipulate the reference
// to the SAFEARRAY contained in the VARIANT from the DISPPARAMS.
//
private object HandleEvent01_v2(ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams)
{
    // We create an array of VariantStructGeneric structs that will represent 
    // the VARIANTS as stored inside the DISPPARAMS.
    VariantStructGeneric[] vsg = new VariantStructGeneric[pDispParams.cArgs];

    IntPtr pVariantTemp = pDispParams.rgvarg;
    for (int i = 0; i < pDispParams.cArgs; i++)
    {
        vsg[i] = Marshal.PtrToStructure<VariantStructGeneric>(pVariantTemp);
        pVariantTemp += Marshal.SizeOf(typeof(VariantStructGeneric));
    }

    // We first access the address of the reference to the SAFEARRAY.
    IntPtr ppsa = vsg[0].variant_part.variant_union.pointer_data;
    // We then dereference this address to get to the actual SAFEARRAY.
    IntPtr psa = Marshal.PtrToStructure<IntPtr>(ppsa);

    Int32 iLBound = 0;
    Int32 iUBound = 0;

    // We then increase the size of the SAFEARRAY by 10 more elements.
    SafeArrayGetLBound(psa, 1, out iLBound);
    SafeArrayGetUBound(psa, 1, out iUBound);

    Int32 iSize = iUBound - iLBound + 1;

    SAFEARRAYBOUND[] rgsabound = new SAFEARRAYBOUND[1];

    rgsabound[0].lLbound = 0;
    rgsabound[0].cElements = (uint)(iSize + 10);

    SafeArrayRedim(psa, rgsabound);

    return null;
}
  • After extracting a VariantStructGeneric array from DISPPARAMS, we access the reference to the SAFEARRAY from the first VariantStructGeneric.
  • We do this by de-referencing the pointer_data stored in VariantStructGeneric :
// We first access the address of the reference to the SAFEARRAY.
IntPtr ppsa = vsg[0].variant_part.variant_union.pointer_data;
// We then dereference this address to get to the actual SAFEARRAY.
IntPtr psa = Marshal.PtrToStructure<IntPtr>(ppsa);
  • The call to Marshal.PtrToStructure() on a data (ppsa) of type IntPtr look like a strange construct.
  • But it is one way to obtain a pointer from a double pointer in managed code without using unsafe code.
  • Note that psa will directly point to the SAFEARRAY contained in the original VARIANT of the DISPPARAMS :

HandleEvent01_v2_01

  • The rest of the code manages the SAFEARRAY using standard OLE APIs like SafeArrayGetLBound(), SafeArrayGetUBound() and SafeArrayRedim() :

HandleEvent01_v2_02

  • Now, on return, there is not need to perform any VARIANT updates in the DISPPARAMS.
  • This is because we have already achieved our objective of modifying the reference SAFEARRAY that is contained in the DISPPARAMS.

6.2 Throughout the entire HandleEvent01_v2() event handler, the actual SAFEARRAY contained in the VARIANT is referenced constantly and hence all changes to it are permanent and will be returned as changed back to the caller.

7. Test Code.

7.1 With all the workaround code in place, shown below is the code for the rest of the test program :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TestCOMServerLib;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

namespace ConsoleClient02
{
    class Program
    {
        static void DoTest2()
        {
            TestCOMClass testCOMObj = new TestCOMClass();
            TestCOMClassEventsListener2 listener = new TestCOMClassEventsListener2(testCOMObj);
            testCOMObj.TestMethod01();
        }

        static void Main(string[] args)
        {
            DoTest2();
            Console.ReadKey();
        }
    }
}

8. In Summary.

8.1 Great kudos to the Microsoft Development Team for designing the ICustomQueryInterface interface which allowed the successful implementation of the workaround code.

8.2 The research work was quite tiring but it was great fun to see eventually that it is possible to pass a referenced SAFEARRAY event parameter to a managed handler and have it returned with changes and the return HRESULT being S_OK.

8.3 I hope this long and detailed treatise has benefitted the reader.

8.4 In the next part to follow this article, I shall expound techniques for working with IDispatch::Invoke() parameters of other referenced types.

9. Source Codes Download.

9.1 The source codes for this article can be found here in CodePlex.

Advertisements

About Lim Bio Liong

I've been in software development for nearly 20 years specializing in C , COM and C#. It's truly an exicting time we live in, with so much resources at our disposal to gain and share knowledge. I hope my blog will serve a small part in this global knowledge sharing network. For many years now I've been deeply involved with C development work. However since circa 2010, my current work has required me to use more and more on C# with a particular focus on COM interop. I've also written several articles for CodeProject. However, in recent years I've concentrated my time more on helping others in the MSDN forums. Please feel free to leave a comment whenever you have any constructive criticism over any of my blog posts.

Discussion

No comments yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: