Introducing Windows Driver Framework
May 6, 20031
Walter Oney

Copyright © 2003 by Walter Oney. All rights reserved

Driver programming has gotten to be wicked hard, even for experts. I was recently talking to a group of engineers whose main job is to design laboratory-type instruments for their company. There are never more than a few dozen of each type of widget they design. In the good old days when dinosaurs roamed the Earth and MS-DOS ruled the PC platform, they could whip off a small console program that would provide all the control they needed for their device. They were able to keep the same basic pseudo-driver code working when they migrated their applications to Windows 3.x and 9x. Now their customers are demanding that the instruments work on modern laptops, which invariably come with Windows XP pre-installed. All of a sudden, their previous casual approach to hardware access won't work. They need to write kernel-mode drivers, or else redesign their hardware to work with some existing set of drivers. As one of these engineers put it:

I can think of many useful devices I'd like to be able to write a driver for that require only toggling a few bits [or] exchanging a little data. [B]ut if all th[e] complexity [of WDM] is required, I probably won't.

This particular engineer went on wish that he could use some standard, system-provided, drivers. Yet, as those of us who write drivers for a living well know, there aren't any "standard" drivers for non-standard devices. At best, there are toolkits that allow experienced programmers to write special-purpose drivers.

Suppose you wanted to take an instrument that formerly plugged into a serial port on a PC and rehost it on USB. Suppose for the sake of argument that you can't fit this device into the HID paradigm. Just imagine how many things you need to know at a pretty deep level to write a professional-quality custom driver for this new widget:

It's simply not possible for a beginner to grasp all these details. The current Driver Development Kit attempts to provide sample code to do many of the things I've listed, but only for a Windows 2000 or later target. Even if a beginner is able to figure out which sample to use as a source for cutting and pasting, he or she won't know which of the myriad details is relevant to his or her problem.

Because of the complexity of driver programming, we tend, as an industry, to end up with lots of poorly implemented drivers and with confused, disgruntled users. We also don't fulfill our potential for hardware innovation, because hardware manufacturers are easily stymied by the cost and delay associated with driver development.

The foregoing lament sets the stage for this introduction to the Windows Driver Framework. Microsoft gave WD-3 special dispensation to give you this early look at the next idiom for writing drivers as soon as it became public at WinHEC 2003.

Bear in mind that Microsoft is still designing the stuff I'm going to describe in this article, and it's not officially released for use in any production drivers. At this preliminary stage, Microsoft is looking for feedback so they can continue to improve the DDK.

The new Driver Framework:

At WinHEC 2003, Microsoft announced a new framework for driver programming. Windows Driver Framework, or WDF, attempts to simplify your job in three ways. First, WDF contains all of the boilerplate code that you used to have to cut and paste from DDK samples like TOASTER. When you're starting out, you need to re-use somebody else's code for Plug and Play and Power Management, and for much else besides. Cutting and pasting can be dangerous, though, unless you understand why the author of the sample put particular code into the sample. Worse yet, you won't have a clue about code left out of the sample because it didn't apply in the situation for which the sample was written.

The second way WDF simplifies your job is by providing sensible default behavior and simple interfaces for the most common cases. For example, I'd conservatively estimate that a simple WDM driver needs between one and two thousand lines of code to handle the IRP_MJ_PNP and IRP_MJ_POWER requests that float through the driver to start and stop the driver or to coordinate suspending and resuming the computer. You would need this code even for a legacy device that has no Plug and Play or power management features. I'll show you an example later in this article of a very simple WDF driver (STUPID.SYS) that has none of this complex code and yet installs and uninstalls normally and stays out of the way of power transitions.

Finally, WDF won't box you in. Previous attempts to "simplify" the programming of minidrivers for particular architectures have worked for a while, but have often been too inflexible to keep up with the pace of innovation. Think about the Network Device Interface Standard (NDIS) as it applies to network interface cards. Originally, NDIS allowed network vendors to write a single, not very hard, driver for all of the Windows platforms. NDIS facilitated platform independence by providing the complete DDI for a minidriver. But, forcing you to color within the lines established by NDIS makes it impossible to write miniport drivers for hardware that the designers of NDIS didn't anticipate. It's hard to manage a USB network card, for example, unless you're able to access the USBD library and significant portions of the kernel. Yet, coloring outside the NDIS lines would (at one point in the past) have caused your driver to flunk the Hardware Compatibility Tests and would, thereby, have disqualified your driver for a logo and a digital signature.

