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

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

1. Introduction

1.1 I recently ran into an unexpected problem with the .NET CLR.

1.2 It appears that its internal internal mechanism for processing COM events inadequately handles reference SAFEARRAYs passed as event parameters.

1.3 In this blog, I will demonstrate the problem with sample codes. It is my hope that interested readers will research into this area and maybe discover any error that gave rise to the issue at hand.

2. Problem Description.

2.1 The problem is described as follows :

  • An unmanaged COM DLL server fires a dispinterface-based event that takes as parameter a reference to a SAFEARRAY (i.e. a double pointer in C++).
  • The event reaches the managed C# client via a delegate method.
  • The event parameter, a reference to a System.Array object, contains the correct data set on the unmanaged server side.
  • However, 2 unexpected and unfavorable things will happen :
    • Any changes done to the array in the C# event is not returned to the COM server.
    • The IDispatch::Invoke() method call (done on the COM server side) always returns a value of E_INVALIDARG.

2.2 The E_INVALIDARG HRESULT code is returned even if the event handler does nothing and simply returns.

3. Sample Code for Unmanaged COM Server.

3.1 Create an unmanaged COM server (I recommend using ATL) with an interface as follows :

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

3.2 Further define a dispinterface-based event interface :

[
    uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)        
]
dispinterface _ITestCOMClassEvents
{
    properties:
    methods:
        [id(1)] HRESULT Event01([in, out] SAFEARRAY(int)* intArray);
};

Notice that Event01() takes a SAFEARRAY(int)* as parameter. This parameter is designated as [in, out].

3.3 The purpose of the TestMethod01() method is simply to fire the Event01 event to the C# client. The following is a sample code :

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);

    SafeArrayDestroy(psa);
    psa = NULL;

    return S_OK;
}

3.4 For event firing, I used the ATL supplied IConnectionPointImpl<>-based proxy, which in my case is named CProxy_ITestCOMClassEvents. The following is a sample implementation of its Fire_Event01() method :

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)
        {
            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 };

            VARIANT varResult;
            VariantClear(&varResult);

            hr = pConnection->Invoke(1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &params, &varResult, NULL, NULL);
        }
    }
    return hr;
}

4. Sample Code for the Managed C# Client.

4.1 Create a Console client application in C# that references the COM server’s .tlb type library file or the DLL itself.

4.2 My sample COM server is named TestCOMServer.dll and so when referenced by a C# client, an interop assembly (e.g. Interop.TestCOMServerLib.dll) will be generated by the IDE’s Type Library Importer.

4.3 The event handler delegate for Event01() is defined as follows :

public delegate void _ITestCOMClassEvents_Event01EventHandler(ref System.Array intArray);

4.4 In the C# client code, add a “using” statement to reference the types in the interop assembly, e.g. :

using TestCOMServerLib;

4.5 Define a DoTest() method as follows :

static void DoTest()
{
    TestCOMClass testCOMObj = new TestCOMClass();
    testCOMObj.Event01 += TestCOMObj_Event01;
    testCOMObj.TestMethod01();
}

4.6 Add the event handler as follows :

private static void TestCOMObj_Event01(ref Array intArray)
{
    return;
}

Notice that the implementation is trivial. It does nothing and just returns.

4.7 Make a call to DoTest() in the Main() method :

static void Main(string[] args)
{
    DoTest();
}

5. Observations at Runtime with Trivial Implementation of TestCOMObj_Event01().

5.1 Run the Console application from the COM server project side.

5.2 Put a breakpoint in the following code in Fire_Event01() :

hr = pConnection->Invoke(1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &params, &varResult, NULL, NULL);

5.3 When Invoke() returns, it will be observed that hr always equals E_INVALIDARG.

6. Non-Trivial Implementation of TestCOMObj_Event01().

6.1 If we insert a more sophisticated implementation for TestCOMObj_Event01(), array modification say, as follows :

private static void TestCOMObj_Event01(ref Array intArray)
{
    for (int i = 0; i < intArray.Length; i++)
    {
        intArray.SetValue((object)(i + 10), i);
    }
}

In the above code, we attempt to modify each element of intArray by incrementing each by 10.

6.2 At the end of TestCOMObj_Event01(), the elements of intArray are indeed changed as expected.

6.3 However, when control reaches back to the server, hr remains set to E_INVALIDARG and the original SAFEARRAY remains unchanged.

7. Some Possible Explanations.

7.1 After doing some research, I managed to find some interesting information about a ComEventsSink class from the Microsoft Reference Source Site.

7.2 The ComEventsSink class implements the COM IDispatch interface and seems likely to be the base class for handling COM dispinterface-based events.

7.3 In fact, when I examined its Invoke() method, the following code appears at the end :

