C++ Programming: Memory Mapped Files using RAII

Introduction

Memory Mapped Files are a form of Interprocess Communication used to pass data between processes. This article introduces a set of C++ helper classes to simplify using MM files.

This article will assume that you know what a memory mapped file (MMF) is and understand that an MMF can be based off of a physical file or backed by the system page file. The classes introduced later will be system page file backed MMFs. For an excellent primer on Memory Mapped Files, see Managing Memory Mapped Files.

Passing Data between processes and Data Synchronization

A Memory Mapped File is like any other shared resource passed between threads - that is, its access must be synchronized otherwise race conditions can occur. Shared data passed between two threads of a single process can be synchronized using a critical section or a mutex. However, when data is shared between multiple processes, a mutex must be used (as critical sections can't be used across processes).

To help with thread synchronization chores, we emply the technique or Resource Acquisition Is Initialization (RAII) via synchronization helper classes defined in AutoLock.h in the attached sources. There are several 'lock' classes that wrap critical sections, mutexes, and reader writer lock implementation which are locked via the RAII approach by an AutoLockT locker class. While the focus of this article isn't on thread synchronization, the use of these classes trivializes the synchronization.

The following table describes the thread sync classes.

ClassDescription
CSLockCritical Section wrapper
MutexLockMutex wrapper
ReaderWriterLockReader Writer lock class. Allows single writer, multiple readers.
AutoLockTRAII locking class used with above lock classes.. Acquires lock on construction and auto-releases lock on destruction

Listing 1 is a simple example of using the AutoLockT class with a CSLock class. // Example class that illustrates using the CSLock and AutoLockT classes

class Example
{
  // Called by thread 1 
  void SetDataSafely( LPCTSTR szData )
  {
    // Acquire the critical section lock (prevent t2 access)
    AutoLockT< CSLock > lock( &m_csLock );

    m_sData = szData;
  }

  // Called by thread 2
  void GetDataSafely( LPTSTR szBuffer, rsize_t size )
  {
    // Acquire the critical section lock (prevent t1 access)
    AutoLockT< CSLock > lock( &m_csLock );

    _tcscpy_s( szBuffer, size, m_sData );
  }
private:
  CSLock m_csLock; // Critical section lock 
  CString	m_sData;  // data to protect
};

Listing 1

In the set and get methods above, the lock is aquired with AutoLockT, the data is accessed and the lock is released when the method goes out of scope. That's RAII as applied to thread synchronization in a nutshell.

MMF Helper Classes

The tables below describe the MMF classes and methods.

MMF Classes

ClassDescription
MMFileTBase class that wraps mmf file functionality. Can be used to pass raw bytes between processes.
MMFileStructTClass that passes a structure between processes.
MMFileStructArrayTClass that passes an array of structures between processes.

Table 1

MMF Methods

MethodDescription
CreateMapCreates a memory mapped file and optionally opens a file mapping. For the structure classes, automatically sizes the mapping to the structure (or array of structures).
FreeMapCloses the file mapping.
MapViewMaps a view. Typically not used for the structure derived implementations.
OpenMapOpens an existing MMF. Fails if the MMF does not exist.
rawAccesses the underlying structureClass that passes an array of structures between processes.
UnMapUnmaps the data view.

Table 2

Sending a structure between processes

The sample solution includes two projects: The LogSender project and LogReceiver project which pass a LOGITEM stucture between two processes simulating interprocess logging.

Running the sample code

Open two instances of Visual Studio and load the MMFile.sln into each instance. In the first instance, set the LogReceive project as the startup project. Set a few breakpoints in the project and press F5 to start the receiver. In the second instance, set the LogSender projects as the startup project. Set a few breakpoints and press F5. As you navigate the break points of the LogSender project, the LogReceiver project should hit the break points and output the log data to it's console.

Note: the code listings below do not include error handling. Consult the attached source code for more detailed error handling.

LogSender Code

int _tmain( int argc, _TCHAR* argv[] )
{
  const int TIMEOUT = 5000;

  // Create the mmf class instance
  MMFileStructT< LOGITEM >	logItemMmf( _T("{91085AD2-ECC3-4bbe-B81C-DC53E6E3A39F}"), TIMEOUT );

  // Create and open the file mapping
  logItemMmf.CreateMap( );

  // Send a bunch of log items over to the LogReceiver process
  for( int x = 0; x < 100; x++ )
  {
    CString sMsg;
    sMsg.Format( _T("Log item msg - %d"), x );

    { // Locking scope (briefly lock the mmf to set the log data)

      // Lock the mmf using RAII
      AutoLockT< MMFileStructT< LOGITEM > > lock( &logItemMmf, TIMEOUT );

      // Change the log data
      LPLOGITEM pLogItem = logItemMmf.raw( );
      pLogItem->uDepth = x;
      _tcscpy_s( pLogItem->szMsg, sMsg ); 

    } // Mmf unlocked here

    // Signal the LogReceiver process and wait for it to retrieve the log data
    ::SignalObjectAndWait( logItemMmf.GetChannelEvent( MMFile::EVENT_CH1 ),
        logItemMmf.GetChannelEvent( MMFile::EVENT_CH2 ),
        TIMEOUT,
        FALSE );

    // Reset the CH2 event
    logItemMmf.ResetChannelEvent( MMFile::EVENT_CH2 );
  }

  return 0;
}

LogReceiver Code

int _tmain(int argc, _TCHAR* argv[])
{
  const int TIMEOUT = 5000;

  MMFileStructT< LOGITEM >	logItemMmf( _T("{91085AD2-ECC3-4bbe-B81C-DC53E6E3A39F}"), TIMEOUT );

  logItemMmf.CreateMap( );

  HANDLE aHandles[] = { logItemMmf.GetChannelEvent( MMFile::EVENT_CH1 ) };

  BOOL bContinue = TRUE;

  while( bContinue )
  {
    switch( WaitForMultipleObjects( sizeof(aHandles) /sizeof(HANDLE),
        &(aHandles[0]),
        FALSE,
        INFINITE))
    {
    // Channel 1 Event Fired (From LogSender process)
    case WAIT_OBJECT_0:
      {
        // Lock the inter-process mmf mutex using RAII
        AutoLockT< MMFileStructT< LOGITEM > > lock( &logItemMmf, TIMEOUT );

        // Access the log item
        LPLOGITEM pLogItem = logItemMmf.raw( );

        // Display the message to the console
        _tprintf( _T( "LogItem - depth: %d message: %s\n" ), pLogItem->uDepth, pLogItem->szMsg );

        // Exit if we've reach 100 log items
        if( pLogItem->uDepth >= 99 )
        {
          bContinue = FALSE;
        }
      }
     
      // Reset the CH1 event
      logItemMmf.ResetChannelEvent( MMFile::EVENT_CH1 );

      // Set the CH2 event
      logItemMmf.SetChannelEvent( MMFile::EVENT_CH2 );
    }
  }

  std::cout << "\nLog Received completed! Exiting..." << std::endl;
	
  Sleep( 5000 );

  return 0;
}

Code Description

As you can see from the LogSender listing above, an instance of the MMFileStructT class is made given a unique string. Next, the CreateMap method is called to create the underlying memory mapped file based on the size of the structure. Internally the CreateMap also creates a view of the file.

To access the LOGITEM structure, the .raw( ) method is called. You'll notice that before accessing the class the AutoLockT is used to lock the logItemMmf object. The base MMFile class internally creates a mutex (which is what the AutoLockT class locks) and a couple of 'channel' events.

The SignalObjectAndWait api is used to set the Channe1 1 event so the LogReceiver process gets signalled that the MMF data has changed. The LogReceiver process waits on the Channel 1 event and then locks the mutex, reads the log item data and then sets the Channel 2 event to indicate that it has finished reading the data. The SignalObjectAndWait api blocks until it the Channel 2 event has been set by the LogReceiver process.

The LogReceiver code creates an MMF instance with the same name as used by the LogSender project. The project code just waits on the Channel 1 event before accessing the log item structure. It sets the Channel 2 event when it's finished accessing the structure. Setting the Channel 2 event causes the LogSender app to send the next log item.

Conclusion

Memory Mapped Files offer one form of interprocess communication. The classes detailed above help the developer use MMF's to pass raw data, a structure, or an array of structures between processes.

About the Author

Arjay Hawco is an application developer/architect and Microsoft C++ MVP who works with the latest WxF .Net technologies. He is also cofounder of Iridyn, Inc, a software consulting firm.




Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: August 20, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT When you look at natural user interfaces as a developer, it isn't just fun and games. There are some very serious, real-world usage models of how things can help make the world a better place – things like Intel® RealSense™ technology. Check out this upcoming eSeminar and join the panel of experts, both from inside and outside of Intel, as they discuss how natural user interfaces will likely be getting adopted in a wide variety …

  • Protecting business operations means shifting the priorities around availability from disaster recovery to business continuity. Enterprises are shifting their focus from recovery from a disaster to preventing the disaster in the first place. With this change in mindset, disaster recovery is no longer the first line of defense; the organizations with a smarter business continuity practice are less impacted when disasters strike. This SmartSelect will provide insight to help guide your enterprise toward better …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds