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

COM 邮箱 -为 VB 设计异步 COM 组件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2002年4月11日

CPOL

11分钟阅读

viewsIcon

107636

downloadIcon

1087

此示例 COM 组件提供了 3 个 COM 对象,用于使用 Win32 邮件槽 IPC 机制。如果您需要从 VB 使用邮件槽进行通信,该组件可能很有用。

概述

此示例 COM 组件提供了三个 COM 对象,用于使用 Win32 邮件槽 IPC 机制。如果您需要从 VB 使用邮件槽进行通信,该组件可能很有用。然而,我编写它的原因是演示如何创建 C++ 中的 COM 组件,该组件可以与 VB 很好地集成并可以触发异步事件。

COM 组件包含一个对象工厂,用于创建邮件槽操作对象的实例。有三个邮件槽对象:一个 ClientMailsot 对象,它提供邮件槽连接的“写入”端;一个同步的 ServerMailslot 对象,它提供邮件槽连接的“读取”端,但需要轮询才能接收数据;以及一个异步的 AsyncServerMailslot 对象,它通过触发事件来发出数据到达的信号。首先,我们将看看组件的对象模型,因为它揭示了使 VB 轻松使用这些对象的一些技巧。然后,我们将研究实现,并解决当组件可以异步运行时出现的线程问题。

接口设计:对象创建

工厂对象之所以存在,是因为您可以将创建和配置邮件槽对象作为一个单步过程。COM 对象不能拥有 C++ 构造函数的等效项,因此如果没有工厂对象,您将需要创建邮件槽对象然后对其进行配置。如果您忽略配置对象,或者尝试配置它但配置失败,您可能会得到一个存在于您的程序中但无用的对象。通过将对象的创建和配置包装成一个单一的步骤,可以更好地防止创建这些“僵尸”对象。如果成功,您将获得一个已正确配置并可运行的对象;如果失败,则您根本不会获得任何对象。

工厂对象接口 IDL 如下所示

   interface IMailslotFactory : IDispatch
   {
      HRESULT CreateClientMailslot(
         [in] BSTR name, 
         [in, optional] VARIANT computerOrDomain, 
         [out, retval] IClientMailslot **ppSlot);

      HRESULT CreateServerMailslot(
         [in] BSTR name,
         [in, optional] VARIANT maxMessageSize,
         [in, optional] VARIANT readTimeOut,
         [out, retval] IServerMailslot **ppSlot);

      HRESULT CreateAsyncServerMailslot(
         [in] BSTR name, 
         [in, optional] VARIANT maxMessageSize, 
         [out, retval] IAsyncServerMailslot **ppSlot);
   };

工厂对象本身被标记为 appobject,这意味着这些方法可以在 VB 中使用,而无需显式指定对象引用,从而可以编写如下代码

   Dim slot As JBCOMMAILSLOTLib.ClientMailslot

   Set slot = CreateClientMailslot("MySlot")

每个方法都会创建并配置相应的邮件槽对象。如果配置失败,则不返回任何对象,并会引发错误。

在内部,对象工厂通过创建 COM 对象实例来工作,但请求的是“初始化”接口而不是正常的客户端接口。初始化接口不需要在 IDL 或类型库中公开,因为它仅供组件内部使用。

ClientMailslot 对象的初始化接口定义如下

   class __declspec(uuid("589E7114-50EE-4598-9140-92610D9BC20F")) IClientMailslotInit;

   class ATL_NO_VTABLE IClientMailslotInit : public IUnknown
   {
      public :

         STDMETHOD(Init)(
            /*[in]*/ BSTR name,
            /*[in]*/ VARIANT computerOrDomain) = 0;
   };

对象工厂将用户提供的参数传递给 ClientMailslot,后者可以尝试自行配置。失败将导致工厂销毁 ClientMailslot 并将错误返回给调用者。

如果对象初始化成功,工厂会查询 ClientMailslot 的客户端接口 IClientMailslot,并释放初始化接口。然后,ClientMailslot 将作为完全初始化并可运行的对象返回给调用者。

为防止用户直接创建对象而不是使用对象工厂,其他对象在 IDL 中被标记为 noncreatable,同时请注意 IDL 中没有提及初始化接口。

   [
      uuid(30A92485-94D2-4CBA-AC32-EF276B7F777B),
      helpstring("ClientMailslot Class"),
      noncreatable
   ]
   coclass ClientMailslot
   {
      [default] interface IClientMailslot;
   };

ServerMailslot 和 AsyncServerMailslot 以相同的方式由工厂创建。

接口设计:数据传输

数据可以作为字符串或字节数组发送。同样,数据也可以这两种格式接收。以一种格式发送并不意味着服务器必须以该格式接收数据。

ClientMailsot 接口的 IDL 如下所示

   interface IClientMailslot : IDispatch
   {
      HRESULT WriteString(
         [in] BSTR data);
		
      HRESULT Write(
         [in] VARIANT arrayOfBytes);
   };

