Mutex, Shared Memory Inter-Process Communication Methods in Windows
Operating systems enforce virtual memory isolation to prevent one application from corrupting or inspecting the memory of another. While this boundary is essential for stability and security, modern software architectures frequently require independent processes to exchange data, coordinate tasks, or share real-time state. I've addressed the necessary issues.
In is it Windows user-mode, Inter-Process Communication (IPC) provides several mechanisms to cross this process boundary. Choosing the right technique depends on bandwidth requirements, latency tolerance, complex synchronization needs, and whether the processes share a parent-child relationship. It's essential.
1. Shared Memory via Memory-Mapped Files
Shared memory; is the fastest form of IPC available on Windows because data does not need to be copied through the kernel for every transfer. Once the shared memory region is mapped into the virtual address space of both processes, writes by one process are immediately visible to the other at hardware memory speeds.
Under the hood, Windows implements shared memory using Pagefile-Backed Memory-Mapped Files. Instead of mapping a physical file on disk, you pass INVALID_HANDLE_VALUE as the file handle to CreateFileMappingW. The OS allocates pages from the system paging file and assigns a global name to the mapping object.
Creating and Writing to Shared Memory
The producing process creates the named mapping and maps a view of it into its virtual address space:
#include <windows.h>
#include <iostream>
struct SharedPayload {
DWORD counter;
wchar_t message[256];
};//struct cpp
int main() {
const wchar_t* mappingName = L"Local\\MyCustomSharedMemory";
SIZE_T bufferSize = sizeof(SharedPayload);// sixeof SharedPayloaf
// Create a pagefile-backed file mapping object
HANDLE hMapFile = CreateFileMappingW(
INVALID_HANDLE_VALUE, // Backed by system paging file
NULL, // Default security attributes
PAGE_READWRITE, // Read/write access
0, // High-order DWORD of size
static_cast<DWORD>(bufferSize), // Low-order DWORD of size
mappingName // Name of the mapping object
);
if (hMapFile == NULL) {
std::cerr << "CreateFileMappingW failed: " << GetLastError() << '\n';
return 1;
}
// Map a view of the memory into the process address space
SharedPayload* pPayload = static_cast<SharedPayload*>(
MapViewOfFile(
hMapFile,
FILE_MAP_ALL_ACCESS,
0, 0, bufferSize
)
);
if (pPayload == NULL) {
std::cerr << "MapViewOfFile failed: " << GetLastError() << '\n';
CloseHandle(hMapFile);
return 1;
}
// Write data directly to the shared memory view
pPayload->counter = 1001;
wcscpy_s(pPayload->message, L"Hello from Producer Process!");
std::cout << "Data written to shared memory. Press ENTER to release...\n";
std::cin.get();
// Cleanup
UnmapViewOfFile(pPayload);
CloseHandle(hMapFile);
return 0;
}Reading from Shared Memory
The consuming process uses OpenFileMappingW with the same global name to map the existing region:
#include <windows.h>
#include <iostream>
struct SharedPayload {
DWORD counter;
wchar_t message[256];
};
int main() {
const wchar_t* mappingName = L"Local\\MyCustomSharedMemory";
// Open the existing named memory mapping
HANDLE hMapFile = OpenFileMappingW(
FILE_MAP_READ, // Read-only access
FALSE, // Do not inherit handle
mappingName
);
if (hMapFile == NULL) {
std::cerr << "OpenFileMappingW failed: " << GetLastError() << '\n';
return 1;
}
SharedPayload* pPayload = static_cast<SharedPayload*>(
MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, sizeof(SharedPayload))
);
if (pPayload == NULL) {
std::cerr << "MapViewOfFile failed: " << GetLastError() << '\n';
CloseHandle(hMapFile);
return 1;
}
std::wcout << L"Counter: " << pPayload->counter << L"\n";
std::wcout << L"Message: " << pPayload->message << L"\n";
UnmapViewOfFile(pPayload);
CloseHandle(hMapFile);
return 0;
}2. Synchronizing Shared Memory Access with Named Mutexes
Because shared memory allows concurrent access from multiple virtual address spaces, access to the shared buffer is inherently prone to race conditions. If one process writes to the buffer while another is reading, memory corruption or torn reads will occur.
To synchronize access across different processes, Windows provides Kernel Named Synchronization Primitives (Mutexes, Semaphores, and Events). Unlike standard std::mutex which operates only within a single process, a named kernel Mutex can be acquired by any process that knows its string name.
#include <windows.h>
#include <iostream>
class ScopedNamedMutex {
private:
HANDLE m_hMutex;
bool m_acquired;
public:
ScopedNamedMutex(const wchar_t* name) : m_acquired(false) {
// Create or open a named kernel mutex
m_hMutex = CreateMutexW(NULL, FALSE, name);
if (m_hMutex != NULL) {
// Wait up to 5 seconds to acquire ownership
DWORD result = WaitForSingleObject(m_hMutex, 5000);
if (result == WAIT_OBJECT_0) {
m_acquired = true;
}
}
}
bool isAcquired() const { return m_acquired; }
~ScopedNamedMutex() {
if (m_acquired) {
ReleaseMutex(m_hMutex);
}
if (m_hMutex != NULL) {
CloseHandle(m_hMutex);
}
}
};
// Usage in Producer/Consumer:
void SafelyWriteData() {
ScopedNamedMutex lock(L"Local\\MySharedMemoryMutex");
if (lock.isAcquired()) {
// Perform shared memory writes here safely
} else {
std::cerr << "Timed out waiting for IPC Mutex.\n";
}
}3. Stream-Based IPC: Named Pipes
While shared memory excels at high-speed data access, it requires manual memory layout and custom synchronization protocols. When your application requires a structured, stream-based, or message-oriented communication channel, Named Pipes are the standard mechanism.
Named Pipes operate in a client-server model. They support half-duplex or full-duplex communication, automatic blocking/non-blocking I/O, and built-in security descriptors to restrict access across user accounts.
Creating a Named Pipe Server
#include <windows.h>
#include <iostream>
void RunPipeServer() {
const wchar_t* pipeName = L"\\\\.\\pipe\\MyCustomPipe";
HANDLE hPipe = CreateNamedPipeW(
pipeName,
PIPE_ACCESS_DUPLEX, // Read/Write access
PIPE_TYPE_MESSAGE | // Message-type pipe
PIPE_READMODE_MESSAGE | // Message-read mode
PIPE_WAIT, // Blocking mode
1, // Max instances
1024, // Output buffer size
1024, // Input buffer size
0, // Default timeout
NULL // Default security
);
if (hPipe == INVALID_HANDLE_VALUE) {
std::cerr << "CreateNamedPipeW failed: " << GetLastError() << '\n';
return;
}
std::cout << "Waiting for client connection...\n";
if (ConnectNamedPipe(hPipe, NULL) || GetLastError() == ERROR_PIPE_CONNECTED) {
const char* response = "ACK: Request Processed";
DWORD bytesWritten = 0;
WriteFile(hPipe, response, static_cast<DWORD>(strlen(response)), &bytesWritten, NULL);
}
CloseHandle(hPipe);
}Named Pipe Client
A client application connects to a pipe as if it were a standard file on disk using CreateFileW:
#include <windows.h>
#include <iostream>
void RunPipeClient() {
const wchar_t* pipeName = L"\\\\.\\pipe\\MyCustomPipe";
HANDLE hPipe = CreateFileW(
pipeName,
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL
);
if (hPipe != INVALID_HANDLE_VALUE) {
char buffer[128] = {0};
DWORD bytesRead = 0;
if (ReadFile(hPipe, buffer, sizeof(buffer) - 1, &bytesRead, NULL)) {
std::cout << "Server Response: " << buffer << '\n';
}
CloseHandle(hPipe);
}
}4. Lightweight Message Passing: WM_COPYDATA
For desktop applications with Win32 window handles (HWND), Windows offers a lightweight, synchronous message-passing mechanism via the WM_COPYDATA window message.
WM_COPYDATA allows an application to pass a pointer to a COPYDATASTRUCT containing payload data. The Windows kernel intercepts the message, allocates temporary memory in the target process's space, copies the payload, and passes the updated pointer to the destination window procedure.
#include <windows.h>
// Sender Process
void SendDataToWindow(HWND hTargetWnd, DWORD dwTag, const void* pData, DWORD dataSize) {
COPYDATASTRUCT cds;
cds.dwData = dwTag; // Custom identifier tag
cds.cbData = dataSize; // Size of data in bytes
cds.lpData = const_cast<void*>(pData); // Pointer to data
// WM_COPYDATA MUST be sent via SendMessage (synchronous).
// PostMessage will fail because the buffer lifetime is tied to the call.
SendMessageW(hTargetWnd, WM_COPYDATA, reinterpret_cast<WPARAM>(NULL), reinterpret_cast<LPARAM>(&cds));
}
// Receiver Window Procedure (Inside target app's WndProc)
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
if (message == WM_COPYDATA) {
PCOPYDATASTRUCT pCds = reinterpret_cast<PCOPYDATASTRUCT>(lParam);
DWORD tag = static_cast<DWORD>(pCds->dwData);
const char* payload = static_cast<const char*>(pCds->lpData);
DWORD payloadSize = pCds->cbData;
// Process the data synchronously. The memory is freed immediately upon return.
return TRUE;
}
return DefWindowProcW(hWnd, message, wParam, lParam);
}5. Architectural Comparison and Trade-Offs
Selecting the appropriate user-mode IPC mechanism requires balancing throughput, architecture constraints, and implementation complexity:
* Shared Memory: Best for streaming large buffers, video frames, or high-frequency shared state (e.g., telemetry, shared game engine state). Requires explicit synchronization via named mutexes or events.
* Named Pipes: Best for client-server architectures, RPC-like streaming, and structured command handling between background services and desktop apps.
* WM_COPYDATA: Best for simple, low-frequency synchronous payloads between windowed GUI applications (e.g., passing command-line parameters to an already-running single-instance application).
* Loopback Sockets (TCP/UDP): Best when cross-platform compatibility is required or when the client and server may eventually be split across different physical machines on a network.
Finally is communication
Now you are knowledgeable about the necessary topics. How you proceed from here is up to you. Everything has its advantages and disadvantages.
Run code to view output...
Fetching engineering questions...