June 16th, 2026
intriguingcelebratelike3 reactions

Retrofitting the WM_COPY­DATA message onto Windows 3.1

Some time ago, I talked about how to return results back from the WM_COPY­DATA message. Which reminded me of a clever bit of history.

The WM_COPY­DATA message was introduced in 32-bit Windows. There was no need for it in 16-bit Windows because all 16-bit programs ran in the same address space. A far pointer in one process was good in any process. You could put it in the lParam of a window message and send it to any other window, same process or different process, doesn’t matter. But 32-bit programs ran in separate address spaces, so this trick didn’t work. Hence the need for WM_COPY­DATA to pass data not only between 32-bit programs, but also between 32-bit programs and 16-bit programs.

How did this message get retrofitted into 16-bit Windows so that Win32s could support it?

Easy: It was already implemented, unwittingly.

If the source and destination windows are both 16-bit windows, then the pointer to the COPY­DATA­STRUCT is already valid in both processes, as is the pointer inside the COPY­DATA­STRUCT. And the window handle in the wParam is also the same for both processes. Therefore, doing absolutely nothing with the wParam and lParam and simply allowing it to pass from a 16-bit program to another 16-bit program will still behave as expected.

And it so happens that Windows 3.1 already did that: Windows 3.1 always passed the wParam and lParam unmodified, even when the message sender and receiver are in different processes, because all programs shared the same address space.

It was just a sneaky trick to design the WM_COPY­DATA message in such a way that the null marshaler is the correct behavior when it is sent between 16-bit programs.

Topics

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

6 comments

Sort by :
  • Steve 5 hours ago

    @Jimc, in the days of cooperative multitasking, I think SendMessage() was fully synchronous to ensure use-after-free didn’t happen.

  • Jimc 5 days ago

    Ah, but what concurrency “fun” if either 16-bit sender or receiver decided to modify the data or the sender deallocated it after sending. The joys of a 16-bit OS.

    • Me Gusta 4 days ago

      While I would have to do some more research, I don't think the constraints of WM_COPYDATA has changed that much since the early years.

      WM_COPYDATA has three major constraints. First, The sender must not modify the buffer until the receiver has finished. Second, the receiver must see the sent data as read only. Third, the data is only guaranteed to exist while the call to SendMessage is in progress, meaning that the receiver must copy the data before it returns or calls ReplyMessage. Finally, the current documentation heavily suggests that this message is only ever used with SendMessage. So if you...

      Read more
    • LB 5 days ago

      Isn’t that impossible due to the cooperative scheduling?

      • LB 4 days ago

        @Jimc I don’t think the scenario you describe is possible using SendMessage, it blocks until the message is processed at the other end, no?

      • Jimc 4 days ago

        No. It was very much possible. The hardware of the time was single core CPUs which could only execute one thread at a time. Consequently, only one of the programs would be executing at a time and on Win 3.x they would be sharing cooperatively. This may lead you to think it was safe to transfer data using WM_COPYDATA with the data in memory accessible to both sender and receiver. However, imagine the scenario where sender sends data and then yields. There is no guarantee the data will be received and *processed* before the sender was executed again. At this...

        Read more