编组IDispatch时,COM Interop灾难性故障(0x8000FFFF)

时间:2012-08-08 06:19:55

标签: c# .net com exception-handling marshalling

我正在为Outlook 2010开发一个IM提供程序。为了做到这一点,我必须实现一些IMessenger * COM接口,在启动时加载Outlook。我想使用C#和一个out-of-proc服务器,我遵循MSDN示例here

这是我已经取得的成就:

  • 正确注册所有内容(CLSID,类型库等)并配置Outlook,以便它在启动时尝试加载我的服务器
  • 将COM服务器实现为本机ATL / C ++服务器并确保其正常工作
  • 在C#/ .NET中实现基本界面并确保已加载

现在,我基本上可以启动我的.EXE服务器,然后启动outlook并观察我的服务器的控制台窗口,同时它显示来自COM子系统的所有方法调用(我已经为每个方法添加了日志记录)。问题是,在某些时候(仅在.NET实现中!)我在Outlook日志中遇到了这样的错误:

CMsoIMProviderOC20::HrEnsureServiceData !failed!  Line: 402  hr = 0x8000FFFF

我确切地知道在我的程序中发生了这种情况,但我无法做任何事情(我已经尝试了几天找到解决方案)。请记住,如果我使用C ++ / ATL实现它实际上是有效的,所以我想它必须与.NET Interop封送工作方式有关。

以下是详细信息:

基本上有3个与此问题相关的界面:IMessenger,IMessengerServices和IMessengerService:

[
    uuid(D50C3186-0F89-48f8-B204-3604629DEE10), // IID_IMessenger
    helpstring("Messenger Interface"),
    helpcontext(0x0000),
    dual,
    oleautomation
]
interface IMessenger : IDispatch
{
    ...
    [id(DISPID_MUAM_SERVICES), propget, helpstring("Returns services list."), helpcontext(0x0000)]
    HRESULT Services([out, retval] IDispatch ** ppdispServices);
    ...
}

虽然我知道这个Services属性实际上应该返回这个接口:

[
 uuid(2E50547B-A8AA-4f60-B57E-1F414711007B), // IID_IMessengerServices
 helpstring("Messenger Services Interface"),
 helpcontext(0x0000),
 dual,
 oleautomation
]
interface IMessengerServices : IDispatch
{
    ...
    [id(DISPID_NEWENUM), propget, restricted, helpstring("Enumerates the services."), helpcontext(0x0000)]
    HRESULT _NewEnum([out, retval] IUnknown **ppUnknown);
    ...
}

这个接口反过来会返回IMesengerService接口。但是,这个问题并不重要。

在C ++ / ATL中,我实现了像这样的属性:

STDMETHOD(get_Services)(IDispatch ** ppdispServices)
{
    (*ppdispServices) = (IDispatch*)new CComObject<CMessengerServices>();
    (*ppdispServices)->AddRef();
    return S_OK;
}

这是C#实现:

public object Services
{
    get { return new MessengerServices(); }
}

直到这里,在这两个实现中一切都还可以!现在问题开始......

首先奇怪的是,在C ++ / ATL中,我的CMessengerServices类的函数get__NewEnum(IUnknown **pUnkown)将永远不会被执行。而是调用其他函数。 这很好。

但是在C#中,我调用的MessengerServices类的第一个成员是GetEnumerator()方法。我可以在那里设置断点并清楚地看到它是C#代码中的最后一行(在我的控制下),它在Outlook停止启动之前执行并在其日志中报告0x8000FFFF错误。之后,我的.EXE服务器中没有其他代码行被调用。

MessengerServices类实现中最重要的部分是:

[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true), Guid("DF394E2C-38E2-4B70-B707-9749A4F857B0")]
public class MessengerServices : IMessengerServices
{
    IEnumerator IMessengerServices.GetEnumerator()
    {
        return messengerServices.GetEnumerator();
    }

    private readonly ArrayList messengerServices;

    public MessengerServices()
    {
        messengerServices = new ArrayList { new MessengerService() };
    }
    ...
}

这里是由tlbimp.exe生成的CCW代理:

[ComVisible(true), Guid("2E50547B-A8AA-4F60-B57E-1F414711007B")]
public interface IMessengerServices : IEnumerable
{
    [TypeLibFunc(TypeLibFuncFlags.FRestricted)]
    [DispId(-4)]
    [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalType = "System.Runtime.InteropServices.CustomMarshalers.EnumeratorToEnumVariantMarshaler, CustomMarshalers, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
    new IEnumerator GetEnumerator();
    ...
}

最后一个提示,如果这可能有帮助:当我在GetEnumerator()实现中设置断点并命中F10进行步骤时,这是Visual Studio的“输出”窗口中的最后一行:

  

单步执行非用户代码'System.Runtime.InteropServices.CustomMarshalers.EnumeratorToEnumVariantMarshaler.MarshalManagedToNative

另一个有趣的事情(至少对我而言)是如果我从Interface声明和我的类定义中完全删除了Enumerator,Outlook仍会调用我的接口,但使用另一种方法(PrimaryService for相反,但基本上有相同的错误。

老实说,我完全不知道如何处理这个错误?提前感谢您的任何帮助!

2 个答案:

答案 0 :(得分:4)

此类问题的标准诊断是您的[ComVisible]界面布局错误。这导致在客户端代码调用时执行错误的方法,比如IDispatch :: GetTypeInfoCount()。这是IMessengerServices v表中的第4个方法指针,您的GetEnumerator()方法是接口v表中的第4个方法。当它获得一个奇怪的返回值时,Outlook会失败,它不知道如何处理。

我不知道你从哪里获得了“CCW代理”,它看起来并不像Tlbimp.exe那样生成它。缺少的重要属性是[InterfaceType(ComInterfaceType.InterfaceIsDual)]。这就是告诉CLR它需要实现IDispatch的原因。所以你的界面缺少四种IDispatch方法。

除了修复接口声明之外,到目前为止,解决此问题的最佳方法是从类型库导入接口定义,以便永远不会出现不匹配。我没有在我的机器上安装它,你可以通过查看HKCR\CLSID\{2E50547B-A8AA-4F60-B57E-1F414711007B}的注册表找到它。

答案 1 :(得分:3)

我现在找到了解决问题的方法。 Hans Passants的回答让我走向了正确的方向(谢谢!),然而,问题在于以下句子是真的:

Tlbimp.exe不像最初在IDL / TLB中定义的那样保留vtable中方法的顺序。相反,方法和属性按字母顺序重新排序!

因此,当您使用Visual Studio实现接口并引用类型库时,可能会破坏CCW创建的vtable。解决方案是将Tlbimp(由Visual Studio生成的工具生成interop程序集)生成的包装器复制到您自己的代码中,并将方法和属性重新排序为与类型库中的顺序相同。