通过 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; }
}
通过进行以下更改
- 将
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
添加到 IMyRootClass
- 将
[ClassInterface(ClassInterfaceType.None)]
添加到 MyRootClass
我认为这现在应该可以通过 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.
等客户端更受限制
我正在尝试开发一个客户端只能通过 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; }
}
通过进行以下更改
- 将
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
添加到IMyRootClass
- 将
[ClassInterface(ClassInterfaceType.None)]
添加到MyRootClass
我认为这现在应该可以通过 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.