Designing asynchonous COM components for VB

This example COM component provides three COM objects for using the Win32 Mailslot IPC mechanism. The component may be useful if you need to communicate from VB using Mailslots. However, the reason I wrote it was to demonstrate creating a COM component in C++ that integrates well with VB and can fire asynchronous events.

The COM component consists of an object factory which is used to create instances of the Mailslot manipulation objects. There are three Mailslot objects: a ClientMailsot object which provides the 'write' end of a Mailslot connection; a synchronous ServerMailslot object which provides the 'read' end of a Mailslot connection but which needs to be polled to receive data; and an asynchronous AsyncServerMailslot object which signals the arrival of data by firing an event. First we will take a look at the object model for the component as this reveals several tricks that make it easy to use the objects from within VB. Then we take a look at the implementation and address the threading issues that arise when a component can operate asynchronously.

Interface design: Object creation
The factory object exists so that you can create and configure the mailslot objects as a one step process. COM objects cannot have the equivalent of C++ constructors so without a factory object you would have to create the mailslot object and then configure it. If you neglected to configure the object, or if you attempted to configure it but the configuration failed, you could end up with an object which exists within your program but is useless. It's far better to prevent the creation of these zombie objects by wrapping the creation and configuration of an object into a single step. If this succeeds then you have your object and it's correctly configured and operational, if it fails then you never get given an object.

The factory object's interface IDL looks something like this:

   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);

The factory object itself is marked as 'appobject' which means that these methods are available for use within VB without specifying an object reference explicitly, allowing code such as this to be written:

   Dim slot As JBCOMMAILSLOTLib.ClientMailslot
   Set slot = CreateClientMailslot("MySlot")

Each method creates and configures the corresponding mailslot object. If the configuration fails then no object is returned and an error is raised.

Internally the object factory works by creating an instance of the COM object required but requesting an 'initialisation' interface rather than the normal client facing interface. The initialisation interface doesn't need to be exposed in the IDL or type library as it's only for internal use within the component.

The initialisation interface for the ClientMailslot object is defined as follows:

   class __declspec(uuid("589E7114-50EE-4598-9140-92610D9BC20F")) IClientMailslotInit;
   class ATL_NO_VTABLE IClientMailslotInit : public IUnknown
      public :
            /*[in]*/ BSTR name,
            /*[in]*/ VARIANT computerOrDomain) = 0;

The object factory passes the user supplied parameters through to the ClientMailslot which can attempt to configure itself. Failure results in the factory destroying the ClientMailslot and returning the error to the caller.

If object initialisation succeeds the factory queries the ClientMailslot for its client facing interface, IClientMailslot, and releases the initialisation interface. The ClientMailslot is then returned to the caller as a completely initialised and operational object.

To prevent the user creating an object directly, rather than using the object factory, the other objects are marked as noncreatable in their IDL, also notice that the IDL doesn't mention the initialisation interface.

      helpstring("ClientMailslot Class"),
   coclass ClientMailslot
      [default] interface IClientMailslot;

The ServerMailslot and AsyncServerMailslot are created by the factory in the same manner.

Interface design: Data transmission
Data can be sent either as a string or as an array of bytes. Likewise, data can be receieved in either format. Sending in one format does not prescribe how the server can receive the data.

The IDL for the ClientMailsot interface is something like this:

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

This can be used as follows:

   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

Note that by sending a string you are actually sending the Unicode string: by sending "AAAA" as a string you actually send the follwoing bytes: 0x65 0x00 0x65 0x00 0x65 0x00 0x65 0x00.

The sychronous ServerMailsot provides corresponding read methods. When a message is available it can be read either as bytes or as a string. However, calling either of the read methods will consume the current message. You cannot call Read() to read a message as an array of bytes and then ReadString() to read the same message as a string, the call to ReadString() will attempt to read the next available message. If you attempt to read a message and there is not one available within the read timeout period then an error is raised. Because the read call is synchronous your code could block in the read call for the length of the read timeout period. The read timeout is specified when you create the ServerMailslot and not on a per read basis.

The asynchronous AsyncServerMailslot delivers messages when they arrive via an event. The IDL for the event interface looks something like this:

   dispinterface _IAsyncServerMailslotEvents
         HRESULT OnDataRecieved(
            [in] IMailslotData *mailslotData);

and the event can be handled in VB like this:

   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

The MailslotData object encapsulates a single mailslot message and should not be retained outside scope of the event handler. If you want to keep the data, extract it as either a string or an array of bytes and keep that representation. This limitation is due to how the AsyncMailslotServer object optimises the event dispatch mechanism - only one MailslotData object exists per AsyncMailslotServer and it is reused for each message that arrives.

Unlike the ServerMailslot you can call both ReadString() and Read() on the MailslotData object to receieve the same message data in either format.

Implementation issues: The CCOMMailslot helper object
Implementation of the ClientMailslot object is pretty simple. It offers only two write methods and the internal initialisation method. All of the actual work is deferred to a helper object, CCOMMailslot, which deals with the code that's common between all of the Mailslot COM objects. The only work that the ClientMailslot actually does is to extract the data from the supplied byte array.

The ServerMailslot is equally straight forward. Most of the methods are implemented by CCOMMailslot with only the Read methods requiring any work within the ServerMailslot object itself. Both Read() and ReadString() call down to CCOMMailslot::Read() and then package the resulting data as either a BSTR or a SafeArray of bytes.

The majority of the work that CCOMMailslot does is simply parameter checking and the wrapping of the Win32 Mailslot API. The only slightly complex code is to be found in the read method. Mailslots can be created with a maximum message size, in which case we know the size of the buffer required for read operations, they can also be created with an unspecified message size which accepts messages of any size. If the Mailslot was created with a maximum message size then we simply allocate a buffer large enough and use that for each read. If there was no maximum specified then we first call SizeOfWaitingMessage() to see if there is a message waiting and if so to retrieve the size of the message. If there is a message waiting we expand the size of our buffer, if necessary, so that we have enough space to read the message, we then read the message in.

Implementation issues: The CAsyncCOMMailslot helper object
Not surprisingly the most complex object is the aysnchronous AsyncServerMailslot. This object is multi-threaded and generates events when messages arrive on the Mailslot. It's generally considered unwise[1] to create multi-threaded DLL hosted COM components. However, the threads created in this component are tied to the lifetime of the AsyncServerMailslot objects so we can guarantee that all worker threads will have ceased by the time the component is to be unloaded. If you're concerned about this then the code could easilly be housed in an EXE component. I feel that the convenience of having a single dll component which includes all required proxy/stub code is worth it in this situation.

When an AsyncServerMailslot is created it spawns a worker thread which performs infinitely blocking, overlapped reads on the Mailslot handle. The worker thread blocks waiting for either the read to complete or for its shutdown event to be signalled. When a read completes the receieved data is wrapped in a MailslotData object and the event is fired to alert clients.

Due to the "Rules of COM" the event sink must either be fired from the same thread that was used to register it or the event sink interface must be marshalled to the thread that will fire the event. Due to how ATL generates Connection Point code for us it's not practical to marshal each event sink to a worker thread to fire the event so instead we opt to fire the event from the component's main thread. To fire the event from the component's main thread we need to have the worker thread communicate with the main thread, one method of doing this is to use window messages another is to marshal an interface from the main thread to the worker thread. The window message method is explained in one of Microsoft's knowledge base articles[2] but requires us to create a dummy window and add other clutter to our code, the interface marshalling method is slightly more complex but offers us some advantages.

When an object needs to operate in a multi-threaded way and needs to call back into itself via COM it should marshal an interface to the worker thread using CoMarshalInterThreadInterfaceInStream(). Inside the worker thread the interface is unmarshalled using CoGetInterfaceAndReleaseStream() and can be used to safely communicate with the component's main thread via COM. This works fine until the time comes to unload the component. Unfortunately by marshalling the interface within the component you have created a circular reference cycle. The component is, essentially, holding a reference on itself and this outstanding reference will prevent the component being destroyed. Since the internal reference will be held until the worker thread shuts down and the worker thread only shuts down when the component is destroyed you can see we have a problem.

Implementation issues: Reference cycles and weak identities
The circular reference cycle problem is generally solved using the "split identity" or "weak reference" idioms[3]. The idea is that the reference cycle is broken by a reference that does not affect the object's reference count or the object exposes a second identity (COM object) that, whilst part of the main object, doesnt affect the main object's reference count. Both of these techniques allow the main object to begin shutdown when all external references have been released. Weak references are fairly complex to achieve within ATL and although a solution is presented in [3] it's overly complex and invasive for what we need here.

Our solution to the reference cycle generated by the interface that we're holding in the worker thread is to create a weak identity for the AsyncServerMailslot. This weak identity supports the interface that is required to communicate between the worker thread and the component's main thread. The weak identity is a simple COM object in its own right, it has its own implementation of AddRef(), Release() and QueryInterface() and, since the IUnknown interface returned is different from the main object it has its own identity in COM.

The IDL for the interface that we use for communicating between threads looks like this:

   interface _AsyncServerEvent : IUnknown
      HRESULT OnDataRecieved();

Essentially it's just a way for the worker thread to "prod" the main thread. This interface appears in the IDL file because although we only use the interface internally and none of the publically visable objects expose the object we need to have proxy/stub code generated for it.

The weak identity we need looks like this:

   class CAsyncServerEventHelper : public _AsyncServerEvent
      public :
         CAsyncServerEventHelper(_AsyncServerEvent &theInterface);
         // IUnknown methods
         STDMETHOD(QueryInterface(REFIID riid, PVOID *ppvObj));
      private :
         _AsyncServerEvent &m_interface;

and is implemented like this:

   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;
         return S_OK;
      return E_NOINTERFACE;

Our main COM identity has a member variable of type CAsyncServerEventHelper which it initialises with a pointer to itself (this) in its constructor. It then marshals the weak identity's _AsyncServerEvent interface to its worker thread, this creates the appropriate proxy so that calls to the interface are marshalled across threads correctly, but it doesn't affect the reference count of the main identity.

When the worker thread reads data from the Mailslot it calls the OnDataRecieved() method of the interface that it unmarshalled, this causes the call to be marshalled into the component's main thread where the helper object passes the call on to the main object. In this example we don't bother to pass any data across in the call, we just use it as a way of having one thread "poke" another. The worker thread reads data into the read buffer and pokes the main thread, the main thread then uses the data that has just been read and fires the events in all connected clients. At first this may look dangerous, but we're using the sychronous nature of the STA apartment of our main object to provide synchronisation across the call. The worker thread will block until the main thread completes the event dispatch.

Of course this is but one way of implementing a weak identity, but it's relatively simple, unobtrusive and works well.

Designing COM components that integrate well with VB is fairly straight forward if you follow some simple rules when desiging your interfaces.

Working with asycnhronous events is easy if you follow the rules of COM, are aware of when you're creating reference cycles and know how to break them.


The following source built using Visual Studio 6.0 SP5. Using the November 2001 edition of the Platform SDK. - C++ COM component source code and VB examples

Revision History

Leave a comment