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

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

1. Introduction.

1.1 This article is a follow up to my earlier blog which outlined the problems with 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 Through this article and the next one to follow, I will demonstrate 2 attempts at working around the problem.

1.3 Both workarounds use the COM Connection Point Protocol for event handling in managed code.

1.4 I have defined a new method to the _ITestCOMClassEvents event interface which was demonstrated in part 1. This will be presented in the next section.

1.5 To facilitate the firing of these new events, I have also extended ITestCOMClass with an additional method.

2. The New Methods of _ITestCOMClassEvents and ITestCOMClass.

2.1 The following is a new listing for the _ITestCOMClassEvents dispinterface :

[
    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);
};
  • Event01() has been discussed in part 1.
  • Event02() is a new event which is fired with a reference VARIANT parameter.

2.2 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();
};

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

3. General Approach for Workarounds.

3.1 As mentioned earlier, the workarounds are based on the use of COM’s native IConnectionPoint and IConnectionPointContainer interfaces to do the event hookup.

3.2 We define classes that will perform the Connection Point Protocol hookup and event handling.

3.3 For the first attempt at workaround, the following class TestCOMClassEventsListener is defined :

using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

// TestCOMClassEventsListener was my first attempt at a workaround
// for the problem. I derived the TestCOMClassEventsListener
// class from the event interface itself _ITestCOMClassEvents.
// 
// Then I connected an instance of TestCOMClassEventsListener to
// the _ITestCOMClassEvents event connection point of an instance
// of the TestCOMClass object.
public class TestCOMClassEventsListener : _ITestCOMClassEvents
{
    public TestCOMClassEventsListener(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 of this object to the connection point.
        icp.Advise(this, out iCookie);
    }

    ~TestCOMClassEventsListener()
    {
        if (iCookie != -1)
        {
            icp.Unadvise(iCookie);
            iCookie = -1;
        }
    }
    ...
    ...
    ...
    private IConnectionPoint icp = null;
    private int iCookie = -1;
}
  • The IConnectionPointContainer and the IConnectionPoint interface definitions are contained in the System.Runtime.InteropServices.ComTypes namespace.
  • The COM Connection Point Protocol is established in the constructor.
  • A reference to the TestCOMClass COM object testCOMObj (the server which fires the events) is passed as parameter to the constructor.
  • A pointer to the IConnectionPointContainer interface is obtained from testCOMObj by performing a cast.
  • The GUID of the _ITestCOMClassEvents event interface is obtained and then passed to the FindConnectionPoint() method.
  • A pointer to the IConnectionPoint interface of testCOMObj is returned.
  • We then establish the event fire-handle connection by calling Advise() and passing a reference to the TestCOMClassEventsListener object as parameter.
  • A cookie is returned which is kept and later used to disconnect from the IConnectionPoint pointer in the destructor.

This is the standard way to connect an unmanaged server with a managed client through the Connection Point Protocol instead of delegates.

3.4 The test client program is listed 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
{    
    class Program
    {
        static void DoTest1()
        {
            TestCOMClass testCOMObj = new TestCOMClass();
            TestCOMClassEventsListener listener = new TestCOMClassEventsListener(testCOMObj);
            testCOMObj.TestMethod01();
            testCOMObj.TestMethod02();
        }

        static void Main(string[] args)
        {
            DoTest1();
            Console.ReadKey();
        }        
    }
}
  • The test code is inside DoTest1().
  • Here, a TestCOMClass instance is created and then passed as parameter to a new instance of TestCOMClassEventsListener.
  • TestMethod01() and TestMethod0s() will each fire Event01() and Event02() respectively.

3.5 We shall now focus our attention on the event handlers beginning from the next section.

3.6 Regarding the problem of passing a reference SAFEARRAY as event parameter, the TestCOMClassEventsListener class itself contains 2 attempts at providing workarounds.

3.7 I will demonstrate how the first workaround, unfortunately, will not work.

3.8 The second one will work as we shall see.

4. The First Workaround Attempt for Passing Referenced SAFEARRAY as Event Parameter (Will Not Work).

4.1 The following is the implementation for CTestCOMClass::TestMethod01() as was shown in part 1 :

