NT device drivers respond to a simple set of file-oriented commands: open, read, write, and close. The NT device driver model supports another command, however: the IOCTL (I/O Control) command. A driver can make available most any custom functionality via an IOCTL command. Many standard Windows NT device drivers provide IOCTL (I/O Control Code) command functionality in addition to the basic device read/write support. These IOCTL commands can sometimes be very useful to applications as well as to other drivers. For example, the NT floppy drive device driver supports an IOCTL command that reports whether or not a floppy is currently inserted in the drive. For file-oriented commands (e.g., open, read, write, and close), an NT application can use familiar functions such as
WriteFile(). Sending an IOCTL command, though, requires calling the somewhat less familiar Win32 function
Device drivers can send IOCTL commands to other device drivers, just as applications do, though it is not as easy as calling
DeviceIoControl(). Some programmers actually create filter drivers to obtain information that they could more easily obtain via an IOCTL command. That's a risky practice, since a bug in a filter driver usually is capable of causing many more problems than a bug in code that just sends an IOCTL command.
This article demonstrates IOCTL commands from the perspective of both applications and drivers. I will demonstrate three aspects of IOCTL commands:
- How to support IOCTL commands in your own device driver.
- How to send an IOCTL command to another driver from your device driver.
- How to send an IOCTL command to a device driver from an application.
To demonstrate these concepts, I wrote a simple application and a simple Windows NT device driver that communicate via a custom-defined IOCTL. When the application sends an IOCTL command to the driver, the driver in turn sends a different IOCTL command to one of the standard built-in Windows NT drivers. This demonstrates how applications send IOCTL commands to drivers and how drivers send IOCTL commands to other drivers. It also provides a template for adding IOCTL commands to your own drivers. In practice, the application could just send an IOCTL directly to the second driver, but this example is contrived to demonstrate all three concepts. You will need the Windows NT DDK to build the examples in this article.
Defining the Custom IOCTL Value
When you decide that your device driver will support an operation via an IOCTL
command, you must define a command code that callers can use.
(Listing 1) contains the definition of my custom
IOCTL command code. Any other application or driver that wants to issue this
IOCTL to my driver will
#include this file.
The actual IOCTL command value is a bitmask that contains several pieces of information. I used the
CTL_CODE macro (defined in
winioctl.h for applications and
ntddk.h for drivers) to define the custom command
IOCTL_WDJ_REQUEST for my sample driver. The first argument to
CTL_CODE is a value that describes the device type. NT defines several standard device types (such as
FILE_DEVICE_DISK for persistent storage devices), but my sample driver doesn't really fit any of the predefined device categories, so I defined a new device type value of
FILE_DEVICE_WDJ. Note that since this is an "OEM" device (rather than a standard device type defined by the operating system), I must use a value in the range
The second argument of the
CTL_CODE macro identifies the specific IOCTL command. When a driver supports more than one custom IOCTL command, this value must be different for each command. Since
IOCTL_WDJ_REQUEST is a custom IOCTL command (rather than a standard IOCTL command, such as
IOCTL_FORMAT_DISK_TRACKS, that is supported by a whole category of device drivers), I'm required to use a value in the range
0xFFF to define my IOCTL command. The third argument describes the type of data transfer;
METHOD_BUFFERED is a typical choice for drivers that transfer a small amount of data. The final argument describes what kind of access the application must specify when opening a handle to this device. A value of
FILE_ANY_ACCESS means the caller can open a handle to this device with any access rights and still be able to send the
IOCTL_WDJ_REQUEST command via that file handle.
The Sample Driver
The sample driver code is in
wdj.c (Listing 2).
This driver has only two requirements: it must support the
command and it must demonstrate how to send an IOCTL command from one driver
to another driver. To demonstrate how one driver sends an IOCTL to another,
I needed to find a standard NT device driver that accepts some useful IOCTL
command. For my target driver I chose the standard Windows NT floppy driver,
because nearly all computers have at least one floppy device and the consequences
of accidently sending an errant command to the floppy driver are not usually
catastrophic. You should be able to experiment with this sample driver on
your own system.
The standard initialization entry point for all Windows NT device drivers (except certain layered drivers such as SCSI miniport drivers) is
DriverEntry(), a device driver typically creates one or more device objects to represent physical or logical devices. My sample driver doesn't actually talk to any real devices, so I create a single logical device object ("\\device\wdjdrv") by calling
IoCreateDevice(). Device objects typically have names of the form "\\device\Xxx". This doesn't really give my sample application something to talk to yet, though, because device object names are not directly accessible to user-mode applications. I need to create a symbolic link between my device object and a name that is visible to user-mode applications by calling
IoCreateSymbolicLink(). Symbolic link names have the form "\\DosDevices\Xxx". When an application passes the symbolic link name to
CreateFile() and then sends read, write, or IOCTL commands to that file handle, the requests are routed to my driver, because my driver created the target device object. Since a driver can create multiple device objects, drivers usually use the private device extension area of the device object to store any information that may need to be retrieved in order to carry out the read, write, or IOCTL command request.
My driver also needs to send IOCTL commands to a device driver (the floppy driver), but NT device drivers don't have access to the Win32 API, so my driver can't call
CreateFile() to obtain a handle. Moreover, the symbolic links that applications use to open devices represent a namespace that drivers don't have access to. Instead, my driver must use a DDK function to obtain a handle to one of the device objects that represent a physical floppy drive. The floppy driver creates device objects of the form "\\device\floppyX", where "X" is "0" for the first floppy device, etc. My driver retrieves a handle to the device object for the first floppy device by passing "\\device\floppy0" to
IoGetDeviceObjectPointer() returns a pointer to both a file object and a device object for "\\device\floppy0"; since I only need the device object, I save it in my device extension area and dereference the file object.
Finally, my driver fills out the dispatch routine table in the device object before exiting
DriverEntry(). The I/O system uses the dispatch routine table to route I/O requests targeted for my device object to the appropriate routine within my driver. When an application attempts to open, close, read, write, or send IOCTL commands to the symbolic link for my device object, the I/O system packages that request into an IRP (I/O Request Packet) and sends the IRP to the appropriate routine listed in the dispatch table. Since my sample driver doesn't really support anything other than IOCTL commands, I filled out only the
IRP_MJ_DEVICE_CONTROL entries. I also provided an "Unload" routine for my driver. This routine is not called in response to an I/O request, so it is not passed an IRP or a device object. Rather, it is called just before my driver is unloaded. In
WdjDrvUnload(), I simply delete the symbolic link and the device object that I created when the driver was initialized.
WdjDrvDispatch() handles more than one command (create, close, and IOCTL commands), so it examines the
MajorFunction field in the current IRP stack location to identify the command. Since I specified
METHOD_BUFFERED as the transfer type when I defined the IOCTL command, any data passed by the caller can be found in the
AssociatedIrp->SystemBuffer field, and any data I transfer back to the caller will also be copied back into this buffer. Status information is communicated back to the I/O system (and ultimately back to the caller) by filling out the
IoStatus.Information fields in the IRP structure. You set the
Status field to an
NTSTATUS value that indicates whether or not the call was successful (
STATUS_SUCCESS), and if not, what kind of error occured. In my case, the
Information field is filled out with the size of any data that I copied to the
SystemBuffer. I'm not actually doing anything special during the create and close commands, but I provided code stubs in case any readers wanted to add their own caller-specific initialization code.
In response to the
IOCTL_WDJ_REQUEST command, I need to send an IOCTL command to the floppy driver. Recall that I already have a handle to the target device object tucked away in my device extension. First, I build an IRP to represent the IOCTL request for this device object by calling
IoBuildDeviceIoControlRequest(). In this case, I'm calling the floppy driver's
IOCTL_DISK_CHECK_VERIFY command, which tells me whether or not a floppy is present in this floppy drive. After building the IRP, I call
IoCallDriver() to pass it to the target driver. Note that since some IOCTL commands are handled asynchronously, callers pass a kernel-mode event handle to
IoBuildDeviceIoControlRequest(); the event is signaled when the request completes. If the
Status field is set to
STATUS_NO_MEDIA_IN_DEVICE on return, then I know that the floppy drive is empty. I copy a Boolean value to the user-mode application's buffer to let them know whether or not a floppy is present in this floppy drive. Finally, to complete the original
IOCTL_WDJ_REQUEST command, I call
IoCompleteRequest() and then return from
You can programmatically install the sample driver by calling
CreateService(). For more information, see "Dynamically Loading Drivers in Windows NT" in the May 1995 issue of WDJ.
The Sample Application
Now that I've demonstrated how drivers send IOCTL commands to other drivers,
I'll show how applications send IOCTL commands to drivers. My sample application
app.c (Listing 3). The user-mode application
must first open a handle to the appropriate device object by passing the symbolic
link name to
CreateFile(). Recall that the symbolic link name created
in the sample driver is "\\DosDevices\wdjdrv". When passing this symbolic
link name to
CreateFile(), you should use the form "\\.\wdjdrv". It's
important to specify the
OPEN_EXISTING flag so that the
call will fail appropriately if the driver is not loaded.
Once you have a handle open to the appropriate device object, you can send IOCTL commands to it by calling
DeviceIoControl() is also used by applications running on Windows 95 to send commands to VxDs (note that the format for specifying the device name in the call to
CreateFile() is different for VxDs). In addition to the file handle and the IOCTL command value, callers can pass an input buffer and an output buffer to
DeviceIoControl(). For my custom
IOCTL_WDJ_REQUEST command, no input buffer is required, but I do need to specify an output buffer that is at least large enough to hold the
ULONG return value.
DeviceIoControl() supports both synchronous and asynchronous operation via the
lpOverlapped parameter. If you wish to call
DeviceIoControl() asynchronously, then the file handle must have been opened with the
As I mentioned before, this example is contrived. My sample application could have bypassed the sample
wdj.sys driver and called the floppy driver directly by opening a handle to "\\.\a:" and then passing the
IOCTL_DISK_CHECK_VERIFY command directly to that file handle. I introduced the complication of
wdj.sys just to demonstrate how drivers implement IOCTL commands, and how drivers can pass IOCTL commands to other drivers.
Drivers have access to a lot of useful information and can perform many useful tasks for applications. If a driver already supplies an IOCTL command that meets your needs, then it is a quite trivial matter for an application to call it. Likewise, drivers can sometimes avoid reinventing code by calling IOCTL commands in other drivers. If you need only to pass IOCTL commands to another driver, it is definitely overkill to layer yourself on top of that driver. Filter drivers are risky in that poorly written filter drivers can compromise the functionality of the driver on which they are layered. It is much simpler and safer in this case to get a pointer to the device object, build an IRP, and send it to the driver when necessary.
Paula Tomlinson has been developing DOS, Windows, and Windows-NT based applications and device drivers for nine years. The opinions expressed here are hers alone. She can be contacted via the internet at [email protected].