To avoid constraining your freedom to write the driver your hardware requires, WDF is extensible to the full WDM programming model. You can, for example, easily get access to the kernel objects that underlie WDF object handles. Let's say you're working with a DFWDEVICE object in a WDF-style driver and that you need to get a pointer to the underlying DEVICE_OBJECT for some reason. I'm hard pressed to think of a reason right now why you'd want this pointer, since WDF aims to anticipate common cases and to eliminate the need to do things like this very often. But imagine you needed it anyway. You'd do this:

PDEVICE_OBJECT fdo = DfwDeviceGetDeviceObject(hDevice);

where hDevice is of type DFWDEVICE and was probably an argument to the function in which you imbed this line of code. Imagine trying to do something similar in a SCSI miniport driver, or in an NDIS driver prior to the introduction of NdisMGetDeviceProperty. As far as I know, you couldn't do it. You were boxed in, and WDF tries not to do that to you.

Gotcha!

Driver Model WDM
Title Open Handles and Surprise Removal
Annoyance HIGH
Description

When a device is surprise-removed, the PnP manager will send a SURPRISE_REMOVAL IRP to the DevNode controlling the device.  When all open handles to the device have been closed (giving the drivers a chance to deallocate any resources or memory that may have been allocated when the handle was opened), then I/O manager will then send a REMOVE_DEVICE IRP telling all the drivers layered above any bus filters to delete their device objects from the DevNode.

The I/O manager tracks handle opens (CREATE IRPs) to devices that are opened either via CreateFile in user-mode or via ZwCreateFile in kernel-mode, and will not send the REMOVE_DEVICE IRP until the handle count goes to zero. However, the I/O manager does not track CREATE IRPs that are generated by a driver and sent to another driver directly via IoCallDriver. Therefore, the REMOVE_DEVICE IRP will be sent without a CLOSE IRP preceding it and the device object will be deleted, possibly causing a resource or memory leak.

Workaround First, never send CREATE IRPs to another driver directly; always use ZwCreateFile. To defend against this, keep track (in your device extension) of how many CREATE and CLOSE IRPs enter your driver through each device object, and perform whatever cleanup is necessary when you get the REMOVE_DEVICE IRP and the number of CREATE IRPs seen is greater than the number of CLOSE IRPs seen.
Versions Win2K, WinXP, WS2003

Read more Gotcha! notes

Framework Concepts

Object orientation is the cornerstone of Driver Frameworks for Windows. The NT kernel has always been "object oriented" in the sense that a centralized Object Manager has been responsible for managing the persistence of data structures like the DRIVER_OBJECT and DEVICE_OBJECT, and for invoking a limited set of method routines for common operations like creation and destruction. The DDK has nonetheless exposed the whole interior of objects that need to be completely or partly hidden from view. Moreover, some critical data objects -- most notably the I/O Request Packet (IRP) -- have never been part of the Object Manager's world. Most importantly, though, kernel objects have never before had any sort of inherited or common behaviors beyond simple reference counting that would provide a clear conceptual framework for programmers.

In contrast to previous driver models, WDF exposes a consistent and rigorous object-oriented view of the kernel. The WDF programmer creates and destroys objects of various kinds. Objects have properties that you can interrogate with function calls having obvious names. WDF method routines provide an interface for performing operations on objects. Objects are opaque unless you use an explicit escape valve (like the DfwDeviceGetDeviceObject function I alluded to earlier) to pierce the object-oriented veil to uncover the underlying kernel object. Finally, the framework associates events of various kinds with WDF objects and allows drivers to register handlers for those events.

Framework functions:

The first thing you need to know about WDF is that it's designed with the C programmer in mind. This fact means that Microsoft has not provided a C++ class library with an extensive hierarchy of base and derived classes. Instead, they've provided a library of functions that use object handles explicitly to identify the objects on which the functions operate. Microsoft didn't make the decision to expose a C, rather than a C++, interface casually. They consulted industry experts on the question of which language would be best, and they had long internal debates about the subject as well. In the end, the designers of WDF felt that a C-language interface would be most accessible to the largest number of programmers.