这可以这样使用

   Private Sub SendString_Click()

      m_slot.WriteString MessageEdit.Text

   End Sub

   Private Sub SendBytes_Click()

      Dim stringLength As Integer
      stringLength = Len(MessageEdit.Text)

      Dim bytes() As Byte

      ReDim bytes(stringLength)

      Dim i As Integer

      For i = 0 To stringLength - 1
         bytes(i) = Asc(Mid(MessageEdit.Text, i + 1, 1))
      Next i

      m_slot.Write bytes

   End Sub

请注意,通过发送字符串,您实际上是在发送 Unicode 字符串:通过发送“AAAA”作为字符串,您实际上发送的是以下字节:0x65 0x00 0x65 0x00 0x65 0x00 0x65 0x00。

同步的 ServerMailsot 提供了相应的读取方法。当消息可用时,它可以按字节或字符串读取。但是,调用任一读取方法都会消耗当前消息。您不能调用 Read() 将消息读取为字节数组,然后调用 ReadString() 将同一消息读取为字符串,调用 ReadString() 将尝试读取下一条可用消息。如果您尝试读取消息但在此读取超时时间内没有可用消息,则会引发错误。因为读取调用是同步的,所以您的代码可能会在读取调用中阻塞读取超时时长。读取超时是在创建 ServerMailslot 时指定的,而不是按每次读取指定的。

异步的 AsyncServerMailslot 通过事件传递消息。事件接口的 IDL 如下所示

   dispinterface _IAsyncServerMailslotEvents
   {
      properties:
      methods:

         HRESULT OnDataRecieved(
            [in] IMailslotData *mailslotData);
   };

并在 VB 中可以这样处理事件

   Private Sub m_slot_OnDataRecieved(ByVal mailslotData As JBCOMMAILSLOTLib.IMailslotData)
      
      Dim stringData as String
      stringData = mailslotData.ReadString()

      ' do something with the string...    

      Dim bytes() As Byte
      bytes = mailslotData.Read()

      ' do something with the bytes

   End Sub

MailslotData 对象封装了一个邮件槽消息,不应在事件处理程序的作用域之外保留。如果您想保留数据,请将其提取为字符串或字节数组,并保留该表示形式。此限制是由于 AsyncMailslotServer 对象如何优化事件分派机制 - 每个 AsyncMailslotServer 只有一个 MailslotData 对象,并且它会被重用于到达的每条消息。

ServerMailslot 不同,您可以同时对 MailslotData 对象调用 ReadString()Read(),以在两种格式下接收相同的消息数据。

实现问题:CCOMMailslot 辅助对象

ClientMailslot 对象的实现非常简单。它只提供两个写入方法和内部初始化方法。所有实际工作都委托给一个辅助对象 CCOMMailslot,该对象处理所有邮件槽 COM 对象之间的通用代码。ClientMailslot 实际完成的唯一工作是从提供的字节数组中提取数据。

ServerMailslot 也同样直接。大多数方法由 CCOMMailslot 实现,只有 Read 方法需要在 ServerMailslot 对象本身内完成。Read()ReadString() 都调用 CCOMMailslot::Read(),然后将结果数据打包为 BSTR 或字节的 SafeArray

CCOMMailslot 完成的大部分工作仅仅是参数检查和封装 Win32 邮件槽 API。唯一稍微复杂一些的代码位于 Read() 方法中。邮件槽可以创建为具有最大消息大小,在这种情况下,我们知道读取操作所需的缓冲区大小;它们也可以创建为具有未指定消息大小,可接受任何大小的消息。如果邮件槽是使用最大消息大小创建的,那么我们只需分配一个足够大的缓冲区并将其用于每次读取。如果未指定最大值,那么我们首先调用 SizeOfWaitingMessage() 来查看是否有消息等待,如果有,则检索消息的大小。如果有消息等待,我们会根据需要扩大缓冲区大小,以便有足够的空间读取消息,然后读取消息。

实现问题:CAsyncCOMMailslot 辅助对象

毫不奇怪,最复杂的对象是异步的 AsyncServerMailslot。该对象是多线程的,并在消息到达邮件槽时生成事件。通常认为创建多线程 DLL 托管的 COM 组件是不明智的[1]。但是,此组件中创建的线程与 AsyncServerMailslot 对象的生命周期相关联,因此我们可以保证所有工作线程将在组件卸载之前停止。如果您对此感到担忧,可以将代码轻松地放入 EXE 组件中。我认为在这种情况下,拥有一个包含所有必需的代理/存根代码的单一 DLL 组件的便利性是值得的。

当创建一个 AsyncServerMailslot 时,它会生成一个工作线程,该线程对邮件槽句柄执行无限阻塞的、重叠的读取。工作线程会阻塞,等待读取完成或其关闭事件被发出信号。当读取完成时,接收到的数据会被封装在一个 MailslotData 对象中,并触发事件以通知客户端。

由于“COM 规则”,事件接收器必须从注册它的同一线程触发,或者必须将事件接收器接口封送(marshal)到将要触发事件的线程。由于 ATL 为我们生成连接点代码的方式,将每个事件接收器封送到工作线程以触发事件并不实用,因此我们选择从组件的主线程触发事件。要从组件的主线程触发事件,我们需要让工作线程与主线程通信,一种方法是使用窗口消息,另一种方法是将接口从主线程封送到工作线程。窗口消息方法在微软的一篇知识库文章[2]中有解释,但需要我们创建一个虚拟窗口并添加其他代码混乱;接口封送方法稍微复杂一些,但为我们提供了一些优势。

当一个对象需要以多线程方式运行并需要通过 COM 回调自身时,它应该使用 CoMarshalInterThreadInterfaceInStream() 将接口封送到工作线程。在工作线程内部,使用 CoGetInterfaceAndReleaseStream() 取消封送接口,并通过 COM 安全地与组件的主线程通信。这在组件需要卸载时会一直工作。不幸的是,通过在组件内部封送接口,您创建了一个循环引用。组件本质上持有自身的一个引用,这个未完成的引用将阻止组件被销毁。由于内部引用将一直持有直到工作线程关闭,而工作线程仅在组件被销毁时关闭,因此您可以看到我们遇到了问题。

实现问题:引用循环和弱身份

循环引用问题通常使用“分割身份”或“弱引用”惯用法[3]来解决。其思想是通过一个不影响对象引用计数的引用来打破引用循环,或者对象公开第二个身份(COM 对象),尽管它是主对象的一部分,但它不影响主对象的引用计数。这两种技术都允许主对象在所有外部引用释放后开始关闭。在 ATL 中实现弱引用相当复杂,尽管在 [3] 中提供了一个解决方案,但对于我们这里所需的内容来说,它过于复杂且具有侵入性。

我们解决工作中线程中持有接口所产生的引用循环问题的方法是为 AsyncServerMailslot 创建一个弱身份。这个弱身份支持工作线程和组件主线程之间通信所需的接口。弱身份本身就是一个简单的 COM 对象,它有自己的 AddRef()Release()QueryInterface() 实现,并且由于返回的 IUnknown 接口与主对象不同,它在 COM 中拥有自己的身份。

我们用于线程间通信的接口的 IDL 如下所示

   interface _AsyncServerEvent : IUnknown
   {
      HRESULT OnDataRecieved();
   };

本质上,这只是工作线程“触碰”主线程的一种方式。此接口出现在 IDL 文件中,因为虽然我们仅在内部使用此接口,并且没有任何公共可见对象公开该对象,但我们需要为其生成代理/存根代码。

我们需要的弱身份如下所示

   class CAsyncServerEventHelper : public _AsyncServerEvent
   {
      public :

         CAsyncServerEventHelper(_AsyncServerEvent &theInterface);

         STDMETHOD(OnDataRecieved)();

         // IUnknown methods
         ULONG STDMETHODCALLTYPE AddRef();
         ULONG STDMETHODCALLTYPE Release();
         STDMETHOD(QueryInterface(REFIID riid, PVOID *ppvObj));
 
      private :
 
         _AsyncServerEvent &m_interface;
   };

并按如下方式实现

   CAsyncServerEventHelper::CAsyncServerEventHelper(_AsyncServerEvent &theInterfce)
      :  m_interface(theInterfce)
   {
   } 

   STDMETHODIMP CAsyncServerEventHelper::OnDataRecieved()
   {
      return m_interface.OnDataRecieved();
   }

   ULONG STDMETHODCALLTYPE CAsyncServerEventHelper::AddRef()
   {
      return 2;
   }

   ULONG STDMETHODCALLTYPE CAsyncServerEventHelper::Release()
   {
      return 1;
   }

   STDMETHODIMP CAsyncServerEventHelper::QueryInterface(REFIID riid, PVOID *ppvObj)
   {
      if (riid == IID_IUnknown || riid == IID__AsyncServerEvent)
      {
         *ppvObj = this;
         AddRef();
         return S_OK;
      }
   
      return E_NOINTERFACE;
   }

我们主要的 COM 身份有一个类型为 CAsyncServerEventHelper 的成员变量,它在其构造函数中用指向自身的指针(this)进行初始化。然后,它将弱身份的 _AsyncServerEvent 接口封送到其工作线程,这会创建适当的代理,以便对接口的调用能够正确地跨线程封送,但它不会影响主身份的引用计数。

当工作线程从邮件槽读取数据时,它会调用已解封接口的 OnDataRecieved() 方法,这会导致调用被封送到组件的主线程,然后在那里,辅助对象将调用传递给主对象。在此示例中,我们不费力地在调用中传递任何数据,我们只是将其用作一个线程“戳”另一个线程的方式。工作线程读取数据到读取缓冲区并戳主线程,然后主线程使用刚刚读取的数据并触发所有连接客户端中的事件。乍一看这可能很危险,但我们使用的是主对象 STA apartment 的同步性来提供跨调用的同步。工作线程将阻塞直到主线程完成事件分派。

当然,这只是实现弱身份的一种方法,但它相对简单、不显眼且效果很好。

结论

遵循一些简单的接口设计规则,为 VB 设计易于集成的 COM 组件是相当直接的。

如果您遵循 COM 规则,了解何时创建引用循环,并知道如何打破它们,那么处理异步事件会很简单。

参考文献

修订历史

  • 2002年4月11日 - 初始修订。
© . All rights reserved.