When people think about running old Windows software on Linux, Wine is usually the first and last answer. Wine has been around since 1993, has decades of compatibility fixes, and handles a genuinely wide range of NT-era Windows applications. What it has never handled particularly well is Windows 9x, and the reason for that is architectural rather than a matter of missing API implementations.
Hailey’s Windows 9x Subsystem for Linux takes the name of Microsoft’s own compatibility layer and inverts it. Instead of Linux binaries running on Windows, this is a subsystem for running Windows 9x software on Linux. The project surfaced on Lobsters with substantial interest, and the reason is that it fills a gap in the compatibility landscape that has existed for a long time.
The Emulation Gap Between DOS and NT
The compatibility landscape for old software has two reasonably mature options. DOSBox handles the DOS era, emulating x86 hardware, the DOS interrupt interface, and enough of the PC hardware environment to run games from the 1980s and early 1990s. Wine handles the Windows NT lineage, from NT 3.1 through modern Windows 11. Between them sits a dead zone: Windows 95, 98, and Millennium Edition.
Win9x is not just an older version of Wine’s target. It is a fundamentally different operating system design. It runs on top of DOS. The kernel, VMM32.VXD, is a protected-mode virtual machine manager that boots from a DOS environment and transitions the processor into 32-bit protected mode, but it retains the DOS layer underneath and exposes it to applications. The Win32 API implementation is not a clean 32-bit subsystem in the NT sense; it is a hybrid stack where 32-bit code in KERNEL32.DLL regularly thunks down into 16-bit code in KRNL386.EXE to do actual work.
Thunking: The Mechanism That Makes Win9x Different
Thunks are small code stubs that translate between 16-bit and 32-bit calling conventions. On Windows NT, 32-bit code lives in a clean 32-bit environment and never needs to call 16-bit code at all; the 16-bit WOW (Windows on Windows) subsystem runs 16-bit applications in their own virtual machines but the core NT system is purely 32-bit.
On Win9x, the thunking goes the other direction. When a 32-bit application calls certain KERNEL32.DLL functions, the implementation transitions back to 16-bit mode to use the legacy code that actually does the work. Microsoft documented three thunk types for Win9x: Universal Thunks, which let 32-bit code call 16-bit DLLs; Flat Thunks, which let 16-bit code call 32-bit code with shared selectors; and Generic Thunks, which allow 16-bit code to call 32-bit DLLs with full parameter marshaling.
For a compatibility layer on Linux, this creates an immediate challenge. Any implementation needs to handle 16-bit x86 code execution, not just as a legacy curiosity but as an integral part of the normal execution path for 32-bit applications. The x86 16-bit real-mode and protected-mode segmented execution model is completely different from the flat 32-bit model that Linux runs in, and getting the segment register semantics right is non-trivial.
VxD Drivers and Ring 0 Access
Win9x’s driver model is another divergence from NT that compatibility layers have historically struggled with. Virtual Device Drivers, VxDs, run at Ring 0 in Win9x. They are kernel-mode code that applications and system components can call directly. The interface to VxD drivers goes through software interrupts and a specific calling convention, and a significant portion of the Win9x ecosystem relies on VxD access.
The most visible example is DirectX and games. Games from the Win9x era frequently used VXDCALL to communicate with DirectX components or hardware abstraction layers that ran as VxDs. VWIN32.VXD provided access to Win32 services from VxD context. IFSMGR.VXD handled the installable file system manager. Applications that went directly to these drivers rather than through documented APIs are effectively doing inline kernel calls.
Wine’s architecture does not attempt to model this layer. It intercepts Win32 API calls at the DLL boundary and translates them to POSIX calls; it does not implement anything that looks like a Ring 0 environment. For NT-era software this is fine because NT’s well-defined system call interface keeps applications at arm’s length from kernel internals. For Win9x software that relied on VxD access, Wine’s approach leaves a structural gap.
The Shared Address Space Problem
Win9x also has a memory model that has no direct analogue in either modern Windows or Linux. The linear address space in Win9x is divided into regions that are visible to all processes simultaneously. From 4MB to 2GB is the “system arena,” shared across all running applications. From 2GB to 3GB is the “shared arena” for DLLs and shared memory. Only the region from 3GB to 4GB is per-process private memory.
The practical implication is that in Win9x, one process can write into another process’s memory if it has the address. This was not a design goal; it was a compromise made to support legacy 16-bit applications and shared DLLs without the overhead of full address space isolation. Win9x applications that relied on this behavior, intentionally or not, cannot run correctly in an environment that enforces per-process isolation without special handling.
For a Linux subsystem this creates a real mapping challenge. Linux processes have fully isolated address spaces by default. Emulating Win9x’s shared arena requires either establishing actual shared memory regions mapped at the same addresses in all processes, or single-stepping through memory accesses and handling cross-process references explicitly. Neither is cheap.
Where This Fits in the Preservation Problem
There is a practical preservation argument for getting Win9x compatibility right. A substantial body of software exists that ran only on Win9x: not just games, though those are numerous, but business software from the late 1990s, specialized applications, early multimedia tools, and a long tail of utilities that were never ported to NT or XP. Some of this software is culturally significant; some of it contains data formats that are otherwise inaccessible.
DOSBox-X has attempted to extend its coverage upward into the Win9x era by implementing enough of the Win9x boot process to run the operating system inside emulation. This works but is slow and requires a complete Win9x installation. The DOSBox-X project notes Win9x support as experimental and the performance overhead of full hardware emulation means it is not suitable for interactive use with demanding applications.
ReactOS approaches the problem from the other direction: a clean-room implementation of the Windows NT architecture that can run NT binaries natively. ReactOS has made real progress over its two-decade development, but it targets NT semantics deliberately. Win9x’s shared address space and VxD architecture are not on its roadmap.
Wine’s documentation is explicit about its focus. The Wine FAQ describes the project as implementing the Windows API to allow Windows applications to run on Unix, where the Windows API in practice means the NT API surface. Win9x-specific APIs and behaviors have always been secondary.
The Implementation Approach
A subsystem-style implementation, as opposed to full hardware emulation, offers a performance advantage that is worth the additional implementation complexity. Rather than emulating x86 hardware and letting Win9x’s own kernel run, a subsystem intercepts Win9x API calls at the boundary and translates them directly to Linux equivalents. The binary runs natively on the host CPU; only the OS interface is translated.
This is the approach Wine takes, and it is why Wine applications run at near-native speed on modern hardware while DOSBox applications run slowly. The challenge is that the Win9x API surface is larger and stranger than Wine’s NT target. The 16-bit thunks need to be handled, the VxD interface needs a stub implementation, and the shared memory model needs enough emulation to prevent crashes in applications that accidentally rely on address space sharing.
A plausible architecture involves the loader mapping Win32 PE binaries into process memory, intercepting imports from KERNEL32.DLL, USER32.DLL, GDI32.DLL, and the other core Win9x system DLLs, and providing Linux-backed implementations of those imports. For 16-bit code sections, a small x86 interpreter or JIT handles the segmented execution. For VxD calls, either stubs that return plausible values or a more complete virtual driver implementation handles the Ring 0 interface.
The interesting engineering question is which applications the project targets. Win9x compatibility work has a natural hierarchy: pure Win32 applications that happen to be distributed on Win9x are the easiest case since their API usage overlaps heavily with NT-era software. Applications that use DirectX 1 through 7 are the next tier. Applications that directly invoke VxDs or rely on shared memory layout are the hardest cases and may require dedicated handling.
Why This Matters Now
The timing of this project is not accidental. Preservation has become a mainstream concern rather than a hobbyist niche. The Software Preservation Network and the Internet Archive have both invested significantly in making old software accessible. Platform emulation projects like the MAME project have demonstrated that complete and accurate emulation of complex systems is achievable at scale.
The Win9x gap is peculiar because the software from that era is neither old enough to be academic history nor new enough to run on current platforms. It sits in a window of roughly 1995 to 2002 where the software is inaccessible without specific compatibility infrastructure and that infrastructure has historically been inadequate.
A native-speed subsystem that can run Win9x applications on Linux without needing a full Win9x installation would be a genuine capability improvement for preservation work, for game historians, and for anyone who has old data in formats that only Win9x-era applications could process. Getting the 16-bit thunking right is unglamorous work with a steep learning curve; that someone has taken it on seriously enough to produce something worth posting about is the news.