通过 IDispatch 将接口数组从 C# COM 服务器传递到 C++

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

我正在尝试开发一个客户端只能通过 IDispatch 访问的 C# COM 服务器。我的 COM 对象的一些方法 return 其他 COM 对象的接口数组。

当我尝试使用早期绑定(例如通过 this answer 中提供的代码)执行此操作时,它没有任何问题。但是,一旦我尝试装饰我的类型以通过 IDispatch 使用后期绑定,请求就会失败并显示 0x80020005:类型不匹配。其他非接口类型可以通过 IDispatch returned 没有问题,只是 COM 接口无法正确传输。

给定以下 C++ 客户端

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

以下代码适用于早期绑定

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

通过进行以下更改

我认为这现在应该可以通过 IDispatch 工作,但我却收到了上面提到的类型不匹配错误。我花了一个下午调试错误的确切来源。对 .NET Framework 的 Dispatch 调用似乎成功,returning 类型 VT_ARRAY | VT_DISPATCH (2009) 的 VARIANT,这是预期的,但是最终结果的验证似乎失败了低于

main
_com_dispatch_method
_com_invoke_helper
VariantChangeType
VariantChangeTypeEx

我查看了 oleaut32.dll 中的反汇编,在这个阶段我只是得出结论,这一定是 Windows 中的错误。有没有人可能有其他建议?

请记住,IDispatch / late-binding 是为特殊客户端创建的(例如:VB/VBA/VBScript/JScript),从纯 C/C++ 客户端使用总是很痛苦.

根据原始定义,这里是 IMyRootClass 的定义方式(您可以在 RegAsm 生成的 .tlb 文件上使用 Windows SDK 中的 OleView tool 读取它) :

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

这将在 #import 之后结束,在 C/C++ header 级别:

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

    SAFEARRAY * GetEntities ( );

    //
    // Raw methods provided by interface
    //

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

其中 GetEntities 实际上只是 IUnknown / early-binding 接口的一个小包装代码:

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

这就是“双重”界面很好的原因(不确定您为什么只想要 IDispatch),因为它们允许轻松访问两个世界。

现在,如果您更改问题中的定义,则不再定义 IMyRootClass COM 接口。相反,您只会在 IDL 级别获得 dispinterface

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

这将在 #import 之后结束,在 C/C++ header 级别:

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

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

其中 GetEntities 实际上是一个完全不同的包装代码:

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

正如您在此处看到的,由于我们使用的是 IDispatch,所有内容都或多或少地接近 VARIANT 类型(同时为这些客户发明),按原样使用或带包装纸。

这就是为什么您会看到预期的 return 类型是 VT_ARRAY|VT_DISPATCH。也可以使用 VT_ARRAY|VT_UNKNOWN,或 VT_ARRAY|VT_VARIANT,或简单的 VT_VARIANT(最终包装器类型),但没有办法说 VT_ARRAY of IEmailEntity*

所以,这个问题至少有两个解决方案:

1 - 你可以这样定义你的接口:

[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 - 或者您可以像这样“手动”使用 IDispatch 界面(不要使用包装器):

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

在这种情况下,结果将是 VT_ARRAY | VT_UNKNOWN(与第一种情况一样),这就是包装器抛出异常的原因。 comdef.h 的包装器在其自动化类型支持方面比 VB.

等客户端更受限制