Pass Interface Array from C# COM Server to C++ Via IDispatch

1

I am trying to develop a C# COM server that is accessible to clients only via IDispatch. Some of the methods of my COM objects return an array of interfaces to other COM objects.

When I attempt to do this using early binding, such as via the code provided in this answer, it works with no issues. As soon as I try and decorate my types to use late binding via IDispatch however, the request fails with 0x80020005: Type mismatch. Other non-interface types can be returned via IDispatch without issue, it's just COM interfaces that are failing to be transmitted properly.

Given the following C++ client

CoInitialize(NULL);
IMyRootClassPtr ptr(__uuidof(MyRootClass));

try
{
    auto entities = ptr->GetEntities();
}
catch (_com_error &err)
{
    wprintf(L"The server throws the error: %s\n", err.ErrorMessage());

    wprintf(L"Description: %s\n", (PCWSTR)err.Description());
}

The following code works with early binding

C#:

[ComVisible(true)]
public interface IMyRootClass
{
    IEmailEntity[] GetEntities();
}

[ComVisible(true)]
public class MyRootClass : IMyRootClass // some class to start with
{
    public IEmailEntity[] GetEntities()
    {
        List<IEmailEntity> list = new List<IEmailEntity>();
        for (int i = 0; i < 10; i++)
        {
            EmailEntity entity = new EmailEntity();
            entity.Body = "hello world " + i;
            list.Add(entity);
        }
        return list.ToArray();
    }
}

[ComVisible(true)]
public interface IEmailEntity
{
    string Body { get; set; }
}

public class EmailEntity : IEmailEntity
{
    public string Body { get; set; }
}

By making the following changes

  • Add [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] to IMyRootClass
  • Add [ClassInterface(ClassInterfaceType.None)] to MyRootClass

I would think this should now work via IDispatch, but instead I get the Type mismatch error referred to above. I've spent the afternoon debugging into where exactly the error is coming from. The Dispatch call to the .NET Framework appears to succeed, returning a VARIANT of type VT_ARRAY | VT_DISPATCH (2009), which is expected, however the validation of the final result appears to fall down under

main
_com_dispatch_method
_com_invoke_helper
VariantChangeType
VariantChangeTypeEx

I've had a look at the disassembly of where its falling down in oleaut32.dll and at this stage I'm just concluding this must be a bug in Windows. Does anyone potentially have an advice otherwise?

c#
c++
com
asked on Stack Overflow Apr 15, 2021 by lordmilko • edited Apr 15, 2021 by lordmilko

1 Answer

0

Remember that IDispatch / late-binding was created for special clients (eg: VB/VBA/VBScript/JScript), and it's always a pain to use from pure C/C++ clients.

With the original definition, here is how the IMyRootClass is defined (you can read that using the OleView tool from the Windows SDK on the .tlb file generated by RegAsm):

interface IMyRootClass : IDispatch {
    [id(0x60020000)]
    HRESULT GetEntities([out, retval] SAFEARRAY(IEmailEntity*)* pRetVal);
};

Which will end up after #import as this, at C/C++ header level:

IMyRootClass : IDispatch
{
    //
    // Wrapper methods for error-handling
    //

    SAFEARRAY * GetEntities ( );

    //
    // Raw methods provided by interface
    //

      virtual HRESULT __stdcall raw_GetEntities (
        /*[out,retval]*/ SAFEARRAY * * pRetVal ) = 0;
};

where GetEntities is in fact just a small wrapper code around the IUnknown / early-binding interface:

inline SAFEARRAY * IMyRootClass::GetEntities ( ) {
    SAFEARRAY * _result = 0;
    HRESULT _hr = raw_GetEntities(&_result);
    if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
    return _result;
}

This is why "dual" interface are nice (not sure why you only want IDispatch), because they allow both worlds an easy access.

Now, if you change the definition as in your question, there's no more an IMyRootClass COM interface defined. Instead you'll only get a dispinterface at IDL level:

dispinterface IMyRootClass {
    properties:
    methods:
        [id(0x60020000)]
        SAFEARRAY(IEmailEntity*) GetEntities();
};

Which will end up after #import as this, at C/C++ header level:

IMyRootClass : IDispatch
{
    //
    // Wrapper methods for error-handling
    //

    // Methods:
    SAFEARRAY * GetEntities ( );
};

where GetEntities is in fact a quite different wrapper code:

inline SAFEARRAY * IMyRootClass::GetEntities ( ) {
    SAFEARRAY * _result = 0;
    _com_dispatch_method(this, 0x60020000, DISPATCH_METHOD, VT_ARRAY|VT_DISPATCH, (void*)&_result, NULL);
    return _result;
}

As you see here, since we're using IDispatch, everything is more or less close to the VARIANT type (which was invented at the same time, again for these clients), used as is or with wrappers.

This is why you see the expected return type is VT_ARRAY|VT_DISPATCH. One can also use VT_ARRAY|VT_UNKNOWN, or VT_ARRAY|VT_VARIANT, or simpy VT_VARIANT (the ultimate wrapper type), but there's no way to say VT_ARRAY of IEmailEntity*.

So, there are at least two solutions to this problem:

1 - you can do define your interface as this:

[ComVisible(true), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IMyRootClass
{
    object[] GetEntities();
}

[ComVisible(true), ClassInterface(ClassInterfaceType.None)]
public class MyRootClass : IMyRootClass
{
    public object[] GetEntities()
    {
        ...
        // the Cast forces the creation of an array of object => VT_ARRAY | VT_DISPATCH
        return list.Cast<object>().ToArray();
    }
}   

2 - or you can use the IDispatch interface "manually" (don't use the wrappers) like this:

IMyRootClassPtr ptr(__uuidof(MyRootClass));
CComVariant result;
DISPPARAMS p = {};
ptr->Invoke(0x60020000, IID_NULL, 0, DISPATCH_METHOD, &p, &result, nullptr, nullptr);

Where in this case, result will be of VT_ARRAY | VT_UNKNOWN (as in the first case), which is why the wrapper throws an exception. comdef.h's wrappers are more limited, in their automation types support, than clients such as VB.

answered on Stack Overflow Apr 16, 2021 by Simon Mourier

User contributions licensed under CC BY-SA 3.0