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: %sn” ), 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.



More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read