STDMETHODIMP CTestCOMClass::TestMethod01()
{
    // TODO: Add your implementation code here
    HRESULT hr;
    SAFEARRAY* psa = NULL;
    SAFEARRAYBOUND rgsabound[1];

    rgsabound[0].lLbound = 0;
    rgsabound[0].cElements = 4;

    int intArray[] = { 1, 2, 3, 4 };

    psa = SafeArrayCreate(VT_I4, 1, rgsabound);

    for (int i = 0; i < 4; i++)
    {
        long ix[1];

        ix[0] = i;
        hr = SafeArrayPutElement(psa, ix, &(intArray[i]));
    }

    Fire_Event01(&psa);

    // psa was passed by reference.
    // If the client event handler wanted to modify it,
    // it has the responsibility to properly resize the array or
    // to destroy it and then re-allocate a new SAFEARRAY.
    // 
    // When we as the server receives the returned 
    // SAFEARRAY, it is the server's responsibility to later
    // SafeArrayDestroy() it.
    SafeArrayDestroy(psa);
    psa = NULL;

    return S_OK;
}
  • As can be seen, TestMethod01() will create a SAFEARRAY of integers.
  • The SAFEARRAY is returned from the SafeArrayCreate() API as a pointer.
  • We pass the address of the SAFEARRAY pointer to Fire_Event01().
  • As per COM protocol, when the event has been fired and control returned to TestMethod01(), TestMethod01() has the responsibility to release the resources of the SAFEARRAY.
  • This we do by calling SafeArrayDestroy().

4.2 The code for Fire_Event01() is listed below :

HRESULT Fire_Event01(SAFEARRAY** ppSafeArray)
{
    HRESULT hr = S_OK;
    T * pThis = static_cast<T *>(this);
    int cConnections = m_vec.GetSize();

    for (int iConnection = 0; iConnection < cConnections; iConnection++)
    {
        pThis->Lock();
        CComPtr<IUnknown> punkConnection = m_vec.GetAt(iConnection);
        pThis->Unlock();

        IDispatch * pConnection = static_cast<IDispatch *>(punkConnection.p);

        if (pConnection)
        {
            int iSize = sizeof(VARIANT);
            VARIANT avarParams[1];
            VariantInit(&(avarParams[0]));
            V_VT(&(avarParams[0])) = VT_I4 | VT_ARRAY | VT_BYREF;
            V_ARRAYREF(&(avarParams[0])) = ppSafeArray;

            DISPPARAMS params = { avarParams, NULL, 1, 0 };

            UINT index = 0;
            EXCEPINFO   ex;
            VARIANT varResult;

            VariantInit(&varResult);

            // This project will demonstrate that hr will always return E_INVALIDARG when the client
            // is a C# managed application. This is so even if the client event handler does nothing
            // but simply returns.
            hr = pConnection->Invoke
            (
                1, 
                IID_NULL, 
                LOCALE_USER_DEFAULT, 
                DISPATCH_METHOD, 
                &params, 
                &varResult, 
                &ex, 
                &index
            );
        }
    }
    return hr;
}
  • Fire_Event01() contains typical ATL event firing code.
  • A double pointer to the SAFEARRAY is passed as parameter.
  • As per COM dispinterfaced event firing protocol, the reference SAFEARRAY is packaged inside a VARIANT “parameter” which is then passed via a DISPPARAMS struct to the IDispatch::Invoke() call.
  • The VARIANT “parameter” is defined to be of type (VT_I4 | VT_ARRAY | VT_BYREF).

4.3 The C# event handler is displayed below :

public class TestCOMClassEventsListener : _ITestCOMClassEvents
{
    ...
    ...
    ...
    // This event handler does not help the problem of the return
    // value of E_INVALIDARG. Neither does it help in ensuring 
    // that modifications to the intArray gets passed back to 
    // the event caller.
    //
    // The eventualities are :
    //
    // 1. The return value from IDispatch::Invoke() on the client
    // side is E_NOINTERFACE.
    //
    // 2. Sometimes, the client (i.e. this application) can trigger
    // an exception after executing the event handler.
    //
    // 3. If all went well, the SAFEARRAY passed to IDispatch::Invoke() 
    // can become invalid. And this happens even if nothing is done 
    // to the array.
    //        
    [DispId(1)]
    public void Event01(ref Array intArray)
    {
        // Note that this event handler is trivial
        // and does nothing but return. Yet the SAFEARRAY
        // from the client side can become invalid.
        return;
    }
    ...
    ...
    ...
}
  • The event handler Event01() is defined with a signature that is generated by the type library importer when the TestCOMServer.dll is referenced by the C# client.
  • Event01() is a completely trivial and does nothing but returns.
  • At runtime, when control reaches Event01(), intArray will contain all the values as set in TestMethod01().
  • But when control returns to the IDispatch::Invoke() call in Fire_Event01(), a crash will sometimes occur.
  • On my machine, the crash is manifested as follows :

