This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


November 1998

Microsoft Systems Journal Homepage

Download Nov98Wicked.exe (82KB)

Jeff Prosise is the author of Programming Windows 95 with MFC (Microsoft Press, 1996). He also teaches Visual C++®/MFC programming seminars. For more information, visit http://www.solsem.com.

Windows®-based programming is full of myths. One is that it's difficult to allocate shared memory—memory that's visible to two or more processes—in Win32®-based applications.
      In truth, shared memory is a breeze to allocate and use; you just have to know how. The secret is a pair of API functions named CreateFileMapping and MapViewOfFile. CreateFileMapping creates file mapping objects that permit files to be browsed as easily as memory. MapViewOfFile converts file mapping object handles into pointers. Passing CreateFileMapping a file handle equal to 0xFFFFFFFF creates a file mapping object that's backed by the system's paging file. Calling MapViewOfFile on the resultant file mapping object is a cheap and effective way to access a region of memory from multiple processes.
      Aside from a few well-known pitfalls, such as passing in shared memory a pointer to an item elsewhere in shared memory (such pointers require special handling because shared memory may be mapped to a different address in each process that it's mapped into), shared memory could hardly be easier. But it would be simpler if the semantics for allocating and using blocks of shared memory were encapsulated in a reusable C++ class. Encapsulation could also eliminate some sources of resource leaks, such as the failure to call UnmapViewOfFile on a pointer returned by MapViewOfFile.
Figure 1 Share
Figure 1 Share

      Writing a class that provides an interface to shared memory isn't rocket science. Share, the sample program shown in Figures 1 and 2, features an MFC-style class named CSharedMemory that's perfect for programming projects that require shared memory. You can see CSharedMemory at work by running two instances of the application. Type a string of text into the box next to the Put button in one instance and click Put. Then click the Get button in the other instance. The text you typed in the first instance should appear in the second instance. This transfer is performed using a block of shared memory that's created when each process is started.

Creating and Initializing CSharedMemory Objects
      Allocating a block of memory that's visible to two or more processes is as simple as creating a named CSharedMemory object in each process and using the same name for each object. Share uses a COM-style GUID to name the object, which virtually eliminates the possibility of a name collision:

 m_pMem = new CSharedMemory (
     256,
     _T ("394D15E1-2481-11D2-BA24-E3E9C2C82A0F")
 );
The first parameter passed to CSharedMemory's constructor is the size (in bytes) of the shared memory block; the second is the object name. Share guards against possible out-of-memory errors by enclosing the call to new in a try/catch handler. CSharedMemory throws an MFC CMemoryException if its constructor fails.
      CSharedMemory also supports the creation of uninitialized shared memory objects and the recycling of existing ones. To create an uninitialized shared memory object, use CSharedMemory's default constructor, like this:
 CSharedMemory* psm = new CSharedMemory;
Then call CSharedMemory::Create to initialize the object:
 psm->Create (
     256,
     _T ("394D15E1-2481-11D2-BA24-E3E9C2C82A0F")
 );
If you want to recycle the object, call CSharedMemory::Delete on it before calling Create a second time:
 psm->Delete ();
 psm->Create (
     4096,
     _T ("39FC04E1-2DD4-11d2-BA24-98EAFC7FBE7C")
 );
There's no need to call Delete explicitly if the object instance won't be recycled because CSharedMem-ory's destructor will call it for you.
      You may specify NULL for the object name (or simply omit the object name altogether) when creating or initializing a CSharedMemory object, and CSharedMemory will pick a name for you. The name is guaranteed to be unique because CSharedMemory uses a stringified form of the value returned by COM's CoCreateGuid function to form the object name. To retrieve the name, call CSharedMemory:: GetName:
 CSharedMemory* psm = new CSharedMemory (256);
 CString name = psm->GetName ();
To connect to this shared memory object from another process, you must use some form of IPC to supply the object name to the other process. Using the same object name in two or more processes is the only way to join two CSharedMemory objects across process boundaries.
      In some cases, the first process to allocate a block of shared memory needs to initialize it further by copying data to it. So that callers can determine whether a newly constructed CSharedMemory object allocated a new block of shared memory (that is, created a new file mapping object) or connected to an existing one, CSharedMemory includes a member function named MeFirst. MeFirst returns nonzero if and only if the calling process was the first to create the corresponding shared memory object. Thus, the code to create and fully initialize a shared memory object might look like this:
 try {
     CSharedMemory* psm = new CSharedMemory;
     psm->Create (
         256,
         _T ("394D15E1-2481-11D2-BA24-E3E9C2C82A0F")
     );
 
     if psm->MeFirst () {
         // Write data to the block.
     }
     else {
         // Don't write data to the block; it has
         // already been written.
     }

      •
      •
      • } catch (CMemoryException* e) { // Handle the exception. e->Delete (); }
      The value returned by MeFirst comes from a member variable that is initialized following a call to GetLastError during the shared memory object's creation. GetLastError returns ERROR_ALREADY_EXISTS if CreateFileMapping returns a handle to an existing file mapping object. MeFirst returns FALSE if GetLastError returned ERROR_ ALREADY_EXISTS, or TRUE if it returns anything else.

Reading and Writing Shared Memory
      The public member variable p holds a pointer to the memory encapsulated by a CSharedMemory object. Reading or writing shared memory is as simple as dereferencing the pointer. The following statement copies a structure of type DATASTRUCT from shared memory into a stack-based variable:

 DATASTRUCT ds = *((DATASTRUCT*) psm->p);
The cast is required because p is a void*.
      One drawback to using pointers for memory access is the lack of protection against accidental buffer overruns. For this reason, CSharedMemory provides alternative methods for reading and writing shared memory in the form of member functions named Read and Write. If you add the beginning offset for a read or write operation to the size of the data being read or written and compare the result to the size of the shared memory buffer, both functions can prevent illicit reads and writes. The following code snippet uses CSharedMemory::Read to copy a DATASTRUCT from shared memory:
 DATASTRUCT ds;
 DWORD dwBytesRead;
 
 BOOL bSuccess = psm->Read (
     &ds,           // Destination for data
     sizeof (ds),   // Number of bytes to read
     &dwBytesRead,  // Buffer for count of bytes read
     0              // Offset into shared memory buffer
   );
 
 if (!bSuccess) {
     // Uh-oh!
 }
This code is functionally equivalent to the sample code shown in the previous paragraph.
      Both Read and Write return BOOLs that indicate whether the operation succeeded or failed. A zero (FALSE) return indicates that no data was read or written because the starting offset specified a location that lay beyond the end of the shared memory block. A nonzero (TRUE) return means that a read or write was performed, either in full or in part. It does not mean that the number of bytes read or written matches the requested byte count. If, for example, an application attempted to write 10 bytes to a location just two bytes short of the end of the shared memory buffer, Write would return true but set the variable passed by address in Write's third parameter to 2.

Synchronizing Reads and Writes
      Most apps that communicate through shared memory use mutexes, events, or other synchronization objects to coordinate reads and writes. Allowing one process to read shared memory while another writes to it is a recipe for disaster if the addresses overlap.
      To simplify matters, a CSharedMemory object contains an embedded mutex that may be used to prevent two processes from accessing shared memory at once. The mutex is exposed through CSharedMemory's Lock and Unlock member functions. The former locks the shared memory block by attempting to acquire the mutex. If the mutex is currently owned by someone else, Lock will block access until the mutex comes free. Unlock frees a mutex claimed with Lock. What's the bottom line? Synchronizing access to shared memory by two or more processes is as simple as bracketing any code that reads or writes shared memory with calls to Lock and Unlock:

 psm->Lock ();
 DATASTRUCT ds = *((DATASTRUCT*) psm->p);
 psm->Unlock ();
Calls to CSharedMemory::Read and CSharedMemory:: Write must also be bracketed in this manner to ensure thread-safe access.

Have a tricky issue dealing with Windows? Send your questions via email to Jeff Prosise: JeffPro@msn.com

From the November 1998 issue of Microsoft Systems Journal.