When engineering high-frequency trading (HFT/HTS) systems or mission-critical C++ applications where zero runtime exceptions are permitted, understanding Inter-Process Communication (IPC) and OS-level memory lifetime management is crucial.

This post covers the complete architectural spectrum: common memory pitfalls in WM_COPYDATA, high-throughput Zero-Copy Shared Memory (File Mapping) internals, and nanosecond-latency Lock-Free (SPSC/MPMC) Ring Buffers with C++11 std::atomic memory ordering.


1. WM_COPYDATA Internals & Why Receivers Must NEVER Call Delete

A common mistake when sending data via WM_COPYDATA from Process A to Process B is assuming Process B should call delete once finished. Doing so results in immediate Access Violation crashes.

🚨 Why Calling Delete in Process B Causes Immediate Crashes:
  • Independent Virtual Address Spaces: Every Windows process resides in its own isolated virtual address space. Process A's heap and Process B's heap are completely separate.
  • Heap Manager Conflict: Memory allocated via new in Process A is tracked exclusively by Process A's heap manager. When Process B attempts to delete this pointer, Process B's heap manager detects an unknown pointer and aborts the process instantly.

WM_COPYDATA Kernel Lifecycle

  1. SendMessage Transmission: Process A populates COPYDATASTRUCT and invokes SendMessage. (Asynchronous PostMessage is strictly forbidden as the OS cannot guarantee memory lifetime).
  2. Kernel Mapping: The Windows Kernel maps/copies Process A's memory into Process B's virtual address space temporarily.
  3. Handler Return & Auto-Unmapping: As soon as Process B's WM_COPYDATA handler returns, the Windows kernel invalidates the temporary mapping in Process B instantly.
πŸ’‘ Correct Receiver Pattern (Deep Copy):
If Process B needs to process the received payload asynchronously in a queue or worker thread, Process B must allocate its own heap memory and perform a Deep Copy before the message handler returns.

2. WM_COPYDATA vs Shared Memory (File Mapping)

For high-frequency market tick feeds or high-throughput data streams, developers should use Shared Memory (File Mapping) instead of WM_COPYDATA to eliminate copying overhead.

πŸ“Š IPC Paradigm Comparison

  • WM_COPYDATA: Kernel memory copy occurs, slower, auto-synchronized via SendMessage, destroyed on handler return
  • Shared Memory: Zero-Copy (shared physical RAM), extremely fast, manual synchronization (Mutex/Event/Lock-Free), lives until ref-count hits 0

Common Misconception: "Is Shared Memory Stored on Hard Disk?"

Passing INVALID_HANDLE_VALUE as the first parameter to CreateFileMapping backs the mapping via the OS Pagefile, operating entirely within high-speed physical RAM. Unless system RAM is exhausted forcing swap-out, Disk I/O is 0 Bytes.

⚠️ Kernel Reference Counting & Cleanup:
Shared memory is managed via kernel object reference counts. When Process A calls CloseHandle, the shared memory persists if Process B is still attached. The OS reclaims physical memory only when all processes unmap and close handles, dropping the reference count to 0.

3. Lock-Free SPSC (Single-Producer Single-Consumer) Ring Buffer

Kernel synchronization objects like Mutex or Event introduce User ↔ Kernel mode context switching overhead. Ultra-low latency systems rely on User-Mode Lock-Free Ring Buffers using atomic CPU instructions.

SPSC Ring Buffer & Memory Ordering (C++11)

// Shared Memory Control Block Layout
struct ControlBlock {
    std::atomic<size_t> head{0}; // Updated by Consumer
    uint8_t padding1[56];        // 64-byte Cache Line Padding (Prevents False Sharing)
    
    std::atomic<size_t> tail{0}; // Updated by Producer
    uint8_t padding2[56];
};

// 1. Producer Logic
void Produce(const DataPacket& packet) {
    size_t current_tail = control->tail.load(std::memory_order_relaxed);
    
    ring_buffer[current_tail] = packet;
    
    // memory_order_release guarantees buffer writes complete before updating Tail
    size_t next_tail = (current_tail + 1) % RING_BUFFER_SIZE;
    control->tail.store(next_tail, std::memory_order_release);
}

// 2. Consumer Logic
bool Consume(DataPacket& outPacket) {
    // memory_order_acquire prevents reads from being reordered prior to Tail load
    size_t current_tail = control->tail.load(std::memory_order_acquire);
    size_t current_head = control->head.load(std::memory_order_relaxed);
    
    if (current_head == current_tail) {
        return false; // Queue empty
    }
    
    outPacket = ring_buffer[current_head];
    
    size_t next_head = (current_head + 1) % RING_BUFFER_SIZE;
    control->head.store(next_head, std::memory_order_release);
    return true;
}
πŸ”₯ Pro Tip: Cache Line Padding to Prevent False Sharing:
If Head and Tail variables reside within the same 64-byte CPU cache line, CPU cores invalidate each other's cache constantly. Inserting uint8_t padding[56] separates Head and Tail into distinct cache lines, unlocking maximum throughput.

4. MPMC Lock-Free, CAS (Compare-And-Swap) & The ABA Problem

In Multi-Producer Multi-Consumer (MPMC) architectures, multiple processes compete to claim indices using CAS (Compare-And-Swap).

// Producer Enqueue CAS Loop (C++11)
void MPMC_Enqueue(const DataPacket& data) {
    size_t currentTail = tail.load(std::memory_order_relaxed);
    size_t nextTail;

    while (true) {
        nextTail = (currentTail + 1) % QUEUE_SIZE;

        // Atomic CAS Attempt
        if (tail.compare_exchange_weak(currentTail, nextTail,
                                       std::memory_order_release,
                                       std::memory_order_relaxed)) {
            // Success: currentTail slot claimed exclusively!
            break;
        }
        // Failure: Another process updated tail. Retry loop with updated currentTail
    }

    slots[currentTail].data = data;
    slots[currentTail].status.store(SLOT_READY, std::memory_order_release);
}

The ABA Problem & Its Solution

If Process 1 reads state A, gets preempted, and another process transitions the state A β†’ B β†’ A, Process 1's CAS succeeds incorrectly despite corrupted underlying data structures.

πŸ”‘ Resolving ABA via Sequence Numbering:
Instead of comparing index values alone, pack a 64-bit monotonically increasing **Sequence Number** into the atomic variable. Even if the value reverts to A, the altered sequence number causes CAS to detect changes and fail correctly.

5. Summary

  • Receiving processes in WM_COPYDATA must never call delete and must perform a Deep Copy inside the handler.
  • High-throughput IPC uses CreateFileMapping Zero-Copy Shared Memory, managed via kernel reference counts.
  • Ultra-low latency systems bypass Mutex overhead by combining SPSC/MPMC Lock-Free Ring Buffers, C++11 atomic memory ordering, CAS, and cache-line padding.