Win3mu - Part 13 - Window Messaging

Win3mu - Part 13 - Window Messaging

Win3mu is a Windows 3 Emulator that makes it possible to run old Windows games on modern versions of Windows. You can read about the rationale for this project here.

This post dives into the details of implementing the Windows API, starting with Window management.

Where We're At

In the previous articles on Win3mu I covered the foundations of the emulator — the CPU, module loading, thunking between 16-bit code and C#, memory management and a few other topics.

We’re now at the point where we can start talking about building the actual Windows 3 API.

Development Cycle

It’s probably worth mentioning the process I’ve been using for deciding which parts of the API’s to implement next. The Windows API consists of about 1,100 functions and several hundred messages.

My initial approach to deciding what methods to implement was to have the module loader throw an exception if any function used by the program wasn’t implemented and abort. ie: the program wouldn’t even start running until all the methods were present.

The problem with this approach is that it means implementing a lot of functions before being able to test any of them. Also, some programs import API functions but never actually call them — so why bother implementing them until a program actually needs them.

To get around this I updated the module loader to create thunks for all imported functions and those that aren’t implemented are mapped to a function that throws an exception. This allowed me to run the program and implement and test each function as it’s used. It breaks an otherwise huge job into much smaller pieces.

Typical error message highlighting an unimplemented function.

The process for working on Win3mu now looks like this:

  1. Point the emulator at a 16-bit program and run it.
  2. If it throws a not implemented exception then implement the missing parts.
  3. If it crashes then debug it to figure out why
  4. Use the program looking for anomalies and try to rectify them
  5. Repeat until the program works

Sometimes a single API can be large piece of work in itself. In those cases I’ll implement whatever’s required by the program and throw exceptions for parts that aren’t implemented.

I’ve been very careful to not blindly suppress things that aren’t implemented or aren’t working as expected. The slightest sign of something not quite right and Win3mu will throw an exception and terminate. I figure it’s the only way to keep track of what’s working and what’s not.

The Challenge

In one of the very early articles about Win3mu I mentioned that I expected implementing the User module to be the most difficult part of the whole project. Since we’re now at this point let’s think about the problems here a little.

If you’re not familiar with the Windows API, there are two main parts to it:

  • API Methods — a large set of functions that a 16-bit program can call
  • Window Messages — a set of messages that can be sent or posted to a Window to tell it something has happened. eg: mouse move, key press, window resize etc…

Window messages are delivered to a window through a Window Procedure, (also known as a WNDPROC) which is essentially a function with the following signature:

LRESULT MyWindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

The first parameter hWnd is the window handle and is relatively easy to map between the two environments. The bigger problem is the message, wParamand lParamparameters.

Depending on the message, wParam and lParam can mean different things — sometimes they’re unused, sometimes they’re simple integer values, sometimes they’re string or other pointers and in the worst case they can point to another structure which in-turn contains pointers to other information.

What’s even worse, the meaning of the parameters for a particular message number might change depending on the window receiving the message. eg: the standard ListBox and ComboBox controls use the same message number for different messages.

This is the problem Win3mu needs to solve — all these messages need to be converted between the 16-bit program and the host operating system.

Handle Mapping

Windows uses handles as identifiers for system objects. There are window handles, menu handles, device context handles, GDI object handles and others.

The problem for Win3mu is that on the host operating system a handle is 32 or 64 bits (depending if running on x86 or x64) whereas 16-bit Windows use 16-bit handles.

Obviously there needs to be a mapping table between the two environments which is the job of the HandleMap class — a simple two way mapping using two dictionaries:

public class HandleMap
{
    // Map between environments
    public IntPtr To32(ushort handle16)
    public ushort To16(IntPtr handle32)

    // Remove destroyed handles
    public void Destroy32(IntPtr handle32);
    public void Destroy16(ushort handle16);
    
    // Check if a handle is known
    public bool IsValid16(ushort handle16)

    // The map
    Dictionary<IntPtr, ushort> _map32to16 = new Dictionary<IntPtr, ushort>();
    Dictionary<ushort, IntPtr> _map16to32 = new Dictionary<ushort, IntPtr>();
    
    // Next unused handle
    ushort _nextHandle = 32;
}

Win3mu has multiple instances of the handle map for each handle type. It also defines a MappedType struct for each handle type so that Module16’s parameter mapping can automatically map them:

[MappedType]
public struct HDC
{
    // Win32 handle value
    public IntPtr value;  
    