TriggerBreakpoint

  • In the course of testing, I discovered that even if no exception were to occur (which is probably by luck), the return value from the IDispatch::Invoke() call is E_NOINTERFACE and the pointer to the SAFEARRAY that originated from TestMethod01() can become invalid on return from IDispatch::Invoke().

4.4 One conclusion we can draw from the above test is that the CLR’s internal default implementation of IDispatch is still inadequate in handling reference SAFEARRAY parameters, even if we were to use the Connection Point Protocol and not event delegates.

5. The Second Workaround Attempt for Passing Referenced SAFEARRAY as Event Parameter (Will Work).

5.1 In this section, I will demonstrate an alternative technique for passing a SAFEARRAY to unmanaged code. Not directly as a double pointer to a SAFEARRAY, but through a referenced VARIANT.

5.2 This is demonstrated by the firing and handling of Event02().

5.3 Before we proceed, I need to point out an important COM protocol which apply in the upcoming case :

When passing a VARIANT parameter by reference, i.e. via an [in, out] parameter, to a method, a standard set of protocols must be followed by the caller and the callee :

  • Whatever the caller passes via the VARIANT, it will cease to be of any concern to the caller once the VARIANT has been passed to the callee.
  • The VARIANT effectively becomes owned by the callee code.
  • The callee is then free to use the contents of the VARIANT as it sees fit.
  • However, because it now owns it, it has the responsibility to free any resources associated with it if necessary.
  • On return from the function, the callee may pass anything back to the caller through the VARIANT which is now returned as an “out” parameter.
  • The callee may choose to pack the original contents back into the VARIANT. This act itself may be considered as freeing the resource.
  • When control returns to the caller and it receives the returned VARIANT, it will now own its contents and it now has the responsibility to free any resources associated with it if necessary.
  • Please refer to “Passing a VARIANT Parameter by Reference” for a spirited discussion of this.

5.4 The following is the implementation for the new CTestCOMClass::TestMethod02() :

STDMETHODIMP CTestCOMClass::TestMethod02()
{
    // TODO: Add your implementation code here
    HRESULT hr;
    SAFEARRAY* psa = NULL;
    SAFEARRAYBOUND rgsabound[1];

    rgsabound[0].lLbound = 0;
    rgsabound[0].cElements = 4;

    int intArray[] = { 1, 2, 3, 4 };

    psa = SafeArrayCreate(VT_I4, 1, rgsabound);

    for (int i = 0; i < _countof(intArray); i++)
    {
        long ix[1];

        ix[0] = i;
        hr = SafeArrayPutElement(psa, ix, &(intArray[i]));
    }

    VARIANT var;

    VariantInit(&var);
    V_VT(&var) = VT_ARRAY | VT_I4;
    V_ARRAY(&var) = psa;

    Fire_Event02(&var);

    // If var is modified by client event handler code,
    // I can expect client to first call VariantClear().
    // psa should have been destroyed by the time Fire_Event02()
    // returns.
    // Hence psa need not be SafeArrayDestroy()'ed.
    /*
    SafeArrayDestroy(psa); 
    psa = NULL;    
    */

    // However, the returned VARIANT is our responsibility.
    // Hence we should VariantClear() it.
    VariantClear(&var); 

    return S_OK;
}

The following is a listing of Fire_Event02() :