Functions in the framework follow a naming convention that emphasizes their object orientation. Yoda right at home with this will be. The name of a framework function begins with Dfw, which is followed by a noun indicating the type of object that the function is primarily concerned with. Then there is a verb that expresses some sort of processing activity that the function performs. For example, DfwDriverCreate creates a driver object, DfwCollectionCreate a generalized collection of other objects, DfwRequestCreate an I/O request, and so on. In many cases, a component name appears immediately after Dfw, as in DfwUsbDeviceCreate, a part of the USB support package.

Life cycle of an object:

To create a framework object, you call the associated Create method. To make things concrete, let's suppose there was such a thing as a "chair" object in the framework. There would then be a DfwChairCreate function that you could call roughly like this:

DFWCHAIR hChair;
DFWSTATUS status;
status = DfwChairCreate(<creation arguments>, &hChair);
if (!NT_SUCCESS(status))
  <handle error>;

In other words, DfwChairCreate attempts to create a chair object based on some set of creation arguments. If it succeeds, it sets your hChair argument to be a handle representing the object and returns a success code. If it fails, it returns an error code.

Status codes: Framework routines return a status code typedef'ed as a DFWSTATUS. For the time being, a DFWSTATUS is the same thing as an NTSTATUS except that it uses a unique facility code to differentiate between framework and other kinds of errors. Most framework functions, even ones that you wouldn't imagine might ever fail, can nominally return a status code, by the way. Your code is likely to have a lot more if statements than would drivers built using previous models!

Generally speaking, there might now be some sort of one-time initialization that you need to perform on the object:

status = DfwChairInitialize(hChair, <initialization arguments>);

Each time you want to use your chair object in a framework call, you specify the hChair handle as an argument:

status = DfwSitDown(hChair, . . .);

If you use a particular object several places in your driver, you can explicitly add and release references in order to pin the object in memory:

DfwObjectReference(hChair);
. . .
DfwObjectDereference(hChair);

With most types of object, you'll eventually delete the object:

DfwChairDelete(hChair);

Advanced object handling:

In the preceding section, I outlined the basic WDF functions for handling a hypothetical "chair" object in the automated living room of the future. The framework also provides two advanced features that will be useful to intermediately skilled driver developers.

The first advanced feature is the concept of a context structure that can be associated with a given instance of a framework object. You're probably already familiar with the DeviceExtension member of the standard DEVICE_OBJECT. The I/O Manager creates an extension structure of a size you specify in a call to IoCreateDevice, and it automatically deletes the structure along with the device object after you call IoDeleteDevice. During the lifetime of the device object, you can store "your stuff" in the extension area. Well, the framework's context concept is similar to the device extension concept, but more general.

The object context idea works this way: one of the arguments to the creation function for a given type of object is an optional pointer to a DFW_OBJECT_ATTRIBUTES structure. You can specify the size of your desired context area within that structure:

typedef struct _CHAIR_CONTEXT_STRUCTURE {
  ULONG NumberOfLegs;
  PWCHAR NameOfCurrentUser;
  } CHAIR_CONTEXT_STRUCTURE, *PCHAIR_CONTEXT_STRUCTURE;

DFW_OBJECT_ATTRIBUTES ChairAttributes;
DFW_OBJECT_ATTRIBUTES_INIT(&ChairAttributes,
  sizeof(CHAIR_CONTEXT_STRUCTURE),
  NULL, NULL);

DFWCHAIR hChair;
status = DfwChairCreate(<other arguments>, &ChairAttributes, &hChair);

After you create an instance of an object, you can retrieve a pointer to the associated context structure this way:

PCHAIR_CONTEXT_STRUCTURE ctx = (PCHAIR_CONTEXT_STRUCTURE) DfwObjectGetContext(hChair);

Note in particular that the framework allows you attach a context structure to any WDF object. You always use the standard function DfwObjectGetContext to retrieve the address of a particular context structure.