    // The map
    public static HandleMap Map = new HandleMap();

    // Methods required by MappedType's
    public static HDC To32(ushort hDC) { return new HDC() { value = Map.To32(hDC) }; }
    public static ushort To16(HDC hDC) { return Map.To16(hDC.value); }
    
    // Other helpers
    public static implicit operator HDC(IntPtr value) { return new HDC() { value = value }; }
    public static HDC Null = IntPtr.Zero;
}

Window Procedures

Window Procedures are a function that’s called to deliver a message to a window. Programs pass the address of the window procedure to Windows and Windows calls it when there’s a message to be delivered.

Obviously Windows can’t call a 16-bit window procedure directly because 16-bit code needs to run on the emulated CPU. Instead Win3mu creates a proxy Window procedure that maps the message parameters and calls the 16-bit code.

The other direction also needs to be handled. Most window procedures don’t handle all messages and those that aren’t need to be passed to Windows’ default window procedure — which means 16-bit code needs to be able to call a Win32 window procedure (converting the message parameters back again in the process).

The nasty business of converting messages will be described in the next post suffice to say that there are two functions CallWndProc16From32 and CallWinProc32From16 which do the conversion and call a target window procedure:

// Call a Win32 window procedure from 16-bit code
public uint CallWndProc32from16(IntPtr pfnProc32, ushort hWnd16, ushort message16, ushort wParam16, uint lParam16)

// Call a 16-bit window procedure from Win32 code
public IntPtr CallWndProc16from32(uint pfnProc16, IntPtr hWnd32, uint message32, IntPtr wParam32, IntPtr lParam32)

Given these two helper functions that do the actual message conversion it’s now fairly easy to create a 16-bit window procedure that’ll call a Win32 window procedure (and vice versa):

// Wrap a 16-bit virtual window procedure in a managed delegate that
// when invoked will call the virtual proc.  
public IntPtr GetWndProc32(uint lpfnWndProc)
{
    if (lpfnWndProc == 0)
        return IntPtr.Zero;

    // Check if already wrapped
    IntPtr wndProc32 = WndProcMap.To32(lpfnWndProc);
    if (wndProc32!=IntPtr.Zero)
        return wndProc32;

    // Nope, create one
    Win32.WNDPROC wndProc32Managed = (hWnd, message, wParam, lParam) =>
    {
        return CallWndProc16from32(lpfnWndProc, hWnd, message, wParam, lParam);
    };

    // Connect
    return WndProcMap.Connect(wndProc32Managed, lpfnWndProc);
}

// Create a 16-bit thunk that will call a Win32 window procedure
public uint GetWndProc16(IntPtr lpfnWndProc32, bool create = true)
{
    if (lpfnWndProc32 == IntPtr.Zero)
        return 0;

    // Already wrapped?
    uint wndProc16 = WndProcMap.To16(lpfnWndProc32);
    if (wndProc16 != 0)
        return wndProc16;

    if (!create)
        return 0;

    // Create a 16-bit thunk that calls it
    wndProc16 = _machine.CreateSystemThunk(() =>
    {
        ushort hWnd16 = _machine.ReadWord(_machine.ss, (ushort)(_machine.sp + 12));
        ushort message16 = _machine.ReadWord(_machine.ss, (ushort)(_machine.sp + 10));
        ushort wParam16 = _machine.ReadWord(_machine.ss, (ushort)(_machine.sp + 8));
        uint lParam16 = _machine.ReadDWord(_machine.ss, (ushort)(_machine.sp + 4));
   
       _machine.dxax = CallWndProc32from16(lpfnWndProc32, hWnd16, message16, wParam16, lParam16);

    }, 10, false, "WndProc32");      // hWnd = 2, msg = 2, wParam = 2, lParam = 4

    // Update maps
    WndProcMap.Connect(lpfnWndProc32, wndProc16);

    // Done
    return wndProc16;
}

WndProcMap is similar to HandleMap and maintains a two-way mapping between these window procedure thunks and delegates.

When a 16-bit program passes a 16-bit window procedures to the emulator (through an emulated API like RegisterClass for example) Win3mu calls GetWndProc32 to create a native window procedure that Windows can call — and when called will invoke the 16-bit code.

The opposite also works — if the 16-bit program asks for the window procedure of a Win32 window (eg: suppose it’s sub-classing a standard control) GetWndProc16 will take that native window procedure and generate a 16-bit thunk that the 16-bit program can call — and when called will invoke the Win32 code.