// Now we need to marshal all the byrefs back
for (i = 0; i < pDispParams.cArgs; i++) {
    int idxToPos = byrefsMap[i];
    if (idxToPos == -1)
        continue;
 
    GetVariant(&pvars[idxToPos])->CopyFromIndirect(args[i]);
}

Here, the code searches for the VARIANTs in pDispParams which have been passed by reference and attempts to update its original value.

7.4 Looking inside the Variant::CopyFromIndirect() method, we see that there is a switch statement in which the Variant Type is evaluated and we see that if the vt is VT_ARRAY, the default part takes effect :

switch (vt) {
    case VarEnum.VT_I1:
        *(sbyte*)this._typeUnion._unionTypes._byref = (sbyte)value;
        break;             
        ...
        ...
        ... 
    default:
        throw new ArgumentException("invalid argument type");
}

7.5 The default part certainly applies in our Event01() case where the parameter is a reference to a SAFEARRAY. And so an ArgumentException is thrown. This exception will likely cause a HRESULT return value of E_INVALIDARG.

7.6 The reason for the lack of support for referenced SAFEARRAY parameters is not stated. But I personally think this is probably due to over-complexity.

7.7 At this time, it is still not conclusive as to whether the findings on the ComEventsSink class applies to our current problem.

7.8 I have also disassembled the interop assembly of the unmanaged test COM server using ILDASM.exe.

7.9 In summary, I observed that an instance of the _ITestCOMClassEvents_SinkHelper class is passed to the IConnectionPoint implementation of the COM server. This is done inside the _ITestCOMClassEvents_EventProvider class :

IL_0019:  ldarg      0
IL_001d:  call       instance void Interop.TestCOMServer._ITestCOMClassEvents_EventProvider::Init()
IL_0022:  newobj     instance void Interop.TestCOMServer._ITestCOMClassEvents_SinkHelper::.ctor()
IL_0027:  stloc.0
IL_0028:  ldc.i4.0
IL_0029:  stloc.1
IL_002a:  ldarg      0
IL_002e:  ldfld      class [mscorlib]System.Runtime.InteropServices.ComTypes.IConnectionPoint Interop.TestCOMServer._ITestCOMClassEvents_EventProvider::m_ConnectionPoint
IL_0033:  ldloc.0
IL_0034:  castclass  [mscorlib]System.Object
IL_0039:  ldloca.s   V_1
IL_003b:  callvirt   instance void [mscorlib]System.Runtime.InteropServices.ComTypes.IConnectionPoint::Advise(object,
                                                                                                                    int32&)

7.10 Hence it stands to reason that when the COM server calls IDispatch::Invoke(), the _ITestCOMClassEvents_SinkHelper instance gets called through some ComVisible method that has the same signature as IDispatch::Invoke().

7.11 However, it is currently not clear to me how this happens, since the SinkHelper class does not by itself derive from the ComEventsSink class.

.class public auto ansi sealed Interop.TestCOMServer._ITestCOMClassEvents_SinkHelper
       extends [mscorlib]System.Object
       implements Interop.TestCOMServer._ITestCOMClassEvents
{
...
...
...
}

7.12 Neither does it hold any ComEventsSink variable.

7.13 Furthermore, when the SinkHelper’s Event01() is called, the following is executed :

  .method public virtual instance void  Event01(int32[]& A_1) cil managed
  {
    // Code size       34 (0x22)
    .maxstack  2
    IL_0000:  ldarg      0
    IL_0004:  ldfld      class Interop.TestCOMServer._ITestCOMClassEvents_Event01EventHandler Interop.TestCOMServer._ITestCOMClassEvents_SinkHelper::m_Event01Delegate
    IL_0009:  brfalse    IL_0021

    IL_000e:  ldarg      0
    IL_0012:  ldfld      class Interop.TestCOMServer._ITestCOMClassEvents_Event01EventHandler Interop.TestCOMServer._ITestCOMClassEvents_SinkHelper::m_Event01Delegate
    IL_0017:  ldarg      A_1
    IL_001b:  callvirt   instance void Interop.TestCOMServer._ITestCOMClassEvents_Event01EventHandler::Invoke(int32[]&)
    IL_0020:  ret

    IL_0021:  ret
  } // end of method _ITestCOMClassEvents_SinkHelper::Event01

Here, the Event01 delegate’s Invoke() method is called. No hint of any use of the ComEventsSink class.

7.14 Hence, it is not clear at all to me where and how an actual IDispatch::Invoke() on the managed side gets called.

8. In Conclusion.

8.1 I have contacted Microsoft Connect and reported this case : Microsoft Connect. and I hope to receive word from them soon.

8.2 Meantime, I am doing some experiments on a possible workaround for this problem and will present it in Part 2.

8.3 I will definitely update all readers if Microsoft reverts and sheds some light on the matter.

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.

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: