In the past few days, I embarked upon a project to port Antoni Sawicki’s aclock, a small text-based clock program (aclock stands for analog clock), to 16-bit Windows. While aclock itself has been ported to over 150 platforms, it is a console program, so a chunk of new Windows-specific code had to be written.
For guidance I went to Charles Petzold’s classic, Programming Windows, in the second edition which covers Windows 3.0. To keep things simple, the Windows version of aclock chooses one of the stock fixed-pitch fonts offered by Windows, calculates how many characters fit into the application’s window horizontally and vertically, and treats the window as a text console in order to draw the clock. The clock is updated every second and resized if the window size changes.
My first target was Windows 3.1, running both in a VM under real Windows 3.1 and in the WoW subsystem on modern 32-bit Windows. The compiler I used was Open Watcom, because I’m familiar with it and because it can target both 16-bit and 32-bit Windows. Porting to Windows 3.1 was relatively easy, and as an unexpected but welcome bonus I ended up with code that can be built as both 16-bit and 32-bit Windows application.
However, I hit the first snag when trying to run the executable on Windows 3.0. It just wasn’t working. After a bit of head-scratching, it turned out that all the exhortations about exporting the window procedure from the module were right—for Windows 3.0 and older. Windows 3.1 is smart enough to realize that a window procedure passed to the RegisterClass API needs a special wrapper and takes care of things. Adding the __export keyword to the window procedure solved the problem for Windows 3.0. Since the Open Watcom linker already marks the executable as requiring Windows 3.0 by default, no other changes were necessary. The executable worked in real, standard, and enhanced 386 mode of Windows 3.0, as well as standard and enhanced 386 mode of Windows 3.1, and of course enhanced 386 mode of Windows 3.11 for Workgroups.
The next step was porting to Windows 2.0, and that turned out to be a bit more involved. While the Open Watcom toolset is suitable for building Windows 2.0 executables, the run-time libraries are not. Which is not entirely surprising, as the Watcom compilers never supported Windows 2.x development (indeed extremely few or no non-Microsoft compilers ever did). There were two basic problems. First, the run-time libraries called APIs which only exist on Windows 3.x. Second, prior to the advent of Windows 3.0, there was very poor support for x87 floating-point math in Windows.
Neither issue was insurmountable, but I decided to take the easy way out and installed Microsoft C 5.1 and the Windows 2.0 SDK in a VirtualBox VM running MS-DOS 4.01. It should be noted that the old Windows SDKs were very much tied to Microsoft compilers, because they replaced significant chunks of the compilers’ run-time libraries with Windows-specific code. For example, malloc() would be changed to use LocalAlloc rather than the DOS memory allocation interface.
The too-new-API problem was implicitly solved by using an old SDK. To fix the FPU issue, I used the -FPa switch (link with alternate math libraries, Microsoft’s software-only floating-point implementation) recommended by the Windows SDK documentation. Luckily I have printed documentation for both Microsoft C 5.1 and the Windows 2.0 SDK. Rather annoyingly, the C 5.1 compiler documentation does not explain any Windows-specific switches and refers to the Windows SDK documentation instead, but everything is reasonably well documented in one manual or the other.
Building the source code with Microsoft C 5.1 was easy, but did not result in a functional executable. Windows seemed to refuse to run the executable, but gave no reason as to why. After examining a few sample programs, I arrived at the conclusion that Microsoft’s linker (i.e. LINK4.EXE shipped with the Windows SDK) does not use reasonable defaults for stack and heap size (I’d been spoiled by the Open Watcom linker which provides sensible defaults). I had to write a simple .DEF file to supply the appropriate HEAPSIZE and STACKSIZE statements. With those changes in place, lo and behold, aclock was working on Windows 2.0.
The final step was porting aclock to Windows 1.x. I decided from the beginning to use the Windows 1.04 SDK and test on Windows 1.04. I have the disks and the printed documentation, so what could possibly go wrong? The process was of course not entirely smooth.
The Windows 1.03 and 1.04 SDKs only support Microsoft C 4.0 (again because the SDK replaces a large chunk of the compiler’s run-time libraries). The Windows-specific code had to be changed slightly, as Windows 1.x did not understand the WS_OVERLAPPEDWINDOW style (since all windows were tiled!). However, on the whole there were remarkably few changes in the basic Windows API between 1.0 and 3.1.
A worse problem was that Microsoft C 4.0 is too old (1986) to understand ANSI-style function prototypes. Converting the function definitions to K&R style C was trivial, but the result was not working. That is to say, the program started, but didn’t draw properly. Luckily it didn’t take me too long to realize that passing a double to a function which takes an integer just doesn’t work—the caller will pass a double, but the called function will assume an integer. The compiler didn’t even peep about that, since it was a legal (if broken) programming construct. Again, I’d been spoiled by 20+ years of ANSI/ISO C. No matter, a few type casts took care of it.
The next hurdle was actually running Windows 1.04. It didn’t appear to like MS-DOS 4.01 very much. As it turns out, there are two options for running Windows 1.04 reliably. Either use DOS 3.x (which I wasn’t keen on, due to the 32MB partition size limitation), or DOS 5.0 or higher with SETVER.EXE loaded. The magic bit is SETVER reporting version 3.40 to WIN100.BIN—without that, Windows 1.x will either fail to start (after spewing a bunch of junk on the startup screen) or starts but doesn’t run properly.
Loath to create yet another VM, I simply used a PC DOS 2000 boot floppy with SETVER and ran Windows 1.04 from the DOS 4.01 virtual hard disk where it was already installed. That did the trick and I was able to run aclock on Windows 1.x.
The executable built with the Windows 1.04 SDK works fine even on newer versions of Windows, although there may be complaints about the application being incompatible:
The warning has to do with the fact that all Windows 1.x and 2.x applications were designed to run in real mode, as there was no other Windows mode back then (not even in Windows/386). However, aclock is simple enough that it works in protected mode as well.
It is interesting to note that the Windows clock applet, which was shipped as a sample with the old Windows SDKs, does not use floating-point math to avoid the issue mentioned above, and presumably also to improve speed. Rather than performing trigonometric calculations at run-time, it includes a pre-computed table of cosine values and uses strictly integer math. The result is of course more or less identical.
Note that the Windows 1.04 screenshots are slightly compressed in the vertical direction as the EGA resolution used (640×350) does not use square pixels.
Another interesting point is that the 16-bit Windows API does not provide any way to query the time and date. The information must be obtained through the DOS INT 21h interface; this is again evidenced by the Windows clock applet sample source code in the SDKs. A small point in favor of those who say that 16-bit Windows was not an OS.
The source code for the Win16/Win32 version may be found here.