Overview
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 :
STDMETHOD(Init)(
BSTR name,
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.
[
uuid(30A92485-94D2-4CBA-AC32-EF276B7F777B),
helpstring("ClientMailslot Class"),
noncreatable
]
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
received 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 following bytes: 0x65 0x00 0x65 0x00 0x65 0x00 0x65 0x00.
The synchronous 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);
};
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()
Dim bytes() As Byte
bytes = mailslotData.Read()
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 publicly visible
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);
STDMETHOD(OnDataRecieved)();
ULONG STDMETHODCALLTYPE AddRef();
ULONG STDMETHODCALLTYPE Release();
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;
AddRef();
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.
Conclusions
Designing COM components that integrate well with VB is fairly straight forward if you follow some simple rules
when designing your interfaces.
Working with asynchronous 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
Revision History
- 11th April 2002 - Initial revision.