Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Using Mailslots for Interprocess Communication

0.00/5 (No votes)
17 Oct 2004 5  
How to use mailslots to communicate between processes

Introduction

Sometimes you want one program to communicate with another. You might have multiple servers running on multiple machines and have the need to remotely monitor one or more servers at one central location. Windows provides a rich set of communication methods, from sockets through named pipes to DDE to DCOM to mailslots. I've done some exploration of named pipes in a previous article[^]. This time I'm going to talk about mailslots. I'm assuming a modicum of familiarity with the CreateFile(), ReadFile() and WriteFile() API's and I'm also going to assume familiarity with the basics of overlapped I/O.

Mailslots

implement a 'many writers/one reader' protocol. A process creates a mailslot by name and then waits for messages to be written to it. Other processes can open the mailslot if they know its name and write messages to it. You can only have one mailslot reader but you can have many mailslot writers. Microsoft use server/client terminology to describe this. In Microsoft terminology a server creates and reads from a mailslot; a client connects to an existing mailslot and writes to it. I find this a trifle confusing - I prefer to think in terms of mail slot readers and mail slot writers.

Mailslots exhibit an interesting and very useful property. One writes a message to a mailslot and the reader receives a message. In this context a message is an entire block of data, of arbitrary length. If the writer writes 60 bytes the reader reads 60 bytes - no more and no less. If the writer writes 327 bytes the reader reads 327 bytes - well you get the idea. It's a message oriented protocol, not a byte oriented one. It's analogous to message mode on a named pipe. This isn't to say that you can't read only part of a message; merely that the 'natural' way to use a mailslot is message oriented and that's reflected in the API available to the reader.

Mailslots work across the network. You specify a mailslot name in UNC format just as you'd specify a filename on a server somewhere on your network. Thus, if you had two processes running on the same computer and used mailslots to communicate between them you'd create the mailslot by specifying the name \\.\mailslot\slotname and the process connecting to that mailslot would use the same name. See the dot in there? That's an alias for the machine the process is running on which, in this case, means to look for the mailslot on the local machine. To make the application network aware you'd replace the dot with the name of the machine running the process that created the mailslot. Thus, if I had two machines called, respectively, Rob and Chris and a process running on the machine called Chris had created a mailslot called cp a process running on the machine called Rob could connect to that mailslot by using the name \\chris\mailslot\cp.

Creating a mailslot

is done by calling the CreateMailslot() API, passing it the mailslot name in UNC format and some other parameters. You can only create a mailslot on the local machine so the server part of the UNC pathname must resolve to the local machine; in other words it must be the local machines name or a dot '.' - using '.' is simplest. The other parameters are, in order, the maximum message size that can be sent to the mailslot, a timeout that specifies how long the reader of the mailslot will wait for a message, and a security descriptor that specifies whether the handle returned by the CreateMailslot() API can be inherited by child processes.

Connecting to a mailslot

is just as simple. You use the CreateFile() API specifying the mailslot name in UNC format. You do need to be careful with the sharing modes when opening a mailslot if you hope to implement the many writers/one reader model. If a mailslot writer opens the mailslot without specifying FILE_SHARE_WRITE as the sharing mode it will preclude any subsequent writer being able to write to the mailslot. The annoying thing about how the API is implemented is that subsequent CreateFile() calls succeed to the extent that they seem to get back a valid handle to the mailslot but anything written using that handle is lost.

Once you've got a handle to a mailslot what do you do with it?

If you created the mailslot via the CreateMailslot() API you read from it using the ReadFile() API. The mailslot handle is created in overlapped I/O mode so you can use overlapped I/O on it, though you can use non overlapped I/O if that suits your model better. You can also call the GetMailslotInfo() API to query how many messages are waiting, how long the next message is and what the timeout is. You can call the SetMailslotInfo() API to change the timeout. Be aware that the handle you pass to those two API's must have been created by the CreateMailslot() API.

If you didn't create the mailslot then you connect to the mailslot by using the CreateFile() API. In this case you can write to it using the WriteFile() API. Whether you can use overlapped I/O depends on how you call the CreateFile() API; it can be synchronous or asynchronous depending on your needs. You can't connect to a mailslot using CreateFile() and expect to be able to read from it (believe me, I've tried).

A gotcha with mailslots

The MSDN documentation for mailslots says that the mailslot hangs around as long as any open handle on the mailslot exists. I've found this isn't quite true (on Windows XP Professional SP2). You can have any number of open writer handles to a mailslot yet the mailslot disappears as soon as the reader handle is closed (and writes to the mailslot fail once the reader handle is closed). This makes sense. If you can only have one reader there wouldn't be much point having the mailslot hang around once that reader is gone since any messages written to the mailslot would be needlessly buffered by the system; if there's no reader, messages buffered in this way would hang around forever (remember that you can't use CreateFile()) to open a read handle to a mailslot).

Other considerations with mailslots

If you read the MSDN documentation closely you'll see that that I somewhat simplified the rules when connecting a writer to an existing mailslot. In addition to specifying the server name, which would connect to a mailslot on a specific server, it's possible to specify a connection to all mailslots of a particular name on a domain. You do this by specifying the domain name rather than a server name, thusly \\domainname\mailslot\name. You can also use an asterisk, * as shorthand for the primary domain. At first glance this sounds wonderful - you could create any number of readers running on various machines within a domain and write to all of them simultaneously by specifying the domain name. But there's a gotcha. If you connect to a mailslot as a writer using the domain specifier you cannot write more than 424 bytes per message. On the other hand, if your application can live within that limit this is a very easy way to monitor a single process from multiple locations in a domain.

Of course there's a class (or two)

What you've read so far might have piqued some interest in using mailslots; the rest of this article will describe a set of classes I've written to make the use of mailslots easy. There are five classes in all.

The first class, CMailslot, is an abstract base class which encapsulates the basics of mailslots.

Then there are two derived classes, CSyncMailslotReader which implements the server (reader) side of a mailslot and CSyncMailslotWriter which implements the client (writer) side of a mailslot. With both of those classes the thread calling the appropriate member function (read or write) blocks until the operation is completed.

Then there is CQueuedMailslotWriter which queues messages and uses an instance of CSyncMailslotWriter on a separate thread to actually write the messages. This class is asynchronous, decoupling the writer from network latencies. A writer process can queue up many thousands of messages if it wishes whilst the main thread continues to generate messages; the CQueuedMailslotWriter class handles the details of dribbling them out to the mailslot as and when the mailslot is able to recieve them.

Finally, there is one higher level class, CAsyncMailslotReader, which implements a simple protocol to allow a reader to treat incoming messages as events.

There isn't a queued mailslot reader class. It would be easy enough to write one but it doesn't make sense to me to write both. Either both ends are able to keep up with the stream of messages or one end queues. Making both ends queue messages simple shifts responsibility from the writer to the reader.

CMailslot

looks like this:

//  Base mail slot class, contains stuff common to servers and clients

class CMailslot
{
protected:
                    CMailslot();
    virtual         ~CMailslot();

    virtual bool    Connect(LPCTSTR szSlotName, 
                            LPCTSTR szServerName = _T(".")) = 0;

public:
    virtual void    Disconnect();

    bool            IsOpen() const 
                    { return m_hMailSlot != INVALID_HANDLE_VALUE; }

protected:
    HANDLE          m_hMailSlot,
                    m_hStopEvent;
    bool            m_bStop;
    LPTSTR          m_pszSlotname;
    COverlappedIO   m_overlapped;
};
        

The class is an abstract base class so you can't directly instantiate a CMailslot. The m_hMaiSlot handle will be pretty obvious but the remaining data members might not be unless you've read some of my previous articles. Briefly, almost all of the remaining member variables exist to allow for graceful shutdown of a multithreaded application (see this article[^] for the gory details and see this article[^] for more than you ever wanted to know about interrupting mutex calls. I also used my COverlappedIO class[^] to encapsulate the overlapped I/O logic.

Notice that the Connect() method is both virtual and abstract. It's that way because (as discussed earlier) the way you open a mailslot depends on whether you're the reader or the writer. It's virtual to make it overrideable, and it's abstract to force each derived class to actually implement the function. If you're a reader class deriving from CMaislot you are expected to use the CreateMailSlot() API to open the mailslot; if you're a writer class you're expected to use the CreateFile() API to open the mailslot. In neither case is there a 'reasonable' default behaviour. The Connect() method requires a mailslot name and takes an optional server name that defaults to '.' (dot) which, as already noted, indicates the local machine.

Disconnect() differs inasmuch as a 'reasonable' default behaviour requires only that the mailslot handle be closed. There might be other behaviours required but, since closing the handle is a 'reasonable' minimum, the class doesn't enforce more.

IsOpen() returns a bool indicating whether the mailslot was successfully opened. The function simply tests that the m_hMailSlot member isn't INVALID_HANDLE_VALUE. As we'll see a little later this isn't quite good enough a test but it's the best we can do short of attempting to write to the mailslot.

CSyncMailslotWriter

This class implements the writer side of a mailslot. It looks like this.

//  Mail slot writer class. Used to write to a mail slot.

//  The class creates an asynchronous mail slot handle which is 

//  used with overlapped I/O to write queued messages.

class CSyncMailslotWriter : public CMailslot
{
public:
                    CSyncMailslotWriter();
    virtual         ~CSyncMailslotWriter();

    virtual bool    Connect(LPCTSTR szSlot, LPCTSTR szServer = _T("."));

    virtual DWORD   Write(BYTE *pbData, DWORD dwDataLength);

protected:
    virtual bool    Connect();
};

The class provides the required override of the public Connect() method which looks like this.

//    Creates a connection to a mail slot.

//    Returns true on success, false on failure.

bool CSyncMailslotWriter::Connect(LPCTSTR szSlotname, LPCTSTR szServer)
{
    assert(szServer);
    assert(szSlotname);

    //    Delete any previous mail slot name

    delete m_pszSlotname;
    m_pszSlotname = new TCHAR[_MAX_PATH];
    assert(m_pszSlotname);

    //    Create our mail slot name

    _sntprintf(m_pszSlotname, _MAX_PATH, _T(\\\\%s\\mailslot\\%s),
                   szServer, szSlotname);
    m_pszSlotname[_MAX_PATH - sizeof(TCHAR)] = TCHAR(0);
    
    //    Now connect...

    return Connect();
}
        

This is pretty easy. First we validate the input parameters. Then we create the canonical form of the mailslot name and call the private Connect() method which does the real connection to the mailslot.

I did it this way because I wanted my mailslot classes to cope with network problems without requiring that the client know very much about error handling. It's perfectly possible, networked or not, for the reader to go away unexpectedly. If and when that happens the writer should attempt to recreate the connection but if it fails it shouldn't block the client. In the latter case, if the class fails to recreate the connection it throws the message away. Yes, information may be lost in this model, but at least the client continues to run. The client is free to do what it chooses when it recieves false as the return value from the Write() method. It can retry the Write() or ignore the error. Thus, there are two Connect() methods. The public one, which takes the server and mailslot name parameters, and a private one which performs the actual connection. The client calls the public method and neither knows nor cares that there's a private method that does the actual connection. The private method looks like this:

bool CSyncMailslotWriter::Connect()
{
    //  Close any existing mail slot

    Disconnect();

    //  Now open the mail slot for overlapped I/O

    if ((m_hMailSlot = CreateFile(m_pszSlotname, 
                            GENERIC_WRITE, 
                            FILE_SHARE_READ | FILE_SHARE_WRITE, 
                            NULL, 
                            OPEN_EXISTING, 
                            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
                            &m_overlapped)
        ) != INVALID_HANDLE_VALUE)
    {
        m_overlapped.Attach(m_hMailSlot);
        return true;
    }

    return false;
}
        

This method disconnects from any previous mailslot and then opens a connection to the mailslot using the CreateFile() API. If it succeeds it attaches the new mailslot handle to the COverlappedIO object and returns true, otherwise it returns false.

The Write() method looks like this:

//  Writes a message to the mail slot.

DWORD CSyncMailslotWriter::Write(BYTE *pbData, DWORD dwDataLength)
{
    assert(pbData);
    assert(dwDataLength);

    int nRetries = 2;

    while (nRetries--)
    {
        //  If the mail slot is closed attempt to reconnect to it

        if (!IsOpen() && m_pszSlotname != LPTSTR(NULL))
            Connect();

        DWORD dwWrittenLength = 0;

        if (IsOpen())
        {
            //  Write using overlapped I/O. We have to use overlapped 

            //  I/O if we want to be able to interrupt the write. If we

            //  use synchronous I/O there's a high chance the operation

            //  will stall inside the WriteFile call.  See 

            //  http://www.codeproject.com/win32/overlappedio.asp

            //  for a more detailed explanation.

            if (m_overlapped.Write(pbData, dwDataLength, &dwWrittenLength,
                                   m_hStopEvent) 
                             && dwWrittenLength == dwDataLength)
                //  The I/O completed so return success (true).

                return dwWrittenLength;
            else
                //  If the write failed discard it but also force a 

                //  disconnect so that the next write will attempt a 

                //  connection.

                Disconnect();
        }
    }

    return 0;
}
        

This method makes two attempts to write the message to the mailslot. If the mailslot is open and the write succeeds well and good, it returns the number of bytes it wrote. If it fails to write it does a Disconnect(), goes around the loop and tries again. If the Connect() attempt succeeds it writes the message and returns the number of bytes it wrote. If the Connect() fails it returns 0 as the number of bytes written and it's up to the caller to decide what to do with the message.

Now you might have noticed that I claim the class is synchronous and yet it uses overlapped I/O. I'm not going mad, nor am I being inconsistent. From the callers point of view the class behaves synchronously unless the caller needs to interrupt the I/O. Actually the thread that uses the class can't interrupt the I/O because it blocks on the Write() call. Nonetheless, another thread CAN interrupt the I/O if it needs to, which is the reason a synchronous class uses overlapped I/O in the first place. See here[^] for an explanation of why.

CSyncMailslotReader

This class implements the reader side of a mailslot and looks like this:

//  Mail slot reader class. Used  to read from a mail slot. 

class CSyncMailslotReader : public CMailslot
{
public: CSyncMailslotReader(); virtual ~CSyncMailslotReader();
    virtual bool    Connect(LPCTSTR szSlotname, 
                            LPCTSTR szServer = _T(".")); 
    BYTE            *Read(DWORD& dwBufferLength);
    DWORD            GetMessageCount(
                         LPDWORD pdwNextMessageLength = (DWORD *)  NULL);
};
        

As with the CSyncMailslotWriter class this class provides its own override of the Connect() method, which looks like this.

//  Create a named mail slot. This must be done on the local machine

//  thus we don't use the server name parameter.

bool CSyncMailslotReader::Connect(LPCTSTR szSlotname, LPCTSTR /*szServer*/)
{
    assert(szSlotname);
    
    if (IsOpen())
    {
        TCHAR szTempSlotname[_MAX_PATH];

        //  If we get here it means the mailslot handle might be valid so

        //  let's check that the m_pszSlotname variable isn't a NULL 

        //  pointer.  If it is then we've got an inconsistency that 

        //  oughtn't to happen.

        assert(m_pszSlotname);
        _sntprintf(szTempSlotname, 
                   _MAX_PATH, _T("\\\\.\\mailslot\\%s"), szSlotname);
        
        if (_tcsicmp(m_pszSlotname, szTempSlotname) == 0)
            return true;
        else
            Disconnect();
        }

    //  Delete any previously created slot name

    delete m_pszSlotname;
    m_pszSlotname = new TCHAR[_MAX_PATH];
    assert(m_pszSlotname);

    //  Create our mail slot name

    _sntprintf(m_pszSlotname, _MAX_PATH, _T(\\\\.\\mailslot\\%s), 
               szSlotname);
    m_pszSlotname[_MAX_PATH - sizeof(TCHAR)] = TCHAR(0);
    
    if ((m_hMailSlot = CreateMailslot(m_pszSlotname, 0, 
                                      MAILSLOT_WAIT_FOREVER, NULL))
                    != INVALID_HANDLE_VALUE)
    {
        //  Attach the mailslot handle to the overlapped

        //  I/O object.

        m_overlapped.Attach(m_hMailSlot);
        return true;
    }

    return false;
}
        

Because a mailslot reader controls the lifetime of a mailslot there's no need to have two Connect() methods. The method disconnects from any previous mailslot controlled by this instance of the class and creates a new mailslot. Notice that we don't use the szServer parameter because, as aforesaid, mailslots must be created on the local machine. Once we've created the mailslot we attach it's handle to the COverlappedIO object in this class instance.

There is a sanity check at the start of the method to guard against the possiblity of calling Connect() more than once with the same mailslot name. Without the sanity check the method would go ahead and close the handle to an existing mailslot and recreate it. This works except that writers that hold open handles to the mailslot will experience a write error on the next write attempt. The CSyncMailslotWriter::Write() method can cope with this but why waste the CPU cycles when the test is relatively simple?

Reading a message from the mailslot is a trifle more complex than writing one. The reason is that you have two choices about how to read. You can sit in a loop polling the class via the GetMessageCount() method or you can actually fall into the Read() method and wait for a message. Using GetMessageCount() lets you determine up front how much data you're going to read (and how much memory to allocate) but the downside is that you waste a lot of CPU cycles polling the mailslot for the next message. On the other hand, calling Read() directly means that you have no idea how much data is going to be read so you have to make some arbitrary decisions about how much data you're prepared to handle in a single Read() call. I chose the second route and an arbitrary limit of 65536 bytes of data (this figure also happens to be Microsoft's recommended upper limit on the size of a mailslot message). The Read() method looks like this.

//  Read a message from the mailslot and return it in a buffer allocated

//  on the heap. The caller must delete the buffer once it's done with it.

BYTE *CSyncMailslotReader::Read(DWORD& dwBufferLength)
{
    //  We have to allocate a large buffer for incoming messages because

    //  we have no idea how much data is coming...

    BYTE  *pbData = (BYTE *) NULL,
          *pbTemp = (BYTE *) NULL;

    dwBufferLength = 0;

    if (IsOpen())
    {
        pbData = new BYTE[65536];
        assert(pbData);

        //  Now read the data

        if (m_overlapped.Read(pbData, 65536 - sizeof(TCHAR), 
                              &dwBufferLength, m_hStopEvent)
            && dwBufferLength)
        {
            //  If we read a message it's time to copy the data into a 

            //  buffer of the correct length to hold the message.  

            //  Actually we add one character to the buffer so that, if 

            //  the message is really a string, it'll be correctly 

            //  terminated and maintain string semantics.

            pbTemp = new BYTE[dwBufferLength + sizeof(TCHAR)];
            assert(pbTemp);
            memcpy(pbTemp, pbData, dwBufferLength);
            pbTemp[dwBufferLength] = TCHAR(0);
        }
    }

    delete [] pbData;
    return pbTemp;
}
        
You'll notice a little bit of trickery going on here. Mailslots aren't string oriented, they're byte oriented and you can send any data you want. However, much of my usage of mailslots is to send text strings from one process to another and I want string semantics if possible. Reserving space for a NULL terminator at the end of the buffer achieves this. Thus, the real limit on the size of a message in this implementation is 65536 bytes minus the sizeof the character encoding.

CQueuedMailslotWriter

This class implements a message queue and uses a background thread to write them to an instance of a CSyncMailslotWriter. The class implements the concept of high priority messages and normal priority messages. There is one queue for each kind of message and, naturally, messages in the high priority queue are sent first. The caller specifies the message priority when it calls the Write() method. You could, if you wished, extend this mechanism to as many priority levels as you like but in practice I've found two levels to be sufficient. There is, of course, a performance penalty involved in extending the priority levels. The class looks like this:

class CQueuedMailslotWriter : public CSyncMailslotWriter
{
    class CQueuedData
    {
    public:
        CQueuedData(BYTE *pbData, DWORD dwDataLength);
        ~CQueuedData();

        DWORD       Length() const      { return m_dwDataLength; }
        BYTE        *Data() const       { return m_pbData; }

    private:
        BYTE        *m_pbData;
        DWORD       m_dwDataLength;
    };

    typedef deque<CQUEUEDDATA *> DATAQUEUE;
    typedef DATAQUEUE::const_iterator DQITER;

public:
                    CQueuedMailslotWriter(void);
    virtual         ~CQueuedMailslotWriter(void);

    virtual bool    Write(BYTE *pbData, DWORD dwDataLength,
                          BOOL bImportant);
    virtual bool    Connect(LPCTSTR szSlotname,
                            LPCTSTR szServername = _T("."));

private:
    static unsigned __stdcall ThreadStub(LPVOID data);
    virtual void    ThreadProc(CBaseThread *pThread);
    void            StopThread();

    HANDLE          m_hStopEvent,
                    m_hSignalEvent,
                    m_haSignal[2];
    CInterruptibleMutex m_imMutex;
    CBaseThread     *m_pThread;
    volatile bool   m_bStop;
    DATAQUEUE       m_highPriorityDataQueue,
                    m_normalPriorityDataQueue;
};
        
The major additions are a private class, CQueuedData and some thread related variables. The CQueuedData class is simply a convenient way of saving the data passed in each call to the Write() method. The Write() method packages up the data passed and adds it to a queue. Some time later the ThreadProc() method will dequeue the data and pass it to the base class Write() method. CQueuedMailslotWriter::Write() looks like this:
//  Writes a message to the mail slot. Actually it queues the message

//  for the mail slot and leaves it to the background thread to actually

//  do the write.

bool CQueuedMailslotWriter::Write(BYTE *pbData, DWORD dwDataLength,
                                  BOOL bImportant)
{
    assert(pbData);
    assert(dwDataLength);

    //  If the mail slot is closed attempt to reconnect to it

    if (!IsOpen() && m_pszSlotname != LPTSTR(NULL))
        CSyncMailslotWriter::Connect();

    if (IsOpen())
    {
        //  Grab the mutex first.  You must have the mutex before

        //  attempting to create the QueuedData object else you'll

        //  corrupt the program heap or deadlock on the heap lock.

        if (m_imMutex.AquireMutex(m_hStopEvent) == 
                                  CInterruptibleMutex::eMutexAquired)
        {
            CQueuedData *pqData = new CQueuedData(pbData, dwDataLength);

            assert(pqData);

            if (bImportant)
                //  High priority message, put it on the high priority

                //  queue

                m_highPriorityDataQueue.push_back(pqData);
            else
                //  Normal priority message, put it on the normal priority 

                //  queue

                m_normalPriorityDataQueue.push_back(pqData);

            m_imMutex.ReleaseMutex();

            //  Now signal the queue handler thread...

            SetEvent(m_hSignalEvent);
            return true;
        }
    }

    return false;
}
        

As with the CSyncMailslotWriter::Write() method, this class first checks that the mailslot connection is open. If it isn't it tries to reconnect to the mailslot. If it has what it believes is a valid mailslot connection it goes ahead and adds the message to the queue. If not it simply discards the message. If it added the message to the queue it sets an event which informs the background thread that a new message has arrived and needs to be sent. The Write() method and the thread procedure share the same data queues so they have to implement a safe method of adding and removing entries from the queue. They do this by using a shared mutex (actually an instance of my CInterruptibleMutex class)[^ ]

The thread looks like this:

void CQueuedMailslotWriter::ThreadProc(CBaseThread *pThread)
{
    CQueuedData *pqData;
    DQITER      pdqIterator;
    bool        bQueuePriority;

    while (!pThread->Stop())
    {
        switch (WaitForMultipleObjects(2, m_haSignal, FALSE, INFINITE))
        {
        case WAIT_OBJECT_0:
            //  Told to stop, so stop

            break;

        case WAIT_OBJECT_0 + 1:
            //  Grab the mutex before we fall into the loop

            if (m_imMutex.AquireMutex(m_hStopEvent) !=
                                      CInterruptibleMutex::eMutexAquired)
                //  Signalled to stop, so stop...

                break;

            //  New message added to the queue, send it

            while ((m_highPriorityDataQueue.size() || 
                    m_normalPriorityDataQueue.size()) 
                   && !pThread->Stop())
            {
                //  Keep looping until either the queue is empty or 

                //  we've been signalled to stop.

                if (m_highPriorityDataQueue.size())
                {
                    pdqIterator = m_highPriorityDataQueue.begin();
                    bQueuePriority = false;
                }
                else
                {
                    pdqIterator = m_normalPriorityDataQueue.begin();
                    bQueuePriority = true;
                }

                pqData = *pdqIterator;

                //  Done for now, release the mutex to give other threads

                //  a chance of queuing data.

                m_imMutex.ReleaseMutex();
                
                if (CSyncMailslotWriter::Write(pqData->Data(),
                        pqData->Length()) == pqData->Length())
                {
                    //  Now aquire the mutex again so we can remove the

                    //  message from the queue

                    if (m_imMutex.AquireMutex(m_hStopEvent) == 
                                  CInterruptibleMutex::eMutexAquired)
                    {
                        //  The bQueuePriority flag tells us which queue we 

                        //  pulled the message from. We can't use the queue

                        //  size here because the queues might have 

                        //  changed in the time between now and when we

                        //  pulled the message to be sent.

                        if (bQueuePriority == false)
                            m_highPriorityDataQueue.pop_front();
                        else
                            m_normalPriorityDataQueue.pop_front();

                        delete pqData;
                        continue;
                    }
                }
                else
                    //  Failed to write the message, leave it on the queue

                    //  and break out of the writer loop

                    break;
            }

            //  Finished the loop, so release the mutex.

            m_imMutex.ReleaseMutex();
            break;
        }
    }

    //  Make sure we cancel any pending I/O before exiting the thread.

    if (IsOpen())
        CancelIo(m_hMailSlot);
}
        
This waits until it's signalled that a new message has arrived. When one arrives it checks the high priority queue first and then the normal priority queue. Either way it gets a message to send, which it does by calling CSyncMailslotWriter::Write(). The main thing of note in this procedure is the locking. When it's signalled that a message is waiting to be sent it locks the message queue by grabbing the mutex. It pulls the message from one or the other queue and releases the mutex before attempting to send the message. Once it's sent the message it grabs the mutex again so it can safely remove the message from the queue. The loop is complicated by the need to ensure that the mutex has been aquired before checking the queue sizes and since the queue sizes are the controlling variable for the while loop that means more calls to aquire the mutex than you'd expect. Naturally, since the call to CSyncMailslotWriter::Write() can block, we release the mutex before making that call. Once the call has returned we need to grab the mutex again before continuing to the top of the while loop.

CAsyncMailslotReader

This class represents a higher level usage of a mailslot reader which runs on a seperate thread and passes incoming messages to the client via a virtual function, OnMessage().

The class looks like this:

class CAsyncMailslotReader : public CSyncMailslotReader
{
public:
                    CAsyncMailslotReader();
    virtual         ~CAsyncMailslotReader();

    virtual bool    OnMessage(BYTE *pbMessage, DWORD dwMessageLength) = 0;
    virtual bool    Connect(LPCTSTR szSlotName);

protected:
    static unsigned int __stdcall ThreadStub(LPVOID data);
    unsigned int    ThreadProc(LPVOID data);

    CBaseThread     *m_pThread;
};
        

The class overrides the virtual Connect() method in order to create the monitor thread at the time the object creates the mailslot. Connect() looks like this:

bool CAsyncMailslotReader::Connect(LPCTSTR szSlotName)
{
    assert(szSlotName);
    bool bStatus = CSyncMailslotReader::Connect(szSlotName);

    if (bStatus)
    {
        //  If we succeeded in creating the mailslot we start up a thread to

        //  monitor it.

        m_pThread = new CBaseThread(m_hStopEvent, &m_bStop, ThreadStub,
                                    false, this);
        assert(m_pThread);
    }

    return bStatus;
}
        

Which is pretty simple. The thread procedure looks like this:

unsigned int CAsyncMailslotReader::ThreadProc(LPVOID data)
{
    CBaseThread *pThread = (CBaseThread *) data;
    BYTE        *pbMessage;
    DWORD       dwMessageLength = 0;

    assert(pThread);

    while (!pThread->Stop())
    {
        //  Get and dispatch messages

        pbMessage = Read(dwMessageLength);

        if (dwMessageLength)
            OnMessage(pbMessage, dwMessageLength);
    }

    return 0;
}
        
which, again, is pretty simple. The thread simply sits inside a call to CSyncMailslotReader::Read() waiting for the next message. When that message arrives the thread calls the virtual OnMessage() method which you override to perform whatever processing is necessary for your application. Notice that the CAsyncMailslotReader::OnMessage() method is a pure virtual method; you cannot instantiate an instance of CAsyncMailslotReader directly but must derive your own class from it.

Be aware that the call to your OnMessage() method occurs on a different thread to your main application thread; it's particularly important to remember this if you're using the class in an MFC application and you're manipulating CWnd's from the OnMessage() method.

Using the code

You need to build the library. It's a static library, not a dll; let's not go there. The source download contains all the files you need to build the library. Include the appropriate headers into your project depending on which non-abstract classes your project uses; they will include mailslots.h and that file, in turn, inserts the necessary statements to pull the library in. All you need to do is ensure the library is somewhere on your library paths.

Demo projects

There are three demo projects. The first doesn't actually use the mailslot library; all it does is enumerate and display which mailslots exist on your system. I wrote this during my initial exploration of mailslots and I've found it useful when trying to determine what mailslots were available. Will you find it useful? *shrug*

The second demo project is a mailslot listener. You specify a mailslot name and hit the create button. Once you've done that a mailslot with that name will exist on your system for as long as the program continues to run. Any messages sent to that mailslot will be displayed by the program. Of course, since this is a mailslot listener, as soon as you close the program the mailslot goes away.

The third demo project is a mailslot writer. As with the mailslot listener demo project, you specify a mailslot name and hit the create button. If a mailslot of that name exists the writer will connect with it. You can then type messages into the message edit control, hit the send button and expect to see the mailslot listener display the message.

Known bugs

I know of one bug. It occurs in a scenario I consider so unlikely in real world applications that I don't worry about it. If you run the mailslot listener demo project and the mailslot writer project, create a mailslot in the listener, connect to the listener, send one or more messages and then change the name in the listener, match the change in the writer and send repeated messages you'll see that every second message is garbage. It's an easy bug to reproduce and I've spent hours trying to find it. (Someone will find it within 5 minutes of my posting this article). It is however unlikely to occur in real world usage of the class because I very much doubt that mailslot listeners will change the name of the mailslot they listen on, let alone inform writers that they're about to change the name of the mailslot.

Mailslots vs Named Pipes

In my experience Mailslots are somewhat easier to use than Named Pipes for interprocess communication. Nonetheless, it's not a simple design decision. If it were then we probably wouldn't have both available. So here are the pros and cons.

As noted at the start of the article, Mailslots implement a many writers/one reader model. One handle on the reader side of a Mailslot can monitor many many writers. Named Pipes require one pipe per connection. This means that an implementation of the many writers/one reader model requires one named pipe per writer and one read handle per writer. This places an upper limit on how many writers a named pipe implementation can monitor of 64 writers per monitor thread.

In addition, you can create Mailslots on any 32 bit Windows implementation including Windows 95.  If you believe Microsoft's marketing that doesn't matter but my experience is that you're better off anytime you can include the Win9X family of operating systems in your target audience. A Win9X system can connect to a Named Pipe but it can't create one - to create a Named Pipe you need Windows NT and descendants.

On the other hand, Mailslots use UDP datagrams for their transport. That means that a message isn't guaranteed to be delivered. It also means that messages aren't guaranteed to be received in the order they're sent. If it's important to your application that messages are received AND received in order Mailslots aren't necessarily the right way to go. If you're using Mailslots as a mechanism for interprocess communications on the same machine you're probably good to go - the odds on a message being lost or sent out of sequence are vanishingly low on the local machine (but beware if your local machine is multiprocessor: mine is). If you're sending messages across a network and you absolutely must have each message arrive in the correct sequence you probably need to look for some other IPC mechanism.

Finally, Named Pipes are a bi-directional communications channel. Each end of the pipe can both read and write from the same pipe. You can't do that with a Mailslot!

History

October 10, 2004 - Initial version.

October 16, 2004 - Added some commentary about Named Pipes vs Mailslots and why one might choose one over the other. (Thanks to JT Anderson).

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here