📟 Retro Mode

Programming for Windows 95 & 98: A C/C++ Win32 API Guide

In the late 1990s, the transition from the cooperative multitasking of 16-bit Windows 3.1 to the preemptive 32-bit architecture of Windows 95 was a revolution. Writing native applications for Windows 9x meant working intimately with the Win32 API—a massive collection of C functions that handle everything from drawing windows on the screen to managing memory and hardware.

If you are restoring vintage hardware and want to write authentic, era-accurate software that runs natively on a Pentium machine without modern overhead, this guide will show you exactly where to start.


1. The Toolkit: Exactly What is Needed

To write software for Windows 95 and 98, you need era-appropriate tools. Modern compilers (like Visual Studio 2022) no longer target the Windows 9x subsystem.

The Gold Standard: Microsoft Visual C++ 6.0 (1998)

VC++ 6.0 is the definitive IDE for Windows 98 development. It is fast, lightweight (by today’s standards), and compiles executables that run natively on Win9x, NT 4.0, and Windows 2000.

  • Where to find it: Physical CD-ROMs are common on eBay, and ISOs are widely archived on platforms like WinWorld.
  • Service Packs: You will want to install Service Pack 6 for VC++ 6.0 to fix known compiler bugs.

Alternative: Borland C++ 5.02 / C++ Builder

Borland was a major competitor in the 90s, beloved for its fast compilation times and the Visual Component Library (VCL). It is an excellent alternative if you prefer Rapid Application Development (RAD) over raw API coding.

The Holy Grail: MSDN Library (October 2001)

You cannot program the Win32 API without documentation. The October 2001 release of the MSDN Library (often distributed on a 3-CD set) is the last version that fully documents and supports Windows 95 and 98 before Microsoft shifted entirely to Windows XP and .NET.


2. Where to Start: The Architecture of a Win32 App

Modern console applications start at int main(). A Win32 graphical application starts at WinMain().

Windows programming is event-driven. Instead of your program dictating the flow of execution from top to bottom, Windows maintains a Message Queue. When a user clicks the mouse, presses a key, or resizes a window, Windows drops a “Message” into your application’s queue. Your program sits in a continuous loop (the Message Loop), pulling these messages out and reacting to them.

Example 1: The Absolute Minimum (Hello World)

If you want to display a simple GUI alert without drawing a full window, the MessageBox function is all you need.

#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    MessageBox(NULL, "Hello from Windows 98!", "W4DXR Retro Lab", MB_OK | MB_ICONINFORMATION);
    return 0;
}

Example 2: The Classic “Petzold” Window

To create an actual window, you must define a Window Class, register it with the OS, create the window, and enter the message loop. This is the foundational skeleton of almost every 90s Windows application (popularized by Charles Petzold’s legendary book, Programming Windows).

#include <windows.h>

// Forward declaration of the Window Procedure
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    static char szAppName[] = "RetroWinApp";
    HWND hwnd;
    MSG msg;
    WNDCLASS wndclass;

    // 1. Define the Window Class
    wndclass.style         = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc   = WndProc; // Pointer to our message handler
    wndclass.cbClsExtra    = 0;
    wndclass.cbWndExtra    = 0;
    wndclass.hInstance     = hInstance;
    wndclass.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndclass.lpszMenuName  = NULL;
    wndclass.lpszClassName = szAppName;

    // 2. Register the Class
    if (!RegisterClass(&wndclass)) {
        MessageBox(NULL, "This program requires Windows!", szAppName, MB_ICONERROR);
        return 0;
    }

    // 3. Create the Window
    hwnd = CreateWindow(szAppName,          // Window class name
                        "My Win9x Program", // Window caption
                        WS_OVERLAPPEDWINDOW,// Window style
                        CW_USEDEFAULT,      // Initial x position
                        CW_USEDEFAULT,      // Initial y position
                        400,                // Initial x size
                        300,                // Initial y size
                        NULL,               // Parent window handle
                        NULL,               // Window menu handle
                        hInstance,          // Program instance handle
                        NULL);              // Creation parameters

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    // 4. The Message Loop
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return msg.wParam;
}

