4

我正在为 Outlook 2010 开发 IM 提供程序。为此,我必须实现 Outlook 在启动时加载的一些 IMessenger* COM 接口。我想使用 C# 和一个进程外服务器来执行此操作,我在此处遵循了 MSDN 示例。

这是我已经实现的:

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

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

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

我确切地知道这在我的程序中发生的位置,但我对此无能为力(我已经尝试为此寻找解决方案好几天了)。请记住,如果我使用 C++/ATL 实现它,它实际上可以工作,所以我想它一定与 .NET 互操作封送处理的工作方式有关。

以下是详细信息:

基本上有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 中get__NewEnum(IUnknown **pUnkown),我的 CMessengerServices 类的函数永远不会被执行。而是调用其他函数。这很好。

然而,在 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

另一个有趣的事情(至少对我来说)是,如果我从接口声明和我的类定义中完全删除 Enumerator 东西,Outlook 仍然会调用我的接口,但使用另一种方法(为了完整起见,PrimaryService)但基本上有同样的错误。

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

4

2 回答 2

4

此类故障的标准诊断是您的 [ComVisible] 界面布局错误。当客户端代码调用 IDispatch::GetTypeInfoCount() 时,这会导致执行错误的方法。这是 IMessengerServices v-table 中的第 4 个方法指针,您的 GetEnumerator() 方法是您接口的 v-table 中的第 4 个方法。当 Outlook 收到一个不知道如何处理的奇怪返回值时,它就会崩溃。

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

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

于 2012-08-08T11:20:08.767 回答
3

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

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

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

于 2012-08-09T08:08:03.453 回答