HRESULT Fire_Event02(VARIANT* pVar)
{
    HRESULT hr = S_OK;
    T * pThis = static_cast<T *>(this);
    int cConnections = m_vec.GetSize();

    for (int iConnection = 0; iConnection < cConnections; iConnection++)
    {
        pThis->Lock();
        CComPtr<IUnknown> punkConnection = m_vec.GetAt(iConnection);
        pThis->Unlock();

        IDispatch * pConnection = static_cast<IDispatch *>(punkConnection.p);

        if (pConnection)
        {
            VARIANT avarParams[1];
            VariantInit(&(avarParams[0]));
            V_VT(&(avarParams[0])) = VT_VARIANT | VT_BYREF;
            V_VARIANTREF(&(avarParams[0])) = pVar;

            EXCEPINFO einfo;
            UINT index = 0;
            VARIANT varResult;

            VariantInit(&varResult);

            DISPPARAMS params = { avarParams, NULL, 1, 0 };
            hr = pConnection->Invoke
                 (
                2, 
                IID_NULL, 
                LOCALE_USER_DEFAULT, 
                DISPATCH_METHOD, 
                &params, 
                &varResult, 
                &einfo, 
                &index
                 );
        }
    }
    return hr;
}

The following is a summary of the actions performed by TestMethod02() and then Fire_Event02() :

  • In TestMethod02(), a SAFEARRAY psa is created and is packed into a VARIANT var.
  • A pointer to var is passed to Fire_Event02().
  • Fire_Event02() will prepare another VARIANT (i.e. avarParams[0]) to hold a pointer to var (which contains the SAFEARRAY psa).
  • avarParams[0] is of type VT_VARIANT | VT_BYREF.
  • Note well it is var that is passed by reference, not the SAFEARRAY that it contains.
  • avarParams[0] gets referenced inside a DISPPARAMS structure and gets passed to the event handler for Event02().
  • As per protocol, once var gets passed to the Event02() handler, the event handler is to assume ownership of var’s contents, i.e. the original SAFEARRAY psa.
  • And when control returns to TestMethod02(), it does not have the obligation to call SafeArrayDestroy() on it.
  • However, TestMethod02() must call VariantClear() on var, whatever it now contains.

5.5 The following is the managed event handler for Event02() :

// This event handler works correctly and may serve as a suitable 
// workaround for the problem provided we use a VARIANT reference
// as the parameter type.
[DispId(2)]
public void Event02(ref object pvarArray)
{
    int[] intArray = (int[])pvarArray;
    int i = 0;

    // Increment each element by 10.
    for (i = 0; i < intArray.Length; i++)
    {
        intArray[i] += 10;
    }

    // Increase the size of the array.
    int iCurrentSize = intArray.Length;

    // Note that Array.Resize() will allocate a new array object
    // and intArray will be made to reference the new object.
    // See "Array.Resize<T> Method (T[], Int32)"
    // https://msdn.microsoft.com/en-us/library/bb348051(v=vs.110).aspx
    Array.Resize<int>(ref intArray, iCurrentSize + 4);

    // Set the value of each of the new elements.
    for (i = iCurrentSize; i < iCurrentSize + 4; i++)
    {
        intArray[i] = i;
    }

    // We have to assign the new intArray array back into the pvarArray object.
    // Otherwise the resized array will not be returned.
    pvarArray = (object)intArray;
}
  • When control reaches Event02(), the pvarArray is filled with the correct data : an array of integers.
  • After that, Event02() attempts to modify the contents of the original array and then resize it and fill the extended parts of the array with values.
  • Now the last statement of Event02() is very important : it assigns intArray to pvarArray.
  • This last statement is necessary otherwise the SAFEARRAY returned to the caller will not reflect the changed size.
  • This has very much to do with the way Array.Resize() works.
  • Array.Resize() will allocate a new array object in memory and assign it to intArray.
  • See the Remarks Section of the MSDN documentation for more information.
  • Once intArray is resized, it departs from pvarArray as a separate array.
  • Hence we must re-assign pvarArray to it in order that the latest array is returned to the caller.

5.6 The managed Event02() event handler generally works correctly. On return to Fire_Event02(), the HRESULT from the call to IDispatch::Invoke() will be S_OK.

5.7 The VARIANT var will contain an updated SAFEARRAY that will contain the latest elements. var may now contain a brand new SAFEARRAY or it may contain the original one (psa) albeit with new elements.

5.8 As per COM protocol for referenced VARIANTs, the original SAFEARRAY psa created in TestMethod02() is to be owned and managed by the CLR once it reaches managed code (i.e. the C# Event02() handler).

5.9 When Event02() returns and control returns to the Fire_Event02() call in TestMethod02(), psa, if it is not re-used and re-packed into var, will be invalidated.

5.10 The evidence of this which can be observed as we debug the test program ConsoleClient02.

5.11 On my machine, I put 2 breakpoints in TestMethod02() : at the point where we call Fire_Event02(), and just before we call VariantClear().

5.12 As shown in the screenshot below, at the point where we call Fire_Event02(), psa is located at address 0x013bd5a8 and its pvData is at 0x013de020.

BPatFireEvent02.small

5.13 Now, right after Fire_Event02() returns, psa remains at 0x013bd5a8 but its pvData has been set to NULL (see the square enclosures in the screenshots below) :

BPatVariantClear.small

5.14 Note that the CLR is free to re-use the original SAFEARRAY psa to hold new array elements. So on return to TestMethod02(), the following will hold true :

  • var may contain the original SAFEARRAY psa.
  • If so, psa -> pvData may point to either a new memory location or to the same memory location albeit with new data.

6. Yet Another Workaround (May Not Work Completely).

6.1 Looking at the signature of Event02(), it looks suspiciously as if we can do away with the Connection Point Protocol and simply use delegates to handle Event02().

6.2 Well, we certainly can. But there is doubt as to whether the original SAFEARRAY, if not re-used, will ever be released.

6.3 Let’s do a test on this. Listed below is a new method of the client application that we will use for the test :

static void DoTestDelegate()
{
    TestCOMClass testCOMObj = new TestCOMClass();

    testCOMObj.Event02 += TestCOMObj_Event02;
    testCOMObj.TestMethod02();
}

private static void TestCOMObj_Event02(ref object pvarArray)
{
    int[] intArray = (int[])pvarArray;
    int i = 0;

    // Increment each element by 10.
    for (i = 0; i < intArray.Length; i++)
    {
        intArray[i] += 10;
    }

    // Increase the size of the array.
    int iCurrentSize = intArray.Length;

    // Note that Array.Resize() will allocate a new array object
    // and intArray will be made to reference the new object.
    // See "Array.Resize<T> Method (T[], Int32)"
    // https://msdn.microsoft.com/en-us/library/bb348051(v=vs.110).aspx
    Array.Resize<int>(ref intArray, iCurrentSize + 4);

    // Set the value of each of the new elements.
    for (i = iCurrentSize; i < iCurrentSize + 4; i++)
    {
        intArray[i] = i;
    }

    // We have to assign the new intArray array back into the pvarArray object.
    // Otherwise the resized array will not be returned.
    pvarArray = (object)intArray;
}

We use a delegate to handle Event02() and the delegate method is TestCOMObj_Event02() which contains the exact same code as TestCOMClassEventsListener::Event02().

6.4 We use the same breakpoints of the last test and observe what happens. The screenshot below shows that just before Fire_Event02() is called, the SAFEARRAY psa is located at address 0x00e92660 and its pvData is at 0x00e9ad98 :

DelegateBPatFireEvent02.small

6.5 Now after Fire_Event02() returns, we see that psa remains pointing at 0x00e92660 and its pvData at 0x00e9ad98 :

DelegateBPatVariantClear.small

6.6 The contents of var, however, shows that it contains a pointer to a SAFEARRAY at memory location 0x00e92720 and its pvData is at 0x00e9d078 :

DelegateBPatVariantClearShowVar.small

This shows that another SAFEARRAY is returned to TestMethod02() via var.

6.7 However, the original SAFEARRAY seems intact and does not seem to be released.

6.8 From here it seems clear that parameter handling is different between event handlers deriving from COM Connection Points and from delegates.

6.9 The above observations are just anecdotal of course. There is no evidence that the CLR will not eventually release the original SAFEARRAY.

6.10. But I would not advise using this workaround if possible.

7. In Conclusion.

7.1 The workaround as demonstrated in section 5 certainly works well.

7.2 The use of the Connection Point Protocol is certainly foreign to the CLR and does not blend naturally with a language like C#. The presence of a COM object and a COM event is evident.

7.3 But this is a trade off that may have to be accepted in order to work with referenced SAFEARRAYs.

7.4 If the use of the Connection Point Protocol suits fine with a project team, an even better solution awaits us in part 3 of this series of articles.

8. Source Codes Download.

8.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.

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: