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.
Overview
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);
};
Dim slot As JBCOMMAILSLOTLib.ClientMailslot
Set slot = CreateClientMailslot("MySlot")
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 :
STDMETHOD(Init)(
/*[in]*/ BSTR name,
/*[in]*/ VARIANT computerOrDomain) = 0;
};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.
[
uuid(30A92485-94D2-4CBA-AC32-EF276B7F777B),
helpstring("ClientMailslot Class"),
noncreatable
]
coclass ClientMailslot
{
[default] interface IClientMailslot;
};
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);
};
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
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
{
properties:
methods:
HRESULT OnDataRecieved(
[in] IMailslotData *mailslotData);
};
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
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();
};
The weak identity we need looks like this:
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;
}
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.
Conclusions
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.
References
Download
The following source built using Visual Studio 6.0 SP5. Using the November 2001 edition of the Platform SDK.
JBCOMMailslot.zip - C++ COM componenbt source code and VB examples
Revision History