// 5. The Window Procedure (Handles events)
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
    HDC hdc;
    PAINTSTRUCT ps;
    RECT rect;

    switch (message) {
        case WM_PAINT:
            hdc = BeginPaint(hwnd, &ps);
            GetClientRect(hwnd, &rect);
            DrawText(hdc, "Welcome to the 1990s!", -1, &rect, 
                     DT_SINGLELINE | DT_CENTER | DT_VCENTER);
            EndPaint(hwnd, &ps);
            return 0;

        case WM_DESTROY:
            PostQuitMessage(0); // Exits the message loop
            return 0;
    }
    
    // Default processing for messages we don't care about
    return DefWindowProc(hwnd, message, wParam, lParam);
}

3. Common Problems and Workarounds

Developing for a 30-year-old operating system comes with unique quirks. Here are the most common hurdles you will face.

Problem 1: The ANSI vs. Unicode Divide

Windows NT, 2000, and XP were built natively on Unicode (16-bit wide characters, WCHAR). However, Windows 95 and 98 were built on the older ANSI (8-bit characters, char) code pages for backward compatibility with DOS.

If you compile a modern C++ program with #define UNICODE, it will instantly crash or refuse to load on Windows 98 because functions like MessageBoxW (Wide) do not exist in the Win9x core; only MessageBoxA (ANSI) exists.

  • Workaround A: Ensure your VC++ 6.0 project settings do not have UNICODE or _UNICODE defined in the preprocessor. Use standard char strings.
  • Workaround B (Advanced): If you must use Unicode (e.g., porting a newer app backward), you must link against the Microsoft Layer for Unicode (MSLU), also known as Unicows.dll, which translates Unicode API calls into ANSI calls on the fly for Win9x systems.

Problem 2: Missing DLL Exports (The “Missing Function” Error)

You write a program, it compiles perfectly, but when you run it on Windows 95, you get an error: “The A device attached to the system is not functioning” or “Export not found in KERNEL32.DLL”.

This happens when you call a Win32 function that was introduced in Windows 2000 or XP, but doesn’t exist in the Win9x kernel.

  • Workaround: Never call newer API functions directly if you want strict 9x compatibility. Instead, load them dynamically at runtime using LoadLibrary() and GetProcAddress(). If the function pointer returns NULL, your program can gracefully fall back to an older method instead of crashing.

Problem 3: GDI Resource Leaks

Windows 9x is infamous for its fragile resource management. The OS only allocates a tiny 64KB heap for Graphical Device Interface (GDI) resources (like pens, brushes, and fonts). If your program creates a brush in WM_PAINT but forgets to delete it, Windows 98 will eventually run out of GDI memory and the entire operating system will crash (BSOD) or draw corrupted UI elements.

  • Workaround: Rule of thumb: If you Create it, you must Delete it. And you cannot delete an object while it is currently selected into a Device Context (DC).
// BAD: Leaks a brush every time the window is painted
HBRUSH hBrush = CreateSolidBrush(RGB(255, 0, 0));
SelectObject(hdc, hBrush);
// ... drawing ...
// Forgot to restore old brush and delete hBrush!

// GOOD: Safe GDI resource management
HBRUSH hBrush = CreateSolidBrush(RGB(255, 0, 0));
HBRUSH hOldBrush = (HBRUSH)SelectObject(hdc, hBrush); // Save the original
// ... drawing ...
SelectObject(hdc, hOldBrush); // Restore the original
DeleteObject(hBrush);         // Now it is safe to delete yours

Conclusion

Programming for Windows 95 and 98 strips away the massive abstraction layers of modern frameworks like Electron or .NET. It forces you to understand memory management, message queues, and hardware limits. By setting up a dedicated Visual C++ 6.0 environment and studying the classic Win32 API, you can write blazingly fast, authentic applications for your vintage workstations.