PHD Computer Consultants Ltd
Using NT drivers from DOS and Win16 applications
Last modified: 8 February 1999.

PHD's Windows Device Driver resources

For the most part, DOS and Win16 programs run unchanged in Windows NT and Windows 2000 without any problems. However accessing hardware directly is strictly not possible in NT.

While a complete port to Win32 is the ideal solution, resource constraints may make this impossible in the short term. If hardware access is the main stumbling block then you can write a Win32 device driver which is accessible to the DOS or Win16 code, provided you have access to the source of the legacy code.

Note that the technique described is not to emulate the hardware.

It is best to isolate your hardware access code into a separate module, or DLL for Win16. Have a DOS or Win16 version which accesses the hardware directly for DOS, Windows 3.x and Windows 95/98. The NT/W2000 version of this module or DLL accesses the Win32 device driver.

The NT/W2000 installation routine must install the device driver. Depending on your detailed requirements, the device driver could start when NT/W2000 starts. A better approach might be to start the device driver just before the Win16 program starts and close it afterwards. A Win32 wrapper program is needed to do this job, using the Win32 Service Control Manager functions OpenSCManager, OpenService, ControlService, StartService and CloseServiceHandle.

How to do It

The trick is to set up a symbolic link from a standard DOS device name to the NT/W2000 driver. This symbolic link can be in addition to the normal Win32 symbolic links that a driver may set up.

However a link from LPT1 is output only. The most useful DOS device names are COM1 to COM9 which are bidirectional. Note that you should not use a colon at the end of the device name and the \\.\ should be omitted at the start of device name.

Now, NT/W2000ís own parallel and serial drivers will attempt to allocate DOS device names for each of the present ports. It is possible to override this allocation by ensuring that your device is loaded before the relevant system device (called "parallel"). The NT/W2000 parallel port arbitrator "parport" driver is in group "Parallel arbitrator". The parallel class driver "parallel" is one of several drivers in group "Extended Base". If you make your driver load after "parport" and "Parallel arbitrator" but before "Extended Base" then it can reserve the name LPT1 before "parallel" does. "parallel" will only moan minorly to the event log. Obviously this approach means that the driver will have to load at boot time.

However it is friendlier just to use a spare DOS device name, eg COM9. It does not matter that the driver talks to the parallel port eventually.

Note that DOS and Win16 do not have an equivalent of the DeviceIoControl function, so only reads and writes can be used. The Win16 functions OpenFile, _lopen, _lwrite, _lread and _lclose should be used, or - in DOS - the unbuffered standard _open, _write, _read and _close.

Storing the Device Name

The device driver and the legacy module need to know what device to open. For DOS, you will need to hardcode the device name or store it in a file somewhere.

However Win16 applications have basic access to the registry using the functions RegOpenKey, RegQueryValue and RegCloseKey. These functions just access the "(Default)" string value for a key, and cannot access String, Binary, DWORD, etc. The device driver can access the "(Default)" string value using the RtlQueryRegistryValues function, setting the RTL_QUERY_REGISTRY_TABLE Name field to a blank Unicode string (L""). The following registry key might be appropriate for an AbcDriver driver.

In addition the driver should read another DWORD parameter from this same key, eg called Win16Port. The driver would then create a Win32 symbolic link between the "(Default)" string name and the required NT/W2000 device name. For example if "(Default)" is "COM9" and Win16Port is 1, then a symbolic link is set up between COM9 and \Device\AbcDriver0, ie talking to COM9 communicates with the first parallel port.

Here is the required Win16 code to read the device name from registry. If successful, it returns a non-zero value and sets the device name in PortName.

Listing 1: Win16 code to read device name from registry

int NEAR GetPicLptDriverDeviceName(LPSTR PortName,int len)
    int rv = 0;
    HKEY HKEY_LOCAL_MACHINE = 0x80000002;
    HKEY hkParameters;
    PortName[0] = '\0';
    if( RegOpenKey(HKEY_LOCAL_MACHINE,
             (HKEY FAR*)&hkParameters) == ERROR_SUCCESS)
        LONG cb = len;
        if( RegQueryValue(hkParameters,NULL,PortName,&cb)
            == ERROR_SUCCESS)
            if( PortName[0]!='\0')
                rv = 1;

    return rv;
This is driver code to read the registry and set up the appropriate symbolic link, visible to DOS and Win16 applications.

Listing 2: Driver code to read registry and set up Win32 symbolic link

static NTSTATUS AbcCreateWin16Port( IN PDRIVER_OBJECT pDriverObject, IN ULONG NumParallelPorts)
    NTSTATUS status;

    UNICODE_STRING number, linkName, deviceName;
    WCHAR numberBuffer[10];
    WCHAR linkNameBuffer[ ABC_MAX_NAME_LENGTH ];
    WCHAR deviceNameBuffer[ ABC_MAX_NAME_LENGTH ];

    // Assume no Win 16 port given

    // Initialise unicode strings
    number.Buffer = numberBuffer;
    number.MaximumLength = 20;
    number.Length = 0;
    linkName.Buffer = linkNameBuffer;
    linkName.MaximumLength = ABC_MAX_NAME_LENGTH*2;
    linkName.Length = 0;
    deviceName.Buffer = deviceNameBuffer;
    deviceName.MaximumLength = ABC_MAX_NAME_LENGTH*2;
    deviceName.Length = 0;

    // Fabricate a Registry query

    RtlZeroMemory( QueryTable, sizeof(QueryTable));

    QueryTable[0].Name    = L"Win16Port";
    QueryTable[0].Flags    = RTL_QUERY_REGISTRY_DIRECT;
    QueryTable[0].EntryContext = &Win16LPTNo;
    QueryTable[1].Name    = L"";    // Default value
    QueryTable[1].Flags    = RTL_QUERY_REGISTRY_DIRECT;
    QueryTable[1].EntryContext = &Win16Device;

    // Look for the Win16Port and device name values in the Registry.
    if( !NT_SUCCESS( 
                    NULL, NULL)) ||
        (Win16LPTNo==ABC_WIN16_UNDEFINED) ||
        // If no Win16Port and device name values
        // then issue warning and just return
        // .. issue warning
        Win16LPTNo = ABC_WIN16_UNDEFINED;
        return STATUS_SUCCESS; 

    // Form Win16 symbolic link name
    RtlAppendUnicodeToString( &linkName, ABC_DOS_DEVICES);
    RtlAppendUnicodeStringToString( &linkName, &Win16Device);

    // Check port number is possible
    if( Win16LPTNo<1 || Win16LPTNo>NumParallelPorts)
        if( Win16LPTNo==0)
            Win16LPTNo = ABC_WIN16_UNDEFINED;
            return STATUS_SUCCESS;
        Win16LPTNo = ABC_WIN16_UNDEFINED;
        // .. issue warning

       // Form the base NT device name...
       RtlAppendUnicodeToString( &deviceName, ABC_NT_DEVICE_NAME);
    number.Length = 0;
    RtlIntegerToUnicodeString( Win16LPTNo-1, 10, &number); 
    RtlAppendUnicodeStringToString( &deviceName, &number);

    // Create a symbolic link so our device is visible to Win32...
    status = IoCreateSymbolicLink( &linkName, &deviceName);

    if( !NT_SUCCESS(status))
        // If already taken, report error
        // .. issue warning
        Win16LPTNo = ABC_WIN16_UNDEFINED;

    // Log a message saying that Win16 link has been created
    // .. log message
    return STATUS_SUCCESS; 

Dos Devices key

"A subkey of the CurrentControlSet\Control key, \Session Manager\DOS Devices, contains subkeys with value entries for the Win32 subsystem's logical device names. During system initialization, the Win32 Session Manager sets up symbolic link objects in the NT object namespace \?? (formerly \DosDevices), using the registry's \Session Manager\DOS Devices subkeys. Each symbolic link object creates an alias between a Win32-visible name and the corresponding NT device object name, so that applications and/or end users can use the Win32-visible name to send I/O requests to the underlying device."

Also look at the DefineDosDevice and QueryDosDevice functions.