Featured image of post Evolution of IPC Apis in Windows

Evolution of IPC Apis in Windows

How the Ability to do Inter Process Communication (IPC) evolved From Windows 1.0 to 11

Design Evolution of the Microsoft Windows Welcome Screen
https://www.versionmuseum.com/history-of/all-microsoft-windows-splash-title-screens

Evolution of Cross-Process Communication in Windows

The History of Windows: From Single-Tasking to Modern Multi-Process Communication

Once upon a time, in the golden era of computing (cue nostalgic 8-bit music), Windows was a far cry from the multi-threaded, process-isolated behemoth it is today.

  • Windows 1.0 (1985) – A GUI shell over MS-DOS. Nobody really cared.

  • Windows 2.0 (1987) – Slightly better, but still awkward.

  • Windows 3.0 (1990) – My personal favorite! This was a real game-changer.
  • Windows 3.11 for Workgroups (1993) – The most popular version of early Windows. It introduced networking support, making it a staple for offices.

  • Windows 95 (1995) – Goodbye, 16-bit era! Hello, true multi-tasking.

  • Windows NT (1993 - today) – True multi-user, multi-process OS with actual security (gasp!).

  • Windows XP, 7, 10, 11 – Evolution continues, and somewhere along the way, Microsoft decided people didn’t need to control their own computers anymore.

Windows 3.x: The Era of Message Passing

Back in the 16-bit days, everything ran in the same address space. There was no such thing as process isolation; your app could poke around in another app’s memory like a nosy neighbor.

The operating system was single-threaded. The way we handled inter-application communication was through message passing.

Windows provided two main functions for message handling:

  • GetMessage() – Waits for a message and retrieves it.
  • PeekMessage() – Checks for a message but doesn’t wait.

The Classic Windows Message Pump

All GUI applications had a message pump that looked like this:

1
2
3
4
5
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

This was the lifeblood of every Windows app.

Sending Messages Between Windows Applications

Back in the Windows 3.x days, because everything was in the same address space, you could actually send a pointer to memory between “applications” (which were actually just different instances in the same memory space).

Here’s an example of sending a registered Windows message between two windows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <windows.h>

#define WM_MY_MESSAGE (WM_USER + 1)

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    if (msg == WM_MY_MESSAGE) {
        MessageBox(hwnd, "Received a custom message!", "Message", MB_OK);
        return 0;
    }
    return DefWindowProc(hwnd, msg, wParam, lParam);
}

void SendCustomMessage(HWND target) {
    SendMessage(target, WM_MY_MESSAGE, 0, 0);
}

This allowed communication between applications, but again, they were all in the same process.

Modern Windows: Cross-Process Communication Methods

With the advent of Windows NT and true process isolation, sharing memory like a free-for-all became a big no-no. Instead, we got proper cross-process communication methods:

1. Named Pipes

Named pipes allow bidirectional communication between processes.

Example: Server

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <windows.h>
#include <stdio.h>

#define PIPE_NAME "\\\\.\\pipe\\MyPipe"

int main() {
    HANDLE hPipe = CreateNamedPipe(PIPE_NAME, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
                                   1, 512, 512, 0, NULL);
    
    if (hPipe == INVALID_HANDLE_VALUE) {
        printf("Error creating named pipe\n");
        return 1;
    }

    printf("Waiting for client connection...\n");
    ConnectNamedPipe(hPipe, NULL);

    char buffer[512];
    DWORD bytesRead;
    ReadFile(hPipe, buffer, sizeof(buffer), &bytesRead, NULL);

    printf("Received: %s\n", buffer);

    CloseHandle(hPipe);
    return 0;
}

Example: Client

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <windows.h>
#include <stdio.h>

#define PIPE_NAME "\\\\.\\pipe\\MyPipe"

int main() {
    HANDLE hPipe = CreateFile(PIPE_NAME, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

    if (hPipe == INVALID_HANDLE_VALUE) {
        printf("Error connecting to named pipe\n");
        return 1;
    }

    const char *message = "Hello from client!";
    DWORD bytesWritten;
    WriteFile(hPipe, message, strlen(message) + 1, &bytesWritten, NULL);

    CloseHandle(hPipe);
    return 0;
}

2. Signals (Event Objects)

Windows supports event objects for signaling between processes.

1
2
3
4
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, "Global\\MyEvent");

SetEvent(hEvent);
WaitForSingleObject(hEvent, INFINITE);

3. Slots (MailSlots)

Mail slots allow one-to-many communication.

Sender:

1
2
3
HANDLE hSlot = CreateFile("\\\\.\\mailslot\\MySlot", GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
WriteFile(hSlot, "Hello", 6, NULL, NULL);
CloseHandle(hSlot);

Receiver:

1
2
3
HANDLE hSlot = CreateMailslot("\\\\.\\mailslot\\MySlot", 0, MAILSLOT_WAIT_FOREVER, NULL);
ReadFile(hSlot, buffer, sizeof(buffer), NULL, NULL);
CloseHandle(hSlot);

Conclusion

From the wild-west days of direct memory sharing to the structured world of named pipes, events, and mail slots, Windows cross-process communication has evolved significantly.

Sure, we lost the reckless fun of passing pointers between apps, but at least our programs no longer crash because some rogue app decided to overwrite half of our memory. Ah, progress.