【问题标题】:Pass Interface Array from C# COM Server to C++ Via IDispatch通过 IDispatch 将接口数组从 C# COM 服务器传递到 C++
【发布时间】:2021-07-10 12:29:51
【问题描述】:

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

当我尝试使用早期绑定(例如通过this answer 中提供的代码)执行此操作时,它可以正常工作。但是,一旦我尝试通过 IDispatch 装饰我的类型以使用后期绑定,请求就会失败,并显示 0x80020005:类型不匹配。其他非接口类型可以通过 IDispatch 毫无问题地返回,它只是未能正确传输的 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; }
}

通过进行以下更改

  • [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] 添加到IMyRootClass
  • [ClassInterface(ClassInterfaceType.None)] 添加到MyRootClass

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

main
_com_dispatch_method
_com_invoke_helper
VariantChangeType
VariantChangeTypeEx

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

【问题讨论】:

  • 你想要后期绑定,现在你有了:-) #import 将生成现在期望 GetEntities 返回 IDispatch(对象)数组的代码。将 IEmailEntity[] GetEntities() 更改为 object[] GetEntities() 并在最后一行将 'list.ToArray()' 更改为 'list.Cast().ToArray()' 或不要使用 Ptr 后缀接口
  • 你解决了!我之前尝试返回一个 object[] 而不是 IWhatever[] 并且没有用,但是我这样做是协变的(即没有显式转换)。当我将返回类型转换为object“正确”时,这似乎可以解决问题。我可以在_com_invoke_helper 的反汇编中看到,它仅根据对返回值的某种检查调用VariantChangeType,所以我想那里一定有一些元数据说“这不是真正的object大批”。如果您发布需要以非协变量方式返回object[],我可以接受它作为答案

标签: c# c++ com


【解决方案1】:

请记住,IDispatch / 后期绑定是为特殊客户端(例如: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++ 标头级别结束:

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 / 早期绑定接口周围的一个小包装代码:

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++ 标头级别结束:

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 类型(它是同时为这些客户发明的),按原样使用或与包装器一起使用。

这就是您看到预期返回类型为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 等客户端更受限制。

【讨论】:

    猜你喜欢
    • 2014-02-19
    • 1970-01-01
    • 2012-07-13
    • 2010-11-16
    • 2011-12-25
    • 1970-01-01
    • 2015-04-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多