Understanding the USB client driver code structure (KMDF)
In this topic, you'll learn about the source code for a KMDF-based USB client driver. The code examples are generated by the USB user mode driver template included with Microsoft Visual Studio 2019.
These sections provide information about the template code.
For instructions on generating the KMDF template code, see How to write your first USB client driver (KMDF).
Driver source code
The driver object represents the instance of the client driver after Windows loads the driver in memory. The complete source code for the driver object is in Driver.h and Driver.c.
Driver.h
Before discussing the details of the template code, let's look at some declarations in the header file (Driver.h) that are relevant to KMDF driver development.
Driver.h, contains these files, included in the Windows Driver Kit (WDK).
#include <ntddk.h>
#include <wdf.h>
#include <usb.h>
#include <usbdlib.h>
#include <wdfusb.h>
#include "device.h"
#include "queue.h"
#include "trace.h"
Ntddk.h and Wdf.h header files are always included for KMDF driver development. The header file includes various declarations and definitions of methods and structures that you need to compile a KMDF driver.
Usb.h and Usbdlib.h include declarations and definitions of structures and routines that are required by a client driver for a USB device.
Wdfusb.h includes declarations and definitions of structures and methods that are required to communicate with the USB I/O target objects provided by the framework.
Device.h, Queue.h, and Trace.h are not included in the WDK. Those header files are generated by the template and are discussed later in this topic.
The next block in Driver.h provides function role type declarations for the DriverEntry routine, and EvtDriverDeviceAdd and EvtCleanupCallback event callback routines. All of these routines are implemented by the driver. Role types help Static Driver Verifier (SDV) analyze a driver's source code. For more information about role types, see Declaring Functions by Using Function Role Types for KMDF Drivers.
DRIVER_INITIALIZE DriverEntry;
EVT_WDF_DRIVER_DEVICE_ADD MyUSBDriver_EvtDeviceAdd;
EVT_WDF_OBJECT_CONTEXT_CLEANUP MyUSBDriver_EvtDriverContextCleanup;
The implementation file, Driver.c, contains the following block of code that uses alloc_text
pragma to specify whether the DriverEntry function and event callback routines are in pageable memory.
#ifdef ALLOC_PRAGMA
#pragma alloc_text (INIT, DriverEntry)
#pragma alloc_text (PAGE, MyUSBDriver_EvtDeviceAdd)
#pragma alloc_text (PAGE, MyUSBDriver_EvtDriverContextCleanup)
#endif
Notice that DriverEntry is marked as INIT, whereas the event callback routines are marked as PAGE. The INIT section indicates that the executable code for DriverEntry is pageable and discarded as soon as the driver returns from its DriverEntry. The PAGE section indicates that the code does not have to remain in physical memory all the time; it can be written to the page file when it is not in use. For more information, see Locking Pageable Code or Data.
Shortly after your driver is loaded, Windows allocates a DRIVER_OBJECT structure that represents your driver. It then calls your driver's entry point routine, DriverEntry, and passes a pointer to the structure. Because Windows looks for the routine by name, every driver must implement a routine named DriverEntry. The routine performs the driver's initialization tasks and specifies the driver's event callback routines to the framework.
The following code example shows the DriverEntry routine generated by the template.
NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
WDF_DRIVER_CONFIG config;
NTSTATUS status;
WDF_OBJECT_ATTRIBUTES attributes;
//
// Initialize WPP Tracing
//
WPP_INIT_TRACING( DriverObject, RegistryPath );
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
//
// Register a cleanup callback so that we can call WPP_CLEANUP when
// the framework driver object is deleted during driver unload.
//
WDF_OBJECT_ATTRIBUTES_INIT(&attributes);
attributes.EvtCleanupCallback = MyUSBDriver_EvtDriverContextCleanup;
WDF_DRIVER_CONFIG_INIT(&config,
MyUSBDriver_EvtDeviceAdd
);
status = WdfDriverCreate(DriverObject,
RegistryPath,
&attributes,
&config,
WDF_NO_HANDLE
);
if (!NT_SUCCESS(status)) {
TraceEvents(TRACE_LEVEL_ERROR, TRACE_DRIVER, "WdfDriverCreate failed %!STATUS!", status);
WPP_CLEANUP(DriverObject);
return status;
}
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Exit");
return status;
}
The DriverEntry routine has two parameters: a pointer to the DRIVER_OBJECT structure that is allocated by Windows, and a registry path for the driver. The RegistryPath parameter represents the driver-specific path in the registry.
In the DriverEntry routine, the driver performs these tasks:
Allocates global resources that are required during the lifetime of the driver. For example, in the template code, the client driver allocates resources required for WPP software tracing by calling the WPP_INIT_TRACING macro.
Registers certain event callback routines with the framework.
To register the event callbacks, the client driver first specifies pointers to its implementations of the EvtDriverXxx routines in certain WDF structures. The driver then calls the WdfDriverCreate method and supplies those structures (discussed in the next step).
Calls the WdfDriverCreate method and retrieves a handle to the framework driver object.
After the client driver calls WdfDriverCreate, the framework creates a framework driver object to represent the client driver. When the call completes, the client driver receives a WDFDRIVER handle and can retrieve information about the driver, such as its registry path, version information, and so on (see WDF Driver Object Reference).
Note that the framework driver object is different from the Windows driver object described by DRIVER_OBJECT. At anytime, the client driver can get a pointer to the WindowsDRIVER_OBJECT structure by using the WDFDRIVER handle and calling the WdfGetDriver method.
After the WdfDriverCreate call, the framework partners with the client driver to communicate with Windows. The framework acts as a layer of abstraction between Windows and the driver, and handles most of the complicated driver tasks. The client driver registers with the framework for events that driver is interested in. When certain events occur, Windows notifies the framework. If the driver registered an event callback for a particular event, the framework notifies the driver by invoking the registered event callback. By doing so, the driver is given the opportunity to handle the event, if needed. If the driver did not register its event callback, the framework proceeds with its default handling of the event.
One of the event callbacks that the driver must register is EvtDriverDeviceAdd. The framework invokes the driver's EvtDriverDeviceAdd implementation when the framework is ready to create a device object. In Windows, a device object is a logical representation of the function of the physical device for which the client driver is loaded (discussed later in this topic).
Other event callbacks that the driver can register are EvtDriverUnload, EvtCleanupCallback, and EvtDestroyCallback.
In the template code, the client driver registers for two events: EvtDriverDeviceAdd and EvtCleanupCallback. The driver specifies a pointer to the its implementation of EvtDriverDeviceAdd in the WDF_DRIVER_CONFIG structure and the EvtCleanupCallback event callback in the WDF_OBJECT_ATTRIBUTES structure.
When Windows is ready to release the DRIVER_OBJECT structure and unload the driver, the framework reports that event to the client driver by invoking the driver's EvtCleanupCallback implementation. The framework invokes that callback just before it deletes the framework driver object. The client driver can free all global resources that it allocated in its DriverEntry. For example, in the template code, the client driver stops WPP tracing that was activated in DriverEntry.
The following code example shows the client driver's EvtCleanupCallback event callback implementation.
VOID MyUSBDriver_EvtDriverContextCleanup(
_In_ WDFDRIVER Driver
)
{
UNREFERENCED_PARAMETER(Driver);
PAGED_CODE ();
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
//
// Stop WPP Tracing
//
WPP_CLEANUP( WdfDriverWdmGetDriverObject(Driver) );
}
After the device is recognized by the USB driver stack, the bus driver creates a physical device object (PDO) for the device and associates the PDO with the device node. The device node is in a stack formation, where the PDO is at the bottom. Each stack must have one PDO and can have filter device objects (filter DOs) and a function device object (FDO) above it. For more information, see Device Nodes and Device Stacks.
This illustration shows the device stack for the template driver, MyUSBDriver_.sys.
Notice the device stack named "My USB Device". The USB driver stack creates the PDO for the device stack. In the example, the PDO is associated with Usbhub3.sys, which is one of the drivers included with the USB driver stack. As the function driver for the device, the client driver must first create the FDO for the device and then attach it to the top of the device stack.
For a KMDF-based client driver, the framework performs those tasks on behalf of the client driver. To represent the FDO for the device, the framework creates a framework device object. The client driver can, however, specify certain initialization parameters that the framework uses to configure the new object. That opportunity is given to the client driver when the framework invokes the driver's EvtDriverDeviceAdd implementation. After the object is created and the FDO is attached to the top of the device stack, the framework provides the client driver with a WDFDEVICE handle to the framework device object. By using this handle, the client driver can perform various device-related operations.
The following code example shows the client driver's EvtDriverDeviceAdd event callback implementation.
NTSTATUS
MyUSBDriver_EvtDeviceAdd(
_In_ WDFDRIVER Driver,
_Inout_ PWDFDEVICE_INIT DeviceInit
)
{
NTSTATUS status;
UNREFERENCED_PARAMETER(Driver);
PAGED_CODE();
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
status = MyUSBDriver_CreateDevice(DeviceInit);
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Exit");
return status;
}
During run time, the implementation of EvtDriverDeviceAdd uses the PAGED_CODE macro to check that the routine is being called in an appropriate environment for pageable code. Make sure you call the macro after declaring all of your variables; otherwise, compilation fails because the generated source files are .c files and not .cpp files.
The client driver's EvtDriverDeviceAdd implementation calls the MyUSBDriver_CreateDevice helper function to perform the required tasks.
The following code example shows the MyUSBDriver_CreateDevice helper function. MyUSBDriver_CreateDevice is defined in Device.c.
NTSTATUS
MyUSBDriver_CreateDevice(
_Inout_ PWDFDEVICE_INIT DeviceInit
)
{
WDF_PNPPOWER_EVENT_CALLBACKS pnpPowerCallbacks;
WDF_OBJECT_ATTRIBUTES deviceAttributes;
PDEVICE_CONTEXT deviceContext;
WDFDEVICE device;
NTSTATUS status;
PAGED_CODE();
WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks);
pnpPowerCallbacks.EvtDevicePrepareHardware = MyUSBDriver_EvtDevicePrepareHardware;
WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit, &pnpPowerCallbacks);
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttributes, DEVICE_CONTEXT);
status = WdfDeviceCreate(&DeviceInit, &deviceAttributes, &device);
if (NT_SUCCESS(status)) {
//
// Get the device context and initialize it. WdfObjectGet_DEVICE_CONTEXT is an
// inline function generated by WDF_DECLARE_CONTEXT_TYPE macro in the
// device.h header file. This function will do the type checking and return
// the device context. If you pass a wrong object handle
// it will return NULL and assert if run under framework verifier mode.
//
deviceContext = WdfObjectGet_DEVICE_CONTEXT(device);
deviceContext->PrivateDeviceData = 0;
//
// Create a device interface so that applications can find and talk
// to us.
//
status = WdfDeviceCreateDeviceInterface(
device,
&GUID_DEVINTERFACE_MyUSBDriver_,
NULL // ReferenceString
);
if (NT_SUCCESS(status)) {
//
// Initialize the I/O Package and any Queues
//
status = MyUSBDriver_QueueInitialize(device);
}
}
return status;
}
EvtDriverDeviceAdd has two parameters: a handle to the framework driver object created in the previous call to DriverEntry, and a pointer to a WDFDEVICE_INIT structure. The framework allocates the WDFDEVICE_INIT structure and passes it a pointer so that the client driver can populate the structure with initialization parameters for the framework device object to be created.
In the EvtDriverDeviceAdd implementation, the client driver must perform these tasks:
Call the WdfDeviceCreate method to retrieve a WDFDEVICE handle to the new device object.
The WdfDeviceCreate method causes the framework to create a framework device object for the FDO and attach it to the top of the device stack. In the WdfDeviceCreate call, the client driver must perform these tasks:
- Specify pointers to the client driver's Plug and play (PnP) power callback routines in the framework-specified WDFDEVICE_INIT structure. The routines are first set in the WDF_PNPPOWER_EVENT_CALLBACKS structure and then associated with WDFDEVICE_INIT by calling the WdfDeviceInitSetPnpPowerEventCallbacks method.
Windows components, PnP and power managers, send device-related requests to drivers in response to changes in PnP state (such as started, stopped, and removed) and power state (such as working or suspend). For KMDF-based drivers, the framework intercepts those requests. The client driver can get notified about the requests by registering callback routines called PnP power event callbacks with the framework, by using the WdfDeviceCreate call. When Windows components send requests, the framework handles them and calls the corresponding PnP power event callback, if the client driver has registered.
One of the PnP power event callback routines that the client driver must implement is EvtDevicePrepareHardware. That event callback is invoked when the PnP manager starts the device. The implementation for EvtDevicePrepareHardware is discussed in the following section.
- Specify a pointer to the driver's device context structure. The pointer must be set in the WDF_OBJECT_ATTRIBUTES structure that is initialized by calling the WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE macro.
A device context (sometimes called device extension) is a data structure (defined by the client driver) for storing information about a specific device object. The client driver passes a pointer to its device context to the framework. The framework allocates a block of memory based on the size of the structure, and stores a pointer to that memory location in the framework device object. The client driver can use the pointer to access and store information in members of the device context. For more information about device contexts, see Framework Object Context Space.
After the WdfDeviceCreate call completes, the client driver receives a handle to the new framework device object, which stores a pointer to the block of memory allocated by the framework for the device context. The client driver can now get a pointer to the device context by calling the WdfObjectGet_DEVICE_CONTEXT macro.
Register a device interface GUID for the client driver by calling the WdfDeviceCreateDeviceInterface method. Applications can communicate with the driver by using this GUID. The GUID constant is declared in the header, public.h.
Set up queues for I/O transfers to the device. The template code defines MyUSBDriver_QueueInitialize, a helper routine for setting up queues, which is discussed in the Queue source code section.
Device source code
The device object represents the instance of the device for which the client driver is loaded in memory. The complete source code for the device object is in Device.h and Device.c.
Device.h
The Device.h header file includes public.h, which contains common declarations used by all files in the project.
The next block in Device.h declares the device context for the client driver.
typedef struct _DEVICE_CONTEXT
{
WDFUSBDEVICE UsbDevice;
ULONG PrivateDeviceData; // just a placeholder
} DEVICE_CONTEXT, *PDEVICE_CONTEXT;
WDF_DECLARE_CONTEXT_TYPE(DEVICE_CONTEXT)
The DEVICE_CONTEXT structure is defined by the client driver and stores information about a framework device object. It is declared in Device.h and contains two members: a handle to a framework's USB target device object (discussed later) and a placeholder. This structure will be expanded in later exercises.
Device.h also includes the WDF_DECLARE_CONTEXT_TYPE macro, which generates an inline function, WdfObjectGet_DEVICE_CONTEXT. The client driver can call that function to retrieve a pointer to the block of memory from the framework device object.
The following line of code declares MyUSBDriver_CreateDevice, a helper function that retrieves a WDFUSBDEVICE handle to the USB target device object.
NTSTATUS
MyUSBDriver_CreateDevice(
_Inout_ PWDFDEVICE_INIT DeviceInit
);
USBCreate takes a pointer to a WDFDEVICE_INIT structure as its parameter. This is the same pointer that was passed by the framework when it invoked the client driver's EvtDriverDeviceAdd implementation. Basically, MyUSBDriver_CreateDevice performs the tasks of EvtDriverDeviceAdd. The source code for EvtDriverDeviceAdd implementation is discussed in the previous section.
The next line in Device.h declares a function role type declaration for the EvtDevicePrepareHardware event callback routine. The event callback is implemented by the client driver and performs tasks such as configuring the USB device.
EVT_WDF_DEVICE_PREPARE_HARDWARE MyUSBDriver_EvtDevicePrepareHardware;
Device.c
The Device.c implementation file contains the following block of code that uses alloc_text
pragma to specify that the driver's implementation of EvtDevicePrepareHardware is in pageable memory.
#ifdef ALLOC_PRAGMA
#pragma alloc_text (PAGE, MyUSBDriver_CreateDevice)
#pragma alloc_text (PAGE, MyUSBDriver_EvtDevicePrepareHardware)
#endif
In the implementation for EvtDevicePrepareHardware, the client driver performs the USB-specific initialization tasks. Those tasks include registering the client driver, initializing USB-specific I/O target objects, and selecting a USB configuration. The following table shows the specialized I/O target objects provided by the framework. For more information, see USB I/O Targets.
USB I/O target object (handle) | Get a handle by calling... | Description |
---|---|---|
USB target device object (WDFUSBDEVICE ) | WdfUsbTargetDeviceCreateWithParameters | Represents a USB device and provides methods for retrieving the device descriptor and sending control requests to the device. |
USB target interface object (WDFUSBINTERFACE ) | WdfUsbTargetDeviceGetInterface | Represents an individual interface and provides methods that a client driver can call to select an alternate setting and retrieve information about the setting. |
USB target pipe object (WDFUSBPIPE) | WdfUsbInterfaceGetConfiguredPipe | Represents an individual pipe for an endpoint that is configured in the current alternate setting for an interface. The USB driver stack selects each interface in the selected configuration and sets up a communication channel to each endpoint within the interface. In USB terminology, that communication channel is known as a pipe. |
This code example shows the implementation for EvtDevicePrepareHardware.
NTSTATUS
MyUSBDriver_EvtDevicePrepareHardware(
_In_ WDFDEVICE Device,
_In_ WDFCMRESLIST ResourceList,
_In_ WDFCMRESLIST ResourceListTranslated
)
{
NTSTATUS status;
PDEVICE_CONTEXT pDeviceContext;
WDF_USB_DEVICE_CREATE_CONFIG createParams;
WDF_USB_DEVICE_SELECT_CONFIG_PARAMS configParams;
UNREFERENCED_PARAMETER(ResourceList);
UNREFERENCED_PARAMETER(ResourceListTranslated);
PAGED_CODE();
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
status = STATUS_SUCCESS;
pDeviceContext = WdfObjectGet_DEVICE_CONTEXT(Device);
if (pDeviceContext->UsbDevice == NULL) {
//
// Specifying a client contract version of 602 enables us to query for
// and use the new capabilities of the USB driver stack for Windows 8.
// It also implies that we conform to rules mentioned in the documentation
// documentation for WdfUsbTargetDeviceCreateWithParameters.
//
WDF_USB_DEVICE_CREATE_CONFIG_INIT(&createParams,
USBD_CLIENT_CONTRACT_VERSION_602
);
status = WdfUsbTargetDeviceCreateWithParameters(Device,
&createParams,
WDF_NO_OBJECT_ATTRIBUTES,
&pDeviceContext->UsbDevice
);
if (!NT_SUCCESS(status)) {
TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE,
"WdfUsbTargetDeviceCreateWithParameters failed 0x%x", status);
return status;
}
//
// Select the first configuration of the device, using the first alternate
// setting of each interface
//
WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_MULTIPLE_INTERFACES(&configParams,
0,
NULL
);
status = WdfUsbTargetDeviceSelectConfig(pDeviceContext->UsbDevice,
WDF_NO_OBJECT_ATTRIBUTES,
&configParams
);
if (!NT_SUCCESS(status)) {
TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE,
"WdfUsbTargetDeviceSelectConfig failed 0x%x", status);
return status;
}
}
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Exit");
return status;
}
Here's a closer look at the client driver's tasks as implemented by the template code:
Specifies the client driver's contract version in preparation to register itself with the underlying USB driver stack, loaded by Windows.
Windows can load the USB 3.0 or USB 2.0 driver stack, depending on the host controller to which the USB device is attached. The USB 3.0 driver stack is new in Windows 8 and supports several new features defined by the USB 3.0 specification, such as the streams capability. The new driver stack also implements several improvements, such as better tracking and processing of USB Request Blocks (URBs), which are available through a new set of URB routines. A client driver that intends to use those features or call the new routines must specify the USBD_CLIENT_CONTRACT_VERSION_602 contract version. A USBD_CLIENT_CONTRACT_VERSION_602 client driver must adhere to a certain set of rules. For more information about those rules, see Best Practices: Using URBs.
To specify the contract version, the client driver must initialize a WDF_USB_DEVICE_CREATE_CONFIG structure with the contract version by calling the WDF_USB_DEVICE_CREATE_CONFIG_INIT macro.
Calls the WdfUsbTargetDeviceCreateWithParameters method. The method requires a handle to the framework device object that the client driver obtained previously by calling WdfDeviceCreate in the driver's implementation of EvtDriverDeviceAdd. The WdfUsbTargetDeviceCreateWithParameters method:
- Registers the client driver with the underlying USB driver stack.
- Retrieves a WDFUSBDEVICE handle to the USB target device object that is created by the framework. The template code stores the handle to the USB target device object in its device context. By using that handle, the client driver can obtain USB-specific information about the device.
You must call WdfUsbTargetDeviceCreate instead of WdfUsbTargetDeviceCreateWithParameters if:
Your client driver does not call the new set of URB routines available with theWindows 8 version of the WDK.
If your client driver calls WdfUsbTargetDeviceCreateWithParameters, the USB driver stack assumes that all URBs are allocated by calling WdfUsbTargetDeviceCreateUrb or WdfUsbTargetDeviceCreateIsochUrb. URBs that are allocated by those methods have opaque URB context blocks that are used by the USB driver stack for faster processing. If the client driver uses an URB that is not allocated by those methods, the USB driver generates a bugcheck.
For more information about URB allocations, see Allocating and Building URBs.
Your client driver does not intend to adhere to the set of rules described in Best Practices: Using URBs.
Such drivers are not required to specify a client contract version and therefore must skip Step 1.
Selects a USB configuration.
In the template code, the client driver selects the default configuration in the USB device. The default configuration includes Configuration 0 of the device and the Alternate Setting 0 of each interface within that configuration.
To select the default configuration, the client driver configures the WDF_USB_DEVICE_SELECT_CONFIG_PARAMS structure by calling the WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_MULTIPLE_INTERFACES function. The function initializes the Type member to WdfUsbTargetDeviceSelectConfigTypeMultiInterface to indicate that if multiple interfaces are available, then an alternate setting in each those interfaces must be selected. Because the call must select the default configuration, the client driver specifies NULL in the SettingPairs parameter and 0 in the NumberInterfaces parameter. Upon completion, the MultiInterface.NumberOfConfiguredInterfaces member of WDF_USB_DEVICE_SELECT_CONFIG_PARAMS indicates the number of interfaces for which Alternate Setting 0 was selected. Other members are not modified.
Note If the client driver wants to select alternate settings other than the default setting, the driver must create an array of WDF_USB_INTERFACE_SETTING_PAIR structures. Each element in the array specifies the device-defined interface number and the index of the alternate setting to select. That information is stored in the device's configuration and interface descriptors that can be obtained by calling the WdfUsbTargetDeviceRetrieveConfigDescriptor method. The client driver must then call WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_MULTIPLE_INTERFACES and pass the WDF_USB_INTERFACE_SETTING_PAIR array to the framework.
Queue source code
The framework queue object represents the I/O queue for a specific framework device object. The complete source code for the queue object is in Queue.h and Queue.c.
Queue.h
Declares an event callback routine for the event raised by the framework's queue object.
The first block in Queue.h declares a queue context.
typedef struct _QUEUE_CONTEXT {
ULONG PrivateDeviceData; // just a placeholder
} QUEUE_CONTEXT, *PQUEUE_CONTEXT;
WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(QUEUE_CONTEXT, QueueGetContext)
Similar to a device context, a queue context is a data structure defined by the client to store information about a particular queue.
The next line of code declares MyUSBDriver_QueueInitialize function, the helper function that creates and initializes the framework queue object.
NTSTATUS
MyUSBDriver_QueueInitialize(
_In_ WDFDEVICE Device
);
The next code example declares a function role type declaration for the EvtIoDeviceControl event callback routine. The event callback is implemented by the client driver and is invoked when the framework processes a device I/O control request.
EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL MyUSBDriver_EvtIoDeviceControl;
Queue.c
The implementation file, Queue.c, contains the following block of code that uses alloc_text
pragma to specify that the driver's implementation of MyUSBDriver_QueueInitialize is in pageable memory.
#ifdef ALLOC_PRAGMA
#pragma alloc_text (PAGE, MyUSBDriver_QueueInitialize)
#endif
WDF provides the framework queue object to handle the request flow to the client driver. The framework creates a framework queue object when the client driver calls the WdfIoQueueCreate method. In that call, the client driver can specify certain configuration options before the framework creates queues. Those options include whether the queue is power-managed, allows zero-length requests, or is the default queue for the driver. A single framework queue object can handle several types of requests, such as read, write, and device I/O control. The client driver can specify event callbacks for each of those requests.
The client driver must also specify the dispatch type. A queue object's dispatch type determines how the framework delivers requests to the client driver. The delivery mechanism can be sequential, in parallel, or by a custom mechanism defined by the client driver. For a sequential queue, a request is not delivered until the client driver completes the previous request. In parallel dispatch mode, the framework forwards the requests as soon as they arrive from I/O manager. This means the client driver can receive one request while processing another. In the custom mechanism, the client manually pulls the next request out of the framework queue object when the driver is ready to process it.
Typically, the client driver must set up queues in the driver's EvtDriverDeviceAdd event callback. The template code provides the helper routine, MyUSBDriver_QueueInitialize, that initializes the framework queue object.
NTSTATUS
MyUSBDriver_QueueInitialize(
_In_ WDFDEVICE Device
)
{
WDFQUEUE queue;
NTSTATUS status;
WDF_IO_QUEUE_CONFIG queueConfig;
PAGED_CODE();
//
// Configure a default queue so that requests that are not
// configure-fowarded using WdfDeviceConfigureRequestDispatching to goto
// other queues get dispatched here.
//
WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(
&queueConfig,
WdfIoQueueDispatchParallel
);
queueConfig.EvtIoDeviceControl = MyUSBDriver_EvtIoDeviceControl;
status = WdfIoQueueCreate(
Device,
&queueConfig,
WDF_NO_OBJECT_ATTRIBUTES,
&queue
);
if( !NT_SUCCESS(status) ) {
TraceEvents(TRACE_LEVEL_ERROR, TRACE_QUEUE, "WdfIoQueueCreate failed %!STATUS!", status);
return status;
}
return status;
}
To set up queues, the client driver performs these tasks:
- Specifies the queue's configuration options in a WDF_IO_QUEUE_CONFIG structure. The template code uses the WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE function to initialize the structure. The function specifies the queue object as the default queue object, is power-managed, and receives requests in parallel.
- Adds the client driver's event callbacks for I/O requests for the queue. In the template, the client driver specifies a pointer to its event callback for a device I/O control request.
- Calls WdfIoQueueCreate to retrieve a WDFQUEUE handle to the framework queue object that is created by the framework.
Here's how the queue mechanism works. To communicate with the USB device, an application first opens a handle to the device by calling the SetDixxx routines and CreateHandle. By using this handle, the application calls the DeviceIoControl function with a specific control code. Depending on the type of control code, the application can specify input and output buffers in that call. The call is eventually received by I/O Manager, which then creates a request (IRP) and forwards it to the client driver. The framework intercepts the request, creates a framework request object, and adds it to the framework queue object. In this case, because the client driver registered its event callback for the device I/O control request, the framework invokes the callback. Also, because the queue object was created with the WdfIoQueueDispatchParallel flag, the callback is invoked as soon as the request is added to the queue.
VOID
MyUSBDriver_EvtIoDeviceControl(
_In_ WDFQUEUE Queue,
_In_ WDFREQUEST Request,
_In_ size_t OutputBufferLength,
_In_ size_t InputBufferLength,
_In_ ULONG IoControlCode
)
{
TraceEvents(TRACE_LEVEL_INFORMATION,
TRACE_QUEUE,
"!FUNC! Queue 0x%p, Request 0x%p OutputBufferLength %d InputBufferLength %d IoControlCode %d",
Queue, Request, (int) OutputBufferLength, (int) InputBufferLength, IoControlCode);
WdfRequestComplete(Request, STATUS_SUCCESS);
return;
}
When the framework invokes the client driver's event callback, it passes a handle to the framework request object that holds the request (and its input and output buffers) sent by the application. In addition, it sends a handle to the framework queue object that contains the request. In the event callback, the client driver processes the request as needed. The template code simply completes the request. The client driver can perform more involved tasks. For instance, if an application requests certain device information, in the event callback, the client driver can create a USB control request and send it to the USB driver stack to retrieve the requested device information. USB control requests are discussed in USB Control Transfer.