In addition to allowing you to keep your own context information with an object, the framework also allows you to specify a callback routine that the framework will call just before it finally destroys an object. There are two ways to specify the destruction callback. First, you can specify the routine's address and an arbitrary context pointer (this is a different context pointer than the one I was just talking about -- it goes with the destruction callback rather than with the object) in the DFW_OBJECT_ATTRIBUTES_STRUCTURE:

PSOMETYPE CallbackContext;  // an input to this code fragment
DFW_OBJECT_ATTRIBUTES ChairAttributes;
DFW_OBJECT_ATTRIBUTES_INIT(&ChairAttributes,
  sizeof(CHAIR_CONTEXT_STRUCTURE),
  (PFN_CONTEXT_DESTROY_CALLBACK) ChairDestroyCallback, CallbackContext);

Alternatively, you can call this framework function:

DfwObjectSetDestroyCallback(hChair, (PFN_CONTEXT_DESTROY_CALLBACK) ChairDestroyCallback, CallbackContext);

The destruction callback itself is a VOID function that receives pointers to the object context structure and the callback context, as follows:

VOID ChairDestroyCallback(PCHAIR_CONTEXT_STRUCTURE ctx, PSOMETYPE CallbackContext)
  {
  <cleanup ctx context structure>
  }

The purpose of a destroy callback is to let you cleanup the object context structure at the end of the object's life. You might wonder why you'd ever want to have such a callback. After all, if you believe what I told you in the previous section, you will be the actor who destroys the object. Since you're doing the destruction, why would you need to be notified of the fact? In other words, you might be wondering why you couldn't just do the following:

<cleanup hChair's CHAIR_CONTEXT_STRUCTURE> // <== don't do it this way
DfwChairDelete(hChair);

Well, for one thing, there are at least two WDF objects that the framework is responsible for deleting: the DFWDRIVER object and the DFWDEVICE object. Thus, there are no functions named DfwDriverDelete or DfwDeviceDelete. For another thing, the framework won't actually delete an object immediately just because you call the associated Delete function: the reference count has to be zero as well. You should not cleanup the contents of the associated context structure until all users of the object are done with it. Using a destroy callback will help you follow that rule.

Events, methods, and properties:

WDF objects support method routines and properties, and they generate events that result in callbacks to drivers. 

The formal distinction between a "method" and a function that gets or sets a "property" is probably not very important, and don't expect the framework documentation to rigorously follow whatever classification scheme you may have in mind.

Look! Up in the sky! It's a method! No -- it's a property! You can see how slippery the distinction between "method" and "property" might be by looking closely at the DfwRequest family of functions for managing "request" objects (a/k/a IRPs). The documentation classifies DfwRequestComplete, which encapsulates IoCompleteRequest, as a "method". It classifies DfwRequestSend, which encapsulates IoCallDriver, as a "property".

The concept of an "event callback" may be a bit new to experienced WDM driver programmers. Speaking for myself, at any rate, I'm used to thinking of AddDevice and DriverUnload as being just service routines that the operating system calls to do particular items of work related to managing my hardware. In WDF, however, both routines are callbacks that occur when particular events happen to a driver object. Thus, instead of setting function pointer members in my DRIVER_OBJECT, I'll register EvtDriverDeviceAdd and/or EvtDriverUnload callback routines when I first create a WDF driver object. When the event "new device detected" occurs, the framework calls my EvtDriverDeviceAdd function. Similarly, when the event "driver about to unload" occurs, the framework calls my EvtDriverUnload function, if I have one.

A major advantage to using an event orientation to describe when callbacks occur is this: handling some events is optional, and the framework will perform some sensible default action when I don't handle them. As presently constituted, WDF does require you to register callbacks for some events, even if you haven't quite reached the stage in developing your driver where there would actually be something useful to accomplish. Whether an event handler is required or not, therefore, is just another fact that you must learn about about each event.

Synchronization:

Beginning driver writers have difficulty understanding how to use all the synchronization primitives that the kernel provides. They also have difficulty understanding when it's necessary to guard some shared resource from destructive access in the multiprocessor, multitasking world of Windows.

As an example of the synchronization problem, consider a driver for a standard UART serial port. The port uses an interrupt to signal various events, and an interrupt service routine (ISR) reacts by reading and writing hardware registers. The upper edge of the driver must support about 40 control operations, many of which involve reading or writing the same registers as the ones used by the ISR. To further complicate matters, you can classify the upper edge functions into several categories based on whether they need to be serialized with reads or writes, with non-transfer control operations, or not at all, and based on whether they require the port to be powered. And the driver needs to be able to accept reads, writes, and most control operations at any IRQL less than or equal to DISPATCH_LEVEL to properly support drivers for devices that attach to the serial port.

How, then, should the driver synchronize access to all the queues it needs to maintain and to all the hardware registers it needs to access? An experienced programmer might immediately decide that all hardware access needs to be synchronized at DIRQL with the ISR, using either KeSynchronizeExecution or KeAcquireInterruptSpinLock, and that the IRP queues need to be synchronized using a spin lock at DISPATCH_LEVEL. These solutions would be much less obvious to a beginner, though. Among the problems a beginner faces are these:

WDF aims to simplify this kind of problem for all levels of driver programmer by providing scalable models for locking, threading, and interrupt synchronization. These models lead to drivers that behave very differently from the average WDM driver.

Let's consider the threading models first. In the DriverEntry routine, you declare (by means of a value in a parameter structure for DfwDriverCreate) whether your driver will use the synchronous or the asynchronous threading model. In the synchronous model, the framework calls most driver callbacks at PASSIVE_LEVEL in the context of a system thread that can be blocked. In the asynchronous model, however, most driver callbacks occur at DISPATCH_LEVEL. As you already know, thread blocking (except the sort of blocking that occurs while waiting for a spin lock) is not allowed at DISPATCH_LEVEL. Therefore, a driver that uses the asynchronous threading model does not block inside its event callbacks.

Locking is provided by the framework using four possible models: per-driver locking, per-device locking, per-queue locking, and no locking at all. The last of these four models corresponds to what happens today in a WDM driver. Namely, the driver is responsible for implementing whatever locks, using whatever primitives at whatever IRQLs, may be required for the job at hand. In fact, as something of a special case, if you specify "no locking" and the "asynchronous" threading model, your driver synchronizes just as a current WDM driver would, at whatever IRQL the system usually calls you at. 

In the other three models, the framework acquires a so-called "presentation lock" before calling an event callback routine in the driver. The presentation lock for an object might be a spin lock or a fast mutex, depending on the threading model the driver uses.

There are two non-hierarchical locking models, in which the framework doesn't automatically lock child objects when making parent-level callbacks:

Lastly, let's consider interrupt synchronization. As presently constituted, WDF doesn't provide for any interrupt-level synchronization at all. A driver, like the serial port example I summarized above, that needs to synchronize with an ISR has to use the raw WDM primitives for that purpose. This portion of the WDF specification is currently under review and, therefore, subject to change.

What I've just said is pretty confusing, I'll admit. I'll try in later articles to indicate situations in which one or the other combination of thread and locking models would be appropriate.

The "Hello, world!" of drivers...

In this last section of the article, I'll show you just how simple it can be to get started writing a driver using WDF. STUPID.SYS (get the complete project at http://www.wd-3.com/downloads/stupid.zip) is the simplest possible driver. You can install it as a legacy device, whereupon it will sit there and do -- nothing at all. Then you can uninstall it. Whoopie!

STUPID contains one source file (Driver.cpp) that contains three subroutines:

As a matter of fact, the EvDriverUnload function is superfluous -- I only included it so that I'd be able to see a debug message telling me that the driver was unloading.

DriverEntry:

Now I'll take you line-by-line through the DriverEntry function. First we have the function definition:

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
  {
  ...
  }

This is exactly the same as every DriverEntry function you've ever written, which emphasizes an important point: the kernel itself isn't changing with the introduction of WDF. Rather, Microsoft is adding new libraries to help you work within the existing kernel. Thus, the system loads and calls drivers exactly as it has done since Windows 2000. In particular, the DriverEntry function has the same role as in a standard WDM driver.

Now for the first line of code in the function:

DfwTraceDbgPrint(DRIVERNAME " - Version %d.%2.2d.%3.3d, %s %s\n", VERMAJOR, VERMINOR, BUILD, __DATE__, __TIME__);

VERMAJOR, VERMINOR, and BUILD are manifest constants in a "version.h" header file. My own "driver.h" defines DRIVERNAME as "STUPID".  The string constants __DATE__ and __TIME__ are compiler intrinsics that provide the compilation date and time, respectively. By coding the debug message using all these macro constants, I can move this line of code from one driver project to another without changing it.

DfwTraceDbgPrint will direct the message to both the kernel debugger and the tracing log. Best practice would be to use the full WPP tracing package, but that (frankly) requires a bit more work than I wanted to get into for so simple an example. Next best would be to use DfwTraceMessage (goes just to the trace log) and DfwTraceError (goes both to the debugger and the trace log).

The only substantive thing we need to do in the DriverEntry function is to call DfwDriverCreate, which requires a parameter structure. So our next step is to define and initialize the structure:

DFW_DRIVER_CONFIG config = {sizeof(DFW_DRIVER_CONFIG)};

This statement reserves storage for the configuration structure, initializes the first member (named Size), and initializes the remainder to zero.

config.DeviceExtensionSize = 0;

This statement specifies the size of the DEVICE_EXTENSION structure we want reserved in each DFWDEVICE object that the framework creates in the DfwDeviceInitialize function. This value corresponds to the second argument in a WDM driver's call to IoCreateDevice. If you were writing a more complex driver than usual, you would separately specify (in an argument to DfwDeviceCreateChild or DfwDeviceCreateControlObject) the size of the extension structure for other device objects. STUPID being less complex than usual, there is no device extension structure -- there's nothing to keep there -- and this value is zero.

config.RequestContextSize = 0;

This statement specifies the default size of the context structure we want reserved in each DFWREQUEST object that we create by calling DfwRequestCreate. You can override the default by specifying another value in the DFW_OBJECT_ATTRIBUTES argument to DfwRequestCreate. Since STUPID doesn't create any requests (in fact, it doesn't handle any IRPs at all), this value is zero.

config.Events.EvtDriverDeviceAdd = EvtDriverDeviceAdd;

This statement specifies an event callback function that the framework will call to initialize a new DFWDEVICE device object. It's a shame we need to write this routine, since STUPID doesn't do any nonstandard initialization at all. But, since you can't write any sort of real driver without doing stuff in this event callback routine, Microsoft requires it. I followed Microsoft's recommendation to use the same name for my function as the framework documentation uses. That makes it more obvious to someone reading my driver code what the function does.

config.Events.EvtDriverUnload = EvtDriverUnload;

This statement specifies an event callback function that the framework will call just before the driver is unloaded. The purpose of such a function is to do any required cleanup of global objects created by the DriverEntry function. Since STUPID doesn't have any such objects requiring cleanup, we don't really need to specify this callback. I'm always comforted while debugging a new driver to see a debug message when my driver unloads, however, so I specified this callback nonetheless.

config.DriverInitFlags = 0;

There may someday be some initialization flags for driver objects, and this statement would be the place to specify them. In the current incarnation of STUPID, this statement is redundant because the compiler will already have zero-filled the config structure.

config.LockingConfig = DfwLockingDevice;

Refer to the earlier discussion of locking models to understand the range of possibilities for the LockingConfig value. A plausible choice for a programmer just starting out with driver programming would be per-device locking. With this choice, the framework acquires the driver object's presentation lock and all device presentation locks when calling event callbacks that relate to the driver as a whole, and it acquires a device object's presentation lock when calling event callbacks related to a particular device object or one of the queues it contains.

config.ThreadingConfig = DfwThreadingAsynchronous;

Refer to the earlier discussion of threading models to understand the two possible choices for the ThreadingConfig value. The preferred model is asynchronous, which causes the framework to implement presentation locks using a standard kernel spin lock and to call most event callback routines in the driver at DISPATCH_LEVEL. This would be my choice for just about any driver I'd write. Mind you, since STUPID doesn't have any event callbacks besides EvtDriverDeviceAdd and EvtDriverUnload, it doesn't much matter what we say here.

config.SynchronizationConfig = DfwSynchronizationNone;

At the present time, there is only one interrupt synchronization choice for a WDF driver: none at all. This statement merely confirms that choice.

After this initialization, we are finally ready to make the following framework call:

DFWDRIVER Driver;
DFWSTATUS status = DfwDriverCreate(DriverObject, RegistryPath, NULL, &config, &Driver);

if (!NT_SUCCESS(status))
  DfwTraceError(DRIVERNAME " - DfwDriverCreate failed - %X\n", status);

return status;

EvtDriverDeviceAdd:

The framework calls our EvtDriverDeviceAdd function from the real AddDevice function. Broadly speaking, the purpose of this callback is to perform these tasks:

I struggled a bit to come up with the absolute minimum set of function calls for EvtDriverDeviceAdd to perform. This is how I always learn a new programming system, by the way: I look for the minimum, and that tells me what's optional embellishment. It also helps me get inside the mind of the designers to better understand how things work. Anyway, here's what I came up with in STUPID:

DFWSTATUS EvtDriverDeviceAdd(DFWDRIVER hDriver, DFWDEVICE hDevice)
  {
1 DfwTraceDbgPrint(DRIVERNAME " - EvtDriverDeviceAdd entered - IRQL is %d\n", KeGetCurrentIrql());
  DFWSTATUS status;

2 status = DfwDeviceInitialize(hDevice);
  if (!NT_SUCCESS(status))
    {
    DfwTraceError(DRIVERNAME " - DfwDeviceInitialize failed - %X\n", status);
    return status;
    }

3 DFW_FDO_EVENT_CALLBACKS callbacks;
  DFW_FDO_EVENT_CALLBACKS_INIT(&callbacks);

  status = DfwDeviceRegisterFdoCallbacks(hDevice, &callbacks);
  if (!NT_SUCCESS(status))
    {
    DfwTraceError(DRIVERNAME " - DfwDeviceRegisterFdoCallbacks failed - %X\n", status);
    return status;
    }

  return status;
  }

  1. I was curious how threading and locking models affected the IRQL at which functions would be called, so I decided to print the IRQL at entry to EvtDriverDeviceAdd. Surprise! In the WinHEC release of the DDK, with asynchronous threading and device locking, the call occurs at APC_LEVEL. I inferred that the framework was implementing the device presentation lock as a fast mutex that it acquires by calling ExAcquireFastMutex instead of ExAcquireFastMutexUnsafe. I would care about this if I wanted to send a synchronous IRP, say an IRP_MN_QUERY_INTERFACE, from this subroutine. I'm told that the developers consider this behavior incorrect and intend to fix it so that EvtDriverDeviceAdd is always called at PASSIVE_LEVEL.
  2. As you know, a WDM AddDevice function creates a function or filter device object by calling IoCreateDevice. This call to DfwDeviceInitialize is where the framework actually does this.
  3. In the WinHEC DDK release, a call to DfwDeviceRegisterFdoCallbacks is required even if you don't actually register any callbacks. This function creates a collection object as a side effect, and the PnP remove code path crashes later on if the collection doesn't exist. Mind you, you can't write a very useful function driver without having EvtDeviceStart and EvtDeviceStop callbacks, so this is one of those points that purists like to argue over despite the lack of practical importance.

Conclusion

I attempted in this article to provide a detailed introduction to the Windows Driver Framework. WDF exports an object-centric interface to the underlying WDM architecture of Windows. Unlike previous schemes to simply driver programming for particular types of device, WDF offers extensible simplification by supplying reasonable default actions that a programmer can override with configuration options and event callback routines.

Stay tuned to this site for future articles on WDF. Don't forget that you can "subscribe" for e-mail notifications whenever a new issue of WD-3 comes out by clicking here and filling out the simple form.

Got a comment? If you have comments about the new Windows Driver Framework, relay them to us at letters@wd-3.com. We'll make sure they get passed on the folks at Microsoft who are designing this wonderful stuff.

About the author:

Walter Oney is a freelance driver programmer, seminar leader, and author based in Boston, Massachusetts. You can reach him by e-mail at waltoney@oneysoft.com. Information about the Walter Oney Software seminar series, and other services, is available online at http://www.oneysoft.com

 


1 -- Revised May 9, 2003 to change abbreviation from DFW (Designed for Windows? The Dallas-Fort Worth airport?) to WDF, even though every DDI begins with the three letters Dfw. Go figure...