Chapter 5
Device Interfaces
This chapter explains how to use device interfaces to make a driver available to the kernel and Win32 user-mode applications. The device interface code in the Wdm1 driver is described. The example Wdm1Test user-mode program shows how to use the Win32 functions to find device interfaces. Wdm1Test then sends some basic I/O requests to the Wdm1 driver.
A driver implements one or more devices. Each device represents an instance of a piece of real or virtual hardware. User-mode programs and the kernel access a driver's device through a device object.
Once a WDM driver's DriverEntry routine has completed, the driver has usually not created any devices. Instead, it has provided the kernel with various callback routine pointers. The Plug and Play (PnP) Manager calls the driver AddDevice callback routine each time the driver should create a device. The PnP Manager makes further calls to configure the device using the PnP IRP, as described in Chapter 8.
This chapter considers only two Plug and Play messages: when a device is added using AddDevice, and when it is removed. The Remove Device message is a PnP IRP with a minor function code of IRP_MN_REMOVE_DEVICE.
Device Access User mode programs access drivers using the Win32 CreateFile function, passing a string in the lpFileName parameter. This routine is normally used to access files on disk. However, it can also be used to access a variety of other devices, such as pipes, mailslots, communication resources, or the console. At the simplest level, specifying C0M1 opens the first serial port.
To access other devices, use \. at the start of the lpFileName string. The following characters specify the device to open. For example, COM1 can also be opened as \.COM1. This form allows devices beyond COM9 to be opened. For example, opening C0M10 will not work but \.COM10 will work (assuming such a device exists).
To access a device created by a driver, you must know the symbolic link name of the device. A symbolic link name is simply the name of the device exposed by the driver. Creating these names is described later.
Suppose a driver called Dongle has created a device with a symbolic link of Dongle1. A Win32 program could open it using \.Dongle1 as the filename parameter passed to CreateFile.
There are two aspects to opening files that are important to understand. The first is that a device can be opened more than once, either in the same process or by different processes or threads. A driver can state that a Win32 thread gets exclusive access to the device until the handle is closed. Or the Win32 program can request exclusive access by setting the CreateFile dwShareMode parameter to zero. Alternative settings of dwShareMode permit other open requests if they specify read and/or write access. Even if a Win32 thread has exclusive access to a device, it can use overlapped I/O to issue more than one request to a driver at the same time.
Your device should cope with access through multiple handles, if it permits nonexclusive access. For example, if your device supports the notion of a file pointer, it has to survive 'simultaneous' read or write requests at the same file pointer location. The usual technique is to serialize all requests so that they are processed in full, one by one, as described in Chapter 16.
The second important aspect of opening a device is that Win32 programs can use file or directory names after the device name. A driver must decide whether it supports the concept of 'files' when a process opens a device handle. For example, \.Dongle1dir1file1 could be used in the call to CreateFile. It is up to you whether your driver interprets such information.
Figure 5.1 illustrates a possible device access situation. The Dongle driver has created two devices, called 'Dongle1' and 'Dongle2'. Process A has opened handles to each of the Dongle devices. Process B has opened a handle to 'file' on the 'Dongle2' device.
Figure 5.1 Device access
Subsequent I/O Once a user mode program has opened a handle to your device, it can use various Win32 routines to access it, such as ReadFile, WriteFile, DeviceIoControl, and CloseHandle. Each of these calls results in a request being passed to your driver in the form of an I/O Request Packet (IRP). If a Win32 program terminates abruptly and finishes without calling CloseHandle, Windows ensures that this call is made (after cleaning up any pending I/O requests).
The Wdm1 handlers for these IRPs are described later, along with an introduction to the AddDevice and IRP_MJ_PNP handlers.
Device Objects and Device Extensions
The kernel stores information about devices in device objects, DEVICE_OBJECT structures. The relevant DEVICE_OBJECT is passed to your driver callback routines for each interaction with a device.
For example, a Win32 call to ReadFile results in a Read IRP (i.e., with major function code IRP_MJ_READ) being sent to your device. The DriverEntry routine must set up a handler for the relevant callback. In the Wdm1 driver, this routine, Wdm1Read, is passed a pointer to the relevant device object and a pointer to the IRP.
The device object is 'owned' by the kernel, but has a few fields that should be used by a device driver, as described later. However, a driver can define a block of memory called a device extension, which it can use for whatever it wants. The device extension structure for each Wdm1 device is defined in Wdm1.h as shown in Listing 5.1. Your device extension structures will probably start with the same fields as Wdm1.
Listing 5.1 Wdm1 Device Extension definition
typedef struct _WDM1_DEVICE_EXTENSION {
PDEVICE_OBJECT fdo;
PDEVICE_OBJECT NextStackDevice;
UNICODE_STRING ifSymlinkName;
} WDM1_DEVICE_EXTENSION, *PWDM1_DEVICE_EXTENSION;
A device is created using the IoCreateDevice kernel call. The DeviceExtensionSize parameter gives the size of the device extension required. IoCreateDevice allocates the memory for you from the nonpaged pool. Access the device extension through the device object DeviceExtension field. For example, Wdm1 accesses the its device extension using the following code.
PWDM1_DEVICE_EXTENSION dx = (PWDM1_DEVICE_EXTENSION)fdo->DeviceExtension;
A device object is deleted using IoDeleteDevice. This routine deallocates the device extension memory. Make sure that you clean up all your device-related objects before calling IoDeleteDevice.
Creating and Deleting Device Objects
Wdm1 creates and deletes devices in the Pnp.cpp module as shown in Listing 5.2. The chapter on Plug and Play explains when the Wdm1AddDevice and Wdm1Pnp routines are called.
The NT_SUCCESS macro is used to test whether the IoCreateDevice kernel call has completed. Do not forget to check that all calls to the kernel succeed. The NT_ERROR macro is not equivalent to !NT_SUCCESS. It is better to use !NT_SUCCESS, as it catches warning status values as well as errors.
Listing 5.2 Wdm1 Pnp.cpp Create and Delete device code
NTSTATUS Wdm1AddDevice(IN PDRIVER_OBJECT DriverObject, IN PDEVICE_OBJECT pdo) {
…
NTSTATUS status;
PDEVICE_OBJECT fdo;
// Create our Functional Device Object in fdo
status = IoCreateDevice(DriverObject, sizeof(WDM1_DEVICE_EXTENSION),
NULL, // No Name
FILE_DEVICE_UNKNOWN, 0,
FALSE, // Not exclusive
&fdo);
if (!NT_SUCCESS(status)) return status;