At the end of December 2005, Microsoft released the new Windows Driver Foundation. This is a new framework for building Windows device drivers. It is a lot more high level than the Windows Driver Model (WDM), and as such it is easier to learn, and takes much less time to develop device drivers. This article will show you how to code, build, and deploy a skeleton WDF Kernel Mode Device Driver. This article does not explain all the low level concepts behind driver development. To learn about these basic concepts, check out the 'Related material' section in this article.
There is no demo application for the device driver explained in this article. This is because it doesn't really do anything yet. The only thing you need is the DebugView utility.
For those of you who don't know what WDF is: it is the best invention since sliced bread. Until recently, if you needed a device driver, you had to use the Windows Driver Model (WDM). WDM provides a low level framework for creating device drivers. Using this framework, your driver had to accept PNP, Power management, and IO requests, and figure out what to do with them, based on the state of your driver.
There are a couple of simple state diagrams describing the transitions between the different PNP states, and between the different power states, along with the events that cause the different state transitions. On the surface, this seems simple enough. There are only 6 PNP states, and 6 system power states, so it shouldn't be that hard, right?
As soon as you start writing your driver, you realize that the more you learn about WDM, the more confused you are. Implementing PNP is doable, with the understanding that you can use someone else's cancel-safe IRP queues. Otherwise, you have to write them yourself. The real horror sets in when you try to implement power management. Typically, your device driver has to manage both the system power state policy, and the device policy IRPs. This is quite complex, and according to the documentation, you must never perform any blocking activities while handling those IRPs. This means that you have to build a complex state machine that is hooked together with the completion routines.
There is no system level synchronization between the IO, PNP, and power IRPs, so it is possible for your driver to receive both a PNP and a power IRP while it is still doing some other IO activities. The complexity this introduces is so enormous that writing this code yourself is nearly impossible if you are not an experienced, professional driver writer.
To be honest, power management was where I bailed out. My OSR USB-FX2 learning kit disappeared in a drawer, and I couldn't spend months of my free time getting the driver to work properly.
Then, WDF was released. It looked very promising. Suddenly, the prospect to write a kernel mode device driver looked exciting again. When I wrote my first device driver, there were very few articles on KMDF. The only source of information I could find were two articles at OSR Online. That's when I thought it would be a good idea to write such an article for beginners in WDF driver programming like myself.
WDF is built on WDM. It is still extremely useful to know the concepts of WDM, and driver development, in general.
The best source of information on modern driver development is still Walter Oney's book 'Programming the Windows Driver Model, 2nd edition'. It explains all the concepts involved with WDM drivers, in specific, and Windows driver development, in general. It is a 'must read' if you are seriously interested in driver development.
Another good source of information is OSR Online. It features a number of driver development newsgroups, free tools, and articles, and even a printed magazine that you can subscribe for free. They also sell learning kits, with sample drivers to make it easy for you to learn driver development.
Last but not the least, there are a number of very good articles on driver development here at CodeProject.
KMDF stands for Kernel Mode Driver Framework. If you need your device driver to run in kernel mode, then KMDF supplies you with an elegant framework that makes kernel device driver development almost painless.
The idea behind KMDF is that your driver is a giant WDM state machine that receives all IO and system requests. This state machine will perform the correct synchronization actions, and execute your call-back functions at the right time. This also means that you don't have to break your head on having to synchronize power transitions with IO and other things. Your driver need not perform any other synchronization other than that needed to protect the internal data structures of your driver.
Another important feature of KMDF is that a lot of functions are optional. If you don't specify call-back functions for certain events, the WDM state machine executes the default functions to handle the request. That way, you don't have to care about things that are not important to our driver.
KMDF is object based. The functionality is contained in different types of objects. These objects can export methods, properties, and events, just like the classes in C++. This insures that the functionality is logically grouped per object, and objects can be placed in a hierarchy, just like the .NET classes or MFC classes, except that you are using the C syntax.
All objects have a reference count that gets set to 1 by the framework when it is created, and decremented when the object is finished with it. An object will never be deleted until the reference count is 0, so drivers can control the lifetime of an object beyond its natural lifetime, by explicitly referencing and dereferencing it. A driver can also supply a deletion call-back function for an object that will be executed when the object is deleted.
Furthermore, all objects can have a parent–child relationship. When the parent is about to be deleted, the framework attempts to delete the children before deleting the parent.
Finally, every object within the framework can have a context space assigned to it. This context space can be used for storing additional information about the object. A good example of this is the device context of a device object. The context space will exist for the entire lifetime of the object, and can be accessed through an accessor macro that was declared earlier.
Implementing the code
The following sections describe the different functions that are to be implemented to build a functional driver, and the things they need to do.
The driver entry point
The driver entry is the place where the driver starts its natural life. For standard drivers, this function is very simple. The only thing it needs to do is create the driver object. If there are any driver global variables, this is the place where they have to be allocated.
The driver object will be initialized with the function that will be called for each new device that is added:
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
KdPrint((__DRIVER_NAME "--> DriverEntry\n"));
status = WdfDriverCreate(
KdPrint((__DRIVER_NAME "WdfDriverCreate failed with status 0x%08x\n",
KdPrint((__DRIVER_NAME "<-- DriverEntry\n"));
Adding a new device
EvtDeviceAdd function will be called each time the system determines that a new device has been connected. This function will shoulder most of the work of our skeleton driver. It is the responsibility of this function to create and initialize the device object.
Before the device can be created, the driver needs to configure the device initialization. That way, the system will know what type of device this is, and how it will behave. As soon as that is done, the device object can be created, together with its device context.
Each device needs to have at least one IO request queue to be able to communicate with user mode applications. A device can also have one default IO queue. This is the IO queue that receives all the IO requests for which no specific queue was created.
A queue can also have request handlers assigned to it. A request handler is a function that will be executed for a specific type of request. It is the framework that will put the different types of requests in different queues, and then call the correct IO handler for those requests.
Since this is a very simple device driver, it only implements the
EvtIoDefault IO handler. This is the handler that will be used by the framework for dispatching all requests for which no specific handler was installed. It is the responsibility of this request handler to determine the type of request, and then do something sensible with it.
After the IO queues are created, it is time to create the device interface. The device now has a unique interface in the system to which the user mode applications can open a handle:
IN WDFDRIVER Driver,
IN PWDFDEVICE_INIT DeviceInit
PDEVICE_CONTEXT devCtx = NULL;
KdPrint((__DRIVER_NAME "--> EvtDeviceAdd\n"));
pnpPowerCallbacks.EvtDeviceD0Entry = EvtDeviceD0Entry;
pnpPowerCallbacks.EvtDeviceD0Exit = EvtDeviceD0Exit;
status = WdfDeviceCreate(&DeviceInit,
"WdfDeviceCreate failed with status 0x%08x\n", status));
devCtx = GetDeviceContext(device);
ioQConfig.EvtIoDefault = EvtDeviceIoDefault;
status = WdfIoQueueCreate(device, &ioQConfig,
"WdfIoQueueCreate failed with status 0x%08x\n", status));
status = WdfDeviceCreateDeviceInterface(device,
"WdfDeviceCreateDeviceInterface failed with status 0x%08x\n",
KdPrint((__DRIVER_NAME "<-- EvtDeviceAdd\n"));
If you have read the code, you may be wondering about the cleanup code, and the fact that there isn't any in this code. I did the same thing when developing my first WDF driver. This is where you first see the beauty of the parent–child object model in action.
Let's assume that for some reason, the device interface could not be registered. In that case, the
EvtDeviceAdd function would return with an error, and the framework will not build a device stack for the device. Still, an IO queue and a device object will be created.
The IO queue is the child of the device object. This means that it will be deleted automatically when the device object is deleted. The device object is a child of the driver object, and will automatically be deleted when the driver is unloaded. The driver will be unloaded as soon as the system detects that there are no more devices connected to the computer that are to be handled by that driver.
So you see: everything will be cleaned up properly without having to program a single line of cleanup code. Isn't that nice?
EvtDeviceAdd function has succeeded,
EvtDevicePrepareHardware is the next function that will be called by the framework. The task of the
EvtDevicePrepareHardware function is to make sure that the driver can access the hardware. This means that it has to map PCI memory ranges, open a USB interface, or do any other activity that is needed for the device to be able to access the device hardware.
Since the skeleton driver is a 'software only' driver, there is no need for it to register this function, but still it does this. That way, it can be modified later on when some real functionality is added.
This function is executed when the device is brought into an un-initialized D0 state, i.e., it is powered on, but the
EvtDeviceD0Entry function has not executed yet:
IN WDFDEVICE Device,
IN WDFCMRESLIST ResourceList,
IN WDFCMRESLIST ResourceListTranslated
NTSTATUS status = STATUS_SUCCESS;
KdPrint((__DRIVER_NAME "--> EvtDevicePrepareHardware\n"));
KdPrint((__DRIVER_NAME "<-- EvtDevicePrepareHardware\n"));
Entry of the device D0 state
EvtDeviceD0Entry function is responsible for starting the activities that the driver is supposed to perform. For example: if the device is a software only device that performs periodic actions, it has to start a timer. If the device is a USB device, it would have to start the low level USB IO target, and possibly load the device firmware.
Note that the actions that are to be performed may vary, depending on the previous power state. Things like loading firmware need not be done unless the device is connected for the first time, or recovered from a D3 power state. A real driver would have a
switch statement here to perform different actions for different types of D0 entry.
As with the
EvtDevicePrepareHardware function, this function does nothing for the moment:
IN WDFDEVICE Device,
IN WDF_POWER_DEVICE_STATE PreviousState
NTSTATUS status = STATUS_SUCCESS;
KdPrint((__DRIVER_NAME "--> EvtDeviceD0Entry\n"));
KdPrint((__DRIVER_NAME "<-- EvtDeviceD0Entry\n"));
Exit of the device D0 state
EvtDeviceD0Exit function performs actions that are the opposite of the
EvtDeviceD0Entry function. Its responsibility is to stop or pause the current IO operations, save the device state, and bring the device to a low power state.
The saved device state information can then be used to bring the device back to the state it had before it was brought to a low power state:
IN WDFDEVICE Device,
IN WDF_POWER_DEVICE_STATE TargetState
NTSTATUS status = STATUS_SUCCESS;
KdPrint((__DRIVER_NAME "--> EvtDeviceD0Exit\n"));
KdPrint((__DRIVER_NAME "<-- EvtDeviceD0Exit\n"));
Handling IO requests
When the default IO queue was created, only one IO handler was registered for it:
EvtDeviceIoDefault. With the implementation shown below, this function will accept any request, and will fail because no IO functionality has yet been implemented.
To actually handle the request, the handler would have to inspect the request object, and then perform some action based on the request type. In that case, it makes more sense to register handlers for the requests that are to be accepted by the driver. That way, the request would be automatically delivered to the correct handler:
IN WDFQUEUE Queue,
IN WDFREQUEST Request
KdPrint((__DRIVER_NAME "--> EvtDeviceIoDefault\n"));
KdPrint((__DRIVER_NAME "<-- EvtDeviceIoDefault\n"));
Interrupt request level and pageability
The interrupt request level (or IRQL) is the priority with which a kernel routine executes. For most of the code, the IRQL is
DISPATCH. If the code runs with an IRQL =
DISPATCH level, it is guaranteed a more timely execution, but this comes at a price.
The code that runs at
DISPATCH level should never block, and it must never, ever generate a page fault. The system cannot handle page faults if the code is executing at
DISPATCH, so a page fault will result in a bug check (Blue Screen Of Death).
To make sure that you don't violate this rule, you have to check the IRQL at which the WDF framework will execute your code. You then have to make sure that you follow the rules that apply for the specified IRQL.
I use pageability because it is possible to tell the compiler to put the code in paged or non paged sections of memory. Actually, the code is always placed in non-paged memory unless you tell the compiler to do otherwise. This can be done with
pragma statement tells the compiler to put the compiled code for a specified function into a specific memory section:
#pragma alloc_text(INIT, DriverEntry)
#pragma alloc_text(PAGE, EvtDeviceAdd)
In most cases, you'll only use two sections:
INIT section is paged in during the initialization phase. After that, it is discarded. This optimization is not so big, since the
DriverEntry function does not contain much code, but there is no need to keep it in memory once it has finished.
PAGE section is paged in and out, based on the system paging algorithms and the code usage patterns.
Putting the code in paged memory is an important optimization because it only has to be in physical memory when your driver is actually doing something with it. This makes the physical memory free to be used for other things if the code is not needed.
To optimize the use of system resources, you should place all the functions that do not explicitly need to be locked, into pageable memory. However, to prevent system crashes, check the IRQL at which your functions will be executed before you place them in pageable sections. The WDF documentation is pretty clear about it, except in the case of the IO request event handlers.
The documentation specifies that these are called at IRQL <=
DISPATCH level, unless the device object was created with the
ExecutionLevel attribute set to
WdfExecutionLevelPassive. Since we didn't do that, it would seem, at first glance, that we cannot allow the
EvtDeviceIoDefault function to be pageable. However, the samples in the WDF DDK seemed to do exactly that. After some searching, someone on the NTDEV list was kind enough to explain to me that it depends on the type of the driver and the way it is used.
Our device driver is a top level function driver that will be called directly from user mode programs. According to the DDK documentation, the dispatch routines of file system drivers and the highest level drivers will be called in a non-arbitrary thread context at IRQL =
PASSIVE. For our driver, this means that all functions can be pageable.
Creating the project files
Now that you have the source code for all the functions in the driver, it is time to prepare the driver project for building a device driver binary. Since you are using the standard DDK build tools, you need to provide a makefile and a sources file.
The makefile is only needed to redirect the make process to the general makefile that is distributed with the DDK. You can basically copy the makefile from a project in the DDK examples section.
The sources file is needed to tell the build process the files it has to compile, and how it has to do that. The sources file for the driver looks like this:
TARGETNAME=basic This will be the name of
the final driver
TARGETTYPE=DRIVER This will be the type of
binary that will be built
MSC_WARNING_LEVEL=/W4 most strict warning level.
INCLUDES = .\ Specify additional include
NO_BINPLACE=1 Specify that the BinPlace
utility will not be used
KMDF_VERSION=1 This driver uses the first
version of KMDF
SOURCES=Driver.c DeviceIO.c Power.c Device.c List of files that make
up the complete driver
!include $(WDF_ROOT)\project.mk Include the default
WDF project settings.
Most of these options speak for themselves. A complete list of all the macros and variables can be found in the DDK documentation set. One macro worth mentioning is the
MSC_WARNING_LEVEL macro. This macro defines the warning level that will be used for compiling the code. The default warning level is 3. This level is mostly used in production settings, and shows the most warnings.
Level 4 will show you every warning that can be considered as a deviation from the C standard. When first compiling with level 4, you are likely to see hundreds if not thousands of warnings flash by. These will be treated as errors because that is the DDK default. The bulk of these errors are caused by the DDK headers themselves. To get rid of these, you have to wrap them between
#pragma warning directives:
The errors that remain are your own errors, and you might have to use more
pragma directives to get a clean compile. The C language allows you to shoot yourself in the foot in subtle ways. Any problem that arises due to an error caused when using /W4 will be extremely difficult to find and debug, so it's better to prevent them from the beginning.
If you look at the above code closely, you'll notice that I not only disabled certain warnings, but also changed the warning level for the DDK headers to 3 instead of 4. This is because there is a bug in some versions of ntddk.h. The header disables some warnings itself, and then reverts them to the default state, instead of setting them to their previous state. Because of that, the DDK headers are compiled with the warning level 3, in which case there won't be a problem. I would like someone from Microsoft to acknowledge this, and let me know that this bug is solved in the new WDK (Windows Vista DDK).
One last thing to mention is the
UNREFERENCED_PARAMETER macro that you'll find in different places in the code. This macro wraps parameters that are not used by a function body. That way, they will not be unused, and will not result in a compiler warning.
Using the build environment
Device drivers are built using a build utility. The DDK can install a build utility for each of the current Windows platforms, from Windows 2000 to Windows 2003. For older versions, you have to install an older version of the DDK, but then you won't be able to build for the WDF framework.
The currently supported hardware architectures are i386, x64 (AMD 64 bit extensions to the i386 instruction set), and ia64 (Itanium).
When I started writing this article (using KMDF 1.0), Windows 2000 was not a supported platform for WDF, meaning you could make KMDF drivers only for Windows XP or higher. However, only a few days ago, Microsoft announced that they have reversed their decision to not support Windows 2000. Starting from the next release of KMDF (KMDF 1.1), you'll be able to build KMDF drivers for Windows 2000 as well.
To build the driver, you first have to choose the minimum platform that your driver supports. As with WDM, a driver built for a specific platform will be upwards compatible with newer platforms. Before the driver can be compiled, the correct environment variables have to be set. To do that, open the build utility, and 'cd' to the KMDF folder. If the DDK was installed with the default settings, that should be 'C:WindDDK\WDF\KMDF'. Then, you execute the command 'set_wdf_env.cmd'.
It is important that the path to your driver project, as well as the path to the DDK, do not contain any spaces. If they do, you'll get all sorts of weird messages that bear no resemblance whatsoever to a warning about spaces. Instead, you'll get an error about an unknown internal command 'JVC'.
Now that you have done all this, you're all set to build your driver. To do that, simply cd to the folder where your driver project is located and execute the 'build' command. That will start the compilation process, and result in either a clean build or one or more compilation errors. In the former case, you are ready to deploy and test your driver. In the latter case, you have to read the error list, correct the errors, and repeat the procedure.
Deploying the driver with an INF file
By now, you have built a WDF device driver. The only thing left to do is install it and test it. For installation, you need an INF file. The INF file contains information that the system needs to install and start your device driver.
There is a DDK tool called geninf.exe that can act as a wizard for creating an INF file for your driver. The problem with this tool is that it doesn't do everything for you. You have to manually edit some things yourself. Having to do that is a bit of a pain since the generated file looks kind of obfuscated.
The better solution is to manually create an INF file for this driver. It is a very simple driver, so the INF can be reasonably simple as well. That way, you also know the meaning of the different sections within the INF file, so that you can fix it yourself, should there be a problem or an addition.
The version section
The first block of text in the INF file is the
Version section. This section is the place where you put your company name and other stuff. The two most important keys in this section are the
Signature and the
Signature specifies the platforms for which this INF file may be used. You can basically choose between the NT family of platforms (NT4, W2K, XP, 2K3), the 9x family (95, 98, Me), or both. Since WDF only runs on the NT family, there is no need to provide information for the 9x platforms. The signature parameter is not actually used according to the DDK, but is an indication to the person reading the INF file.
Class defines the type of your device. This is where you tell the system that your device is a PCI bridge, USB host controller, or any type of predefined device. Our device is not a predefined type, so you have to specify a custom class and class GUID. You can generate the GUID using guidgen.exe.
CatalogFile variable contains the catalog of your driver if it should be WHQL certified. That file may be empty if the driver is not signed:
Signature = "$Windows NT$"
;copyright Bruno van Dooren
The standard sections
Version section, follows a number of sections that tell the system which devices will be supported by the device driver. It is possible for one driver to support multiple devices, but in this case, the list consists of only one device:
First, the INF file lists the different disks that make up the installation package. Unless you are planning to distribute your driver on floppy disks, there is no need to specify more than one disk:
Then, the INF file has to declare a list of files that are available in this package, the disk on which they can be found, and their relative path on the installation disks. In this list, the file is assumed to be in the root of the installation disk. If you want the driver to install without popping up a dialog asking for that file, you'll either have to put it there, or put it in a subfolder and declare the relative path in this list:
DestinationDirs section specifies the destination folder for the different
CopyFiles sections. All file actions that do not have a key in the
DestinationDirs section will be done in the DefaultDestDir folder. WDM and WDF drivers have to go into %windir%\system32\drivers. %windir% has the number 10. %windir%\system32 has the number 11. Since the
CopyFiles name for the
DriverInstall is not listed in this section, its files will be copied to the default destination:
CoInstaller_CopyFiles = 11
DeviceList section specifies the list of devices that are supported. The format of the device identification string depends on the device type. See the DDK documentation for exact details on the format for different device types.
If you don't have the USB-FX2 learning kit, you can specify 'Root\WdfBasic' as the device type (without the quotes of course). The system will then load your driver as a software only driver:
The class installation
The installation of the driver class is very simple, since it does not require any files to be installed. As a result, the installer section is very brief. It contains an installation of a registry key to specify the family name. The icon that is shown in the device manager is one of the default icons:
HKR,,,,"Sample device drivers"
The driver installation
The driver installation is started at the section that was specified in the device list. The platform identifier (nt, ntx86, ntia64, or ntamd64) specifies the platform for which the installer is intended. With the platform identifiers, it is easy to add installation sections for other platforms. This will soon become more important since amd64 will be a common platform of the future.
The main install section should list the driver version, and the name of the section that contains the list of files to install. The list of files has the files listed by their names. This is why the
SourceDisksFiles section was declared earlier, so that the installer service knows where it can find those files. A file flag with a value of 2 indicates that the file is critical for the installation:
Copying the files onto the system is not enough. The system should know which file contains the driver entry point, and how the driver has to be started. This is declared in the
DriverInstall.xxxx.Services section. This section is mandatory for PNP drivers:
ServiceType=1 ;;kernel mode driver
StartType=3 ;;start on demand
ErrorControl=1 ;;normal error handling.
hw section is optional, and can be used to configure driver specific registry keys. It is placed here to demonstrate the principle:
HKR,,SampleInfo,,"Basic registry key"
The CoInstaller installation
The driver that was created earlier is a WDF driver. Because of that, it needs the WDF library to be able to run. To make sure that the driver can be loaded in memory, the INF file needs a
CoInstaller section to install the WDF library together with the driver files. Specifying
CoInstaller is done with a '
CoInstallers' section. It specifies a registry section and a
CopyFiles section, just like the
DriverInstall section and the
AddReg section specifies a registry key of type
REG_MULTI_SZ. It specifies the file that contains the
CoInstaller entry point, and the name of the entry point:
Wdf section is a new addition to the INF format, and is used for telling the system which driver uses which version of the KMDF, and for specifying the WDF install section. It will be read by the
CoInstaller after the installation.
Specifying the WDF version is necessary because it is possible to install multiple drivers with one INF file, and each driver may be written against a different KMDF version.
The WDF install section contains (at least) the key '
KmdfLibraryVersion'. This is the KMDF version that the driver was built against:
KmdfService = basic, basic_wdfsect
KmdfLibraryVersion = 1.0
The strings section
Strings section is the last section in the INF file. It contains a list of variables that are used in the installation file:
INSTDISK=" Installation Disc"
DEV_DESCRIPTION="Basic WDF device"
INST_DISK_NAME="Basic WDF device driver installation disk"
Installing the driver
Installing the driver can be done in different ways. The simplest one can be used for installing PNP devices. Copy the installation package onto the hard disk (possibly via a setup program), and plug in the device. This might require a shutdown and restart, if the device is not hot-swappable. The other solution is to use the 'Add hardware' applet in the Control Panel. Either way, follow the installation wizard, browse to the location where the INF file is located, and continue installation. What you might want to do is have the debug monitor running during the device installation. As soon as the device is started, you can see the debug statements scrolling by in the output windows.
You now have a fully functioning device driver that can be built and installed. It doesn't actually do anything, but that was not the purpose of this article. This driver can be used as the base on which you can build a USB driver, for example. Now, you also know the different steps that are involved in writing, building, and installing a driver using the WDF Kernel Mode Device Foundation.
If you are seriously interested in driver development (be it WDF or WDM), try to read as much as you can. As I said in the beginning of this article, the other driver development articles on this website, and especially Oney’s book, are worth spending your time (and your money) on. Since WDF is built on WDM, it pays to know the concepts behind it.
It is also a good idea to buy one of the learning kits from OSR Online if you do not have a piece of hardware available with the register level documentation. I used the OSR USB-FX2 device, because that has the advantage of being usable on a laptop.
The following versions of this article have been released:
- 1.0: Initial version of this article.
- 1.1: Corrected the explanation of the WDF
Coinstaller section, as explained by Vishal Manan.