65.9K
CodeProject 正在变化。 阅读更多。
Home

在非托管 C++ 中接收托管代码的事件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (11投票s)

2008年4月24日

CPOL

5分钟阅读

viewsIcon

95568

downloadIcon

1312

在托管代码中引发事件,并在非托管 C++ 中进行处理。

引言

随着时间的推移,我正在开发的一个大型项目中的某些部分正从原生 C++ 被迁移到 .NET。第一个被迁移的模块是一个较低级别的模块,它应该由一个非托管代码应用程序使用。这引发了一个有趣的问题。该低级别库使用互操作性将其包装在 COM 外壳中。由于该库处理异步 I/O,它可以引发事件。我找到的唯一一个适用于 C++ 代码的示例用法是 Microsoft 文章 如何:引发由 COM 接收器处理的事件 的扩展。完整文章不仅包含 VB6 接收器示例,还包含 C++ 接收器:COM 互操作:真正引发由 COM 接收器处理的事件

所有这些示例唯一的问题是,它们对我来说都不起作用。当调用委托时,ATL/C++ 示例会在托管 .NET 对象中抛出 `AccessViolationException`。控件从未到达 C++ 接收器中的处理函数。如果未处理,`E_POINTER` (0x80004003) 结果将作为 `E_POINTER` (0x80004003) 返回给 COM 客户端。

经过一周的痛苦和寻找解决方案以使委托生效,我向上述文章的作者 Jason Hunt 求助。提出的唯一解决方案是手动完成所有工作。这不像听起来那么难。

实现

让我们从实现开始。首先,我们来看看事件处理程序的接口。这个接口由 .NET 对象定义,必须由 COM 客户端实现。

[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IHehServerEvents
{
    void EmptyEvent();
}

该接口描述了一个由 .NET 组件引发的单一事件处理程序。为了简单起见,将 `InterfaceType` 设置为 `IUnknown`,因此 COM 客户端将拥有一个较小的接口需要实现。

我们继续讨论接口。可以公开服务器的实际实现,但最好只定义一个狭窄、定义良好的接口。

[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IHehServer
{
    void TestAddListener(IHehServerEvents evt);
    void TestEvent();
}

我们用 .NET 实现的 COM 对象将只公开两个方法。

  • `TestAddListener` - 允许为事件添加一个侦听器。
  • `TestEvent` - 启动事件。

现在,重头戏来了,引发事件的类的实际实现。

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class HehServer : IHehServer
{
    public List<IHehServerEvents> m_listeners = new List<IHehServerEvents>();

    public void TestEvent()
    {
        foreach (IHehServerEvents evt in m_listeners)
        {
            evt.EmptyEvent();
        }
    }

    public void TestAddListener(IHehServerEvents evt)
    {
        m_listeners.Add(evt);
    }
}

首先,接口类型通过将 `ClassInterfaceAttribute` 设置为 `ClassInterfaceType.None` 来隐藏服务器的实现。客户端将仅通过实现的接口 `IHehServer` 来使用它。请注意,没有任何迹象表明此实现会像其他所有使用委托的示例那样引发任何类型的事件:`ComSourceInterfacesAttributes` 被省略了。

实现维护着一个订阅了该事件的侦听器列表。使用 `TestAddListener` 订阅事件只是将侦听器添加到列表中。这样,当调用事件时,实现只需遍历列表并调用每个侦听器。

构建时,不要忘记将组件编译为对 COM 可见的 DLL。在 .NET 项目属性中,将“输出类型”设置为“类库”,然后输入“程序集信息”,并选择“使程序集 COM 可见”。我还使用“生成”选项卡中的“为 COM 互操作注册”选项将组件更紧密地集成到 Visual Studio 中。

现在,让我们使用实现的类。我们要转到“黑暗面”:C++。其中一个头文件应包含对已编译的 `TLB` 文件的引用。

#import "..\comserver\bin\Debug\comserver.tlb" \
    raw_interfaces_only, named_guids, no_namespace

这是类声明。

class CClientTest : public IHehServerEvents
{
private:
    IHehServerPtr m_server;
    int m_EmptyEventTester;
public:
    CClientTest(void);
    ~CClientTest(void);
    void TestEvent(void);
    HRESULT __stdcall QueryInterface(const IID &, void **);
    ULONG __stdcall AddRef(void) { return 1; }
    ULONG __stdcall Release(void) { return 1; }
    HRESULT __stdcall EmptyEvent(void);
};

首先,该类继承自事件接口 `IHehServerEvents`,使其成为一个侦听器。从 C++ 编译器的角度来看,由于 `IHehServerEvents` 有一个 `IUnknown` 父级,因此我们必须实现 `QueryInterface`、`AddRef` 和 `Release` 方法。此类不是一个真正的 COM 对象,不需要计算引用计数,因此只需为添加和删除对象的引用返回一个虚拟值就足够了。唯一需要“真正”实现的方法是 `QueryInterface`。当然,此类应实现每个事件处理方法。

请注意,此处每个方法都使用 `__stdcall` 属性进行声明,因为 COM 就是这样工作的。这里可以使用 ATL 的 `STDMETHOD/STDMETHODIMP` 宏。

类的实现相当简单。首先,我们需要实例化一个引发事件的 COM 对象。

CClientTest::CClientTest(void)
{
    HRESULT hr = S_OK;
    hr = m_server.CreateInstance(__uuidof(HehServer));
    ATLASSERT(SUCCEEDED(hr));
}

此处使用了 .NET 提供的库。`IHehServerPtr` 具有允许创建底层类相关实例的方法。请注意,创建的类的 GUID 是实现的标识,而不是接口的标识。

使用此类也非常简单。

void CClientTest::TestEvent(void)
{
    m_server->TestAddListener(this);
    m_EmptyEventTester = 0;
    m_server->TestEvent();
    ATLASSERT(m_EmptyEventTester != 0);
}

首先,该类订阅了 .NET 组件的事件。然后,调用 `TestEvent` 方法。事件处理程序的唯一作用是更改用于测试的标志的值。

HRESULT CClientTest::EmptyEvent(void)
{
    m_EmptyEventTester = 1;
    return S_OK;
}

最后但同样重要的是,`TestAddListener` 接受一个 `IHehServerEvents` 参数,而我们的类是不同类型的。由于我们在 COM 世界中工作,简单的类型转换是不行的,应该使用 `QueryInterface`。

HRESULT CClientTest::QueryInterface(const IID & iid,void ** pp)
{
    if (iid == __uuidof(IHehServerEvents) || iid == __uuidof(IUnknown))
    {
        *pp = this;
        AddRef();
        return S_OK;
    }
    return E_NOINTERFACE;
}

瞧!我们收到了来自 COM 代码的回调。

关注点

当然,实际的实现应该包括错误检查和取消订阅事件的选项,但在理解了这些原理后,这是一项相当简单的任务。如果我错了,请留下评论,我会看看该怎么做。这个练习进一步证明了你不能总是依赖编译器和平台提供的工具。有时,你必须自己动手完成工作。

致谢

感谢 Jason Hunt (Noticably Different) 在撰写本文时提供的想法和帮助。

© . All rights reserved.