Understanding the USB client driver code structure (UMDF)
In this topic you'll learn about the source code for a UMDF-based USB client driver. The code examples are generated by the USB User-Mode Driver template included with Microsoft Visual Studio 2012. The template code uses the Active Template Library (ATL) to generate the COM infrastructure. ATL and details about the COM implementation in the client driver are not discussed here.
For instructions about generating the UMDF template code, see How to write your first USB client driver (UMDF). The template code is discussed in these sections:
Before discussing the details of the template code, let's look at some declarations in the header file (Internal.h) that are relevant to UMDF driver development.
Internal.h contains these files, included in the Windows Driver Kit (WDK):
#include "atlbase.h" #include "atlcom.h" #include "wudfddi.h" #include "wudfusb.h"
Atlbase.h and atlcom.h include declarations for ATL support. Each class implemented by the client driver implements ATL class public CComObjectRootEx.
Wudfddi.h is always included for UMDF driver development. The header file includes various declarations and definitions of methods and structures that you need to compile a UMDF driver.
Wudfusb.h includes declarations and definitions of UMDF structures and methods that are required to communicate with the USB I/O target objects provided by the framework.
The next block in Internal.h declares a GUID constant for the device interface. Applications can use this GUID to open a handle to the device by using SetupDiXxx APIs. The GUID is registered after the framework creates the device object.
// Device Interface GUID // f74570e5-ed0c-4230-a7a5-a56264465548 DEFINE_GUID(GUID_DEVINTERFACE_MyUSBDriver_UMDF_, 0xf74570e5,0xed0c,0x4230,0xa7,0xa5,0xa5,0x62,0x64,0x46,0x55,0x48);
The next portion declares the tracing macro and the tracing GUID. Note the tracing GUID; you'll need it in order to enable tracing.
#define WPP_CONTROL_GUIDS \ WPP_DEFINE_CONTROL_GUID( \ MyDriver1TraceGuid, (f0261b19,c295,4a92,aa8e,c6316c82cdf0), \ \ WPP_DEFINE_BIT(MYDRIVER_ALL_INFO) \ WPP_DEFINE_BIT(TRACE_DRIVER) \ WPP_DEFINE_BIT(TRACE_DEVICE) \ WPP_DEFINE_BIT(TRACE_QUEUE) \ ) #define WPP_FLAG_LEVEL_LOGGER(flag, level) \ WPP_LEVEL_LOGGER(flag) #define WPP_FLAG_LEVEL_ENABLED(flag, level) \ (WPP_LEVEL_ENABLED(flag) && \ WPP_CONTROL(WPP_BIT_ ## flag).Level >= level) #define WPP_LEVEL_FLAGS_LOGGER(lvl,flags) \ WPP_LEVEL_LOGGER(flags) #define WPP_LEVEL_FLAGS_ENABLED(lvl, flags) \ (WPP_LEVEL_ENABLED(flags) && WPP_CONTROL(WPP_BIT_ ## flags).Level >= lvl)
The next line in Internal.h forward declares the client driver-implemented class for the queue callback object. It also includes other project files generated by the template. The implementation and project header files are discussed later in this topic.
// Forward definition of queue. typedef class CMyIoQueue *PCMyIoQueue; // Include the type specific headers. #include "Driver.h" #include "Device.h" #include "IoQueue.h"
After the client driver is installed, Windows loads the client driver and the framework in an instance of the host process. From here, the framework loads and initializes the client driver. The framework performs these tasks:
- Creates a driver object in the framework, which represents your client driver.
- Requests an IDriverEntry interface pointer from the class factory.
- Creates a device object in the framework.
- Initializes the device object after the PnP Manager starts the device.
While the driver is loading and initializing, several events occur and the framework lets the client driver participate in handling them. On the client driver's side, the driver performs these tasks:
- Implements and exports the DllGetClassObject function from your client driver module so that the framework can get a reference to the driver.
- Provides a callback class that implements the IDriverEntry interface.
- Provides a callback class that implements IPnpCallbackXxx interfaces.
- Gets a reference to the device object and configures it according to the client driver's requirements.
Driver callback source code
The framework creates the driver object, which represents the instance of the client driver loaded by Windows. The client driver provides at least one driver callback that registers the driver with the framework.
The complete source code for the driver callback is in Driver.h and Driver.c.
The client driver must define a driver callback class that implements IUnknown and IDriverEntry interfaces. The header file, Driver.h, declares a class called CMyDriver, which defines the driver callback.
EXTERN_C const CLSID CLSID_Driver; class CMyDriver : public CComObjectRootEx<CComMultiThreadModel>, public CComCoClass<CMyDriver, &CLSID_Driver>, public IDriverEntry { public: CMyDriver() { } DECLARE_NO_REGISTRY() DECLARE_NOT_AGGREGATABLE(CMyDriver) BEGIN_COM_MAP(CMyDriver) COM_INTERFACE_ENTRY(IDriverEntry) END_COM_MAP() public: // IDriverEntry methods virtual HRESULT STDMETHODCALLTYPE OnInitialize( __in IWDFDriver *FxWdfDriver ) { UNREFERENCED_PARAMETER(FxWdfDriver); return S_OK; } virtual HRESULT STDMETHODCALLTYPE OnDeviceAdd( __in IWDFDriver *FxWdfDriver, __in IWDFDeviceInitialize *FxDeviceInit ); virtual VOID STDMETHODCALLTYPE OnDeinitialize( __in IWDFDriver *FxWdfDriver ) { UNREFERENCED_PARAMETER(FxWdfDriver); return; } }; OBJECT_ENTRY_AUTO(CLSID_Driver, CMyDriver)
The driver callback must be a COM class, meaning it must implement IUnknown and the related methods. In the template code, ATL classes CComObjectRootEx and CComCoClass contain the IUnknown methods.
After Windows instantiates the host process, the framework creates the driver object. To do so, the framework creates an instance of the driver callback class and calls drivers implementation of DllGetClassObject (discussed in the Driver entry source code section) and to obtain the client driver’s IDriverEntry interface pointer. That call registers the driver callback object with the framework driver object. Upon successful registration, the framework invokes the client driver's implementation when certain driver-specific events occur. The first method that the framework invokes is the IDriverEntry::OnInitialize method. In the client driver's implementation of IDriverEntry::OnInitialize, the client driver can allocate global driver resources. Those resources must be released in IDriverEntry::OnDeinitialize that is invoked by the framework just before it is preparing to unload the client driver. The template code provides minimal implementation for the OnInitialize and OnDeinitialize methods.
The most important method of IDriverEntry is IDriverEntry::OnDeviceAdd. Before the framework creates the framework device object (discussed in the next section), it calls the driver's IDriverEntry::OnDeviceAdd implementation. When calling the method, the framework passes an IWDFDriver pointer to the driver object and an IWDFDeviceInitialize pointer. The client driver can call IWDFDeviceInitialize methods to specify certain configuration options.
Typically, the client driver performs the following tasks in its IDriverEntry::OnDeviceAdd implementation:
- Specifies configuration information for the device object to be created.
- Instantiates the driver’s device callback class.
- Creates the framework device object and registers its device callback object with the framework.
- Initializes the framework device object.
- Registers the device interface GUID of the client driver.
The following code example shows the IDriverEntry::OnDeviceAdd implementation in the template code.
HRESULT CMyDriver::OnDeviceAdd( __in IWDFDriver *FxWdfDriver, __in IWDFDeviceInitialize *FxDeviceInit ) { HRESULT hr = S_OK; CMyDevice *device = NULL; hr = CMyDevice::CreateInstanceAndInitialize(FxWdfDriver, FxDeviceInit, &device); if (SUCCEEDED(hr)) { hr = device->Configure(); } return hr; }
The following code example shows device class declaration in Device.h.
class CMyDevice : public CComObjectRootEx<CComMultiThreadModel>, public IPnpCallbackHardware { public: DECLARE_NOT_AGGREGATABLE(CMyDevice) BEGIN_COM_MAP(CMyDevice) COM_INTERFACE_ENTRY(IPnpCallbackHardware) END_COM_MAP() CMyDevice() : m_FxDevice(NULL), m_IoQueue(NULL), m_FxUsbDevice(NULL) { } ~CMyDevice() { } private: IWDFDevice * m_FxDevice; CMyIoQueue * m_IoQueue; IWDFUsbTargetDevice * m_FxUsbDevice; private: HRESULT Initialize( __in IWDFDriver *FxDriver, __in IWDFDeviceInitialize *FxDeviceInit ); public: static HRESULT CreateInstanceAndInitialize( __in IWDFDriver *FxDriver, __in IWDFDeviceInitialize *FxDeviceInit, __out CMyDevice **Device ); HRESULT Configure( VOID ); public: // IPnpCallbackHardware methods virtual HRESULT STDMETHODCALLTYPE OnPrepareHardware( __in IWDFDevice *FxDevice ); virtual HRESULT STDMETHODCALLTYPE OnReleaseHardware( __in IWDFDevice *FxDevice ); };
Device callback source code
The framework device object is an instance of the framework class that represents the device object that is loaded in the device stack of the client driver. For information about the functionality of a device object, see Device Nodes and Device Stacks.
The complete source code for the device object is located in Device.h and Device.c.
The framework device class implements the IWDFDevice interface. The client driver is responsible for creating an instance of that class in the driver's implementation of IDriverEntry::OnDeviceAdd. After the object is created, the client driver obtains an IWDFDevice pointer to the new object and calls methods on that interface to manage the operations of the device object.
IDriverEntry::OnDeviceAdd implementation
In the previous section, you briefly saw the tasks that a client driver performs in IDriverEntry::OnDeviceAdd. Here's more information about those tasks. The client driver:
- Specifies configuration information for the device object to be created.
In the framework call to the client driver's implementation of the IDriverEntry::OnDeviceAdd method, the framework passes a IWDFDeviceInitialize pointer. The client driver uses this pointer to specify configuration information for the device object to be created. For example, the client driver specifies whether the client driver is a filter or a function driver. To identify the client driver as a filter driver, it calls IWDFDeviceInitialize::SetFilter. In that case, the framework creates a filter device object (FiDO); otherwise, a function device object (FDO) is created. Another option that you can set is the synchronization mode by calling IWDFDeviceInitialize::SetLockingConstraint.
- Calls the IWDFDriver::CreateDevice method by passing the IWDFDeviceInitialize interface pointer, an IUnknown reference of the device callback object, and a pointer-to-pointer IWDFDevice variable.
If the IWDFDriver::CreateDevice call is successful:
- The framework creates the device object.
- The framework registers the device callback with the framework.
After the device callback is paired with the framework device object, the framework and the client driver handle certain events, such as PnP state and power state changes. For example, when the PnP Manager starts the device, the framework is notified. The framework then invokes the device callback's IPnpCallbackHardware::OnPrepareHardware implementation. Every client driver must register at least one device callback object.
- The client driver receives the address of the new device object in the IWDFDevice variable. Upon receiving a pointer to the framework device object, the client driver can proceed with initialization tasks, such as setting up queues for I/O flow and registering the device interface GUID.
- Calls IWDFDevice::CreateDeviceInterface to register the device interface GUID of the client driver. The applications can use the GUID to send requests to the client driver. The GUID constant is declared in Internal.h.
- Initializes queues for I/O transfers to and from the device.
The template code defines the helper method Initialize, which specifies configuration information and creates the device object.
The following code example shows implementations for Initialize.
HRESULT CMyDevice::Initialize( __in IWDFDriver * FxDriver, __in IWDFDeviceInitialize * FxDeviceInit ) { IWDFDevice *fxDevice = NULL; HRESULT hr = S_OK; IUnknown *unknown = NULL; TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DEVICE, "%!FUNC! Entry"); FxDeviceInit->SetLockingConstraint(None); FxDeviceInit->SetPowerPolicyOwnership(TRUE); hr = this->QueryInterface(__uuidof(IUnknown), (void **)&unknown); if (FAILED(hr)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE, "%!FUNC! Failed to get IUnknown %!hresult!", hr); goto Exit; } hr = FxDriver->CreateDevice(FxDeviceInit, unknown, &fxDevice); DriverSafeRelease(unknown); if (FAILED(hr)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE, "%!FUNC! Failed to create a framework device %!hresult!", hr); goto Exit; } m_FxDevice = fxDevice; DriverSafeRelease(fxDevice); Exit: TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DEVICE, "%!FUNC! Exit"); return hr; }
In the preceding code example, the client driver creates the device object and registers its device callback. Before creating the device object, the driver specifies its configuration preference by calling methods on the IWDFDeviceInitialize interface pointer. That is the same pointer passed by the framework in its previous call to the client driver's IDriverEntry::OnDeviceAdd method.
The client driver specifies that it will be the power policy owner for the device object. As the power policy owner, the client driver determines the appropriate power state that the device should enter when the system power state changes. The driver is also responsible for sending relevant requests to the device in order to make the power state transition. By default, a UMDF-based client driver is not the power policy owner; the framework handles all power state transitions. The framework automatically sends the device to D3 when the system enters a sleep state, and conversely brings the device back to D0 when the system enters the working state of S0. For more information, see Power Policy Ownership in UMDF.
Another configuration option is to specify whether the client driver is the filter driver or the function driver for the device. Notice that in the code example, the client driver does not explicitly specify its preference. That means the client driver is the function driver and the framework should create an FDO in the device stack. If the client driver wants to be the filter driver, then the driver must call the IWDFDeviceInitialize::SetFilter method. In that case, the framework creates a FiDO in the device stack.
The client driver also specifies that none of the framework's calls to the client driver's callbacks are synchronized. The client driver handles all synchronization tasks. To specify that preference, the client driver calls the IWDFDeviceInitialize::SetLockingConstraint method.
Next, the client driver obtains an IUnknown pointer to its device callback class by calling IUnknown::QueryInterface. Subsequently, the client driver calls IWDFDriver::CreateDevice, which creates the framework device object and registers the client driver's device callback by using the IUnknown pointer.
Notice that the client driver stores the address of the device object (received through the IWDFDriver::CreateDevice call) in a private data member of the device callback class and then releases that reference by calling DriverSafeRelease (inline function defined in Internal.h). That is because the lifetime of the device object is tracked by the framework. Therefore the client driver is not required to keep additional reference count of the device object.
The template code defines the public method Configure, which registers the device interface GUID and sets up queues. The following code example shows the definition of the Configure method in the device callback class, CMyDevice. Configure is called by IDriverEntry::OnDeviceAdd after the framework device object is created.
CMyDevice::Configure( VOID ) { HRESULT hr = S_OK; TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DEVICE, "%!FUNC! Entry"); hr = CMyIoQueue::CreateInstanceAndInitialize(m_FxDevice, this, &m_IoQueue); if (FAILED(hr)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE, "%!FUNC! Failed to create and initialize queue %!hresult!", hr); goto Exit; } hr = m_IoQueue->Configure(); if (FAILED(hr)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE, "%!FUNC! Failed to configure queue %!hresult!", hr); goto Exit; } hr = m_FxDevice->CreateDeviceInterface(&GUID_DEVINTERFACE_MyUSBDriver_UMDF_,NULL); if (FAILED(hr)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE, "%!FUNC! Failed to create device interface %!hresult!", hr); goto Exit; } Exit: TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DEVICE, "%!FUNC! Exit"); return hr; }
In the preceding code example, the client driver performs two main tasks: initializing queues for I/O flow and registering the device interface GUID.
The queues are created and configured in the CMyIoQueue class. The first task is to instantiate that class by calling the static method named CreateInstanceAndInitialize. The client driver calls Configure to initialize queues. CreateInstanceAndInitialize and Configure are declared in CMyIoQueue, which is discussed later in this topic.
The client driver also calls IWDFDevice::CreateDeviceInterface to register the device interface GUID of the client driver. The applications can use the GUID to send requests to the client driver. The GUID constant is declared in Internal.h.
IPnpCallbackHardware implementation and USB-specific tasks
Next, let's look at the implementation of the IPnpCallbackHardware interface in Device.cpp.
Every device callback class must implement the IPnpCallbackHardware interface. This interface has two methods: IPnpCallbackHardware::OnPrepareHardware and IPnpCallbackHardware::OnReleaseHardware. The framework calls those methods in response to two events: when the PnP Manager starts the device and when it removes the device. When a device is started, communication to the hardware is established but the device has not entered Working state (D0). Therefore, in IPnpCallbackHardware::OnPrepareHardware the client driver can get device information from the hardware, allocate resources, and initialize framework objects that are required during the lifetime of the driver. When the PnP Manager removes the device, the driver is unloaded from the system. The framework calls the client driver's IPnpCallbackHardware::OnReleaseHardware implementation in which the driver can release those resources and framework objects.
PnP Manager can generate other types of events that result from PnP state changes. The framework provides default handling for those events. The client driver can choose to participate in the handling of those events. Consider a scenario where the USB device is detached from the host. The PnP Manager recognizes that event and notifies the framework. If the client driver wants to perform additional tasks in response to the event, the driver must implement the IPnpCallback interface and the related IPnpCallback::OnSurpriseRemoval method in the device callback class. Otherwise, the framework proceeds with its default handling of the event.
A USB client driver must retrieve information about the supported interfaces, alternate settings, and endpoints and configure them before sending any I/O requests for data transfer. UMDF provides specialized I/O target objects that simplify many of the configuration tasks for the client driver. To configure a USB device, the client driver requires device information that is available only after the PnP Manager starts the device.
This template code creates those objects in the IPnpCallbackHardware::OnPrepareHardware method.
Typically, the client driver performs one or more of these configuration tasks (depending on the design of the device):
- Retrieves information about the current configuration, such as the number of interfaces. The framework selects the first configuration on a USB device. The client driver cannot select another configuration in the case of multi-configuration devices.
- Retrieves information about interfaces, such as the number of endpoints.
- Changes the alternate setting within each interface, if the interface supports more than one setting. By default, the framework selects the first alternate setting of each interface in the first configuration on a USB device. The client driver can choose to select an alternate setting.
- Retrieves information about endpoints within each interface.
To perform those tasks, the client driver can use these types of specialized USB I/O target objects provided by the WDF.
USB I/O target object | Description | UMDF interface |
---|---|---|
Target device object | Represents a USB device and provides methods for retrieving the device descriptor and sending control requests to the device. | IWDFUsbTargetDevice |
Target interface object | Represents an individual interface and provides methods that a client driver can call to select an alternate setting and retrieve information about the setting. | IWDFUsbInterface |
Target pipe object | Represents an individual pipe for an endpoint that is configured in the current alternate setting for an interface. The USB bus driver 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 called a pipe. | IWDFUsbTargetPipe |
The following code example shows the implementation for IPnpCallbackHardware::OnPrepareHardware.
HRESULT CMyDevice::OnPrepareHardware( __in IWDFDevice * /* FxDevice */ ) { HRESULT hr; IWDFUsbTargetFactory *usbFactory = NULL; IWDFUsbTargetDevice *usbDevice = NULL; TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DEVICE, "%!FUNC! Entry"); hr = m_FxDevice->QueryInterface(IID_PPV_ARGS(&usbFactory)); if (FAILED(hr)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE, "%!FUNC! Failed to get USB target factory %!hresult!", hr); goto Exit; } hr = usbFactory->CreateUsbTargetDevice(&usbDevice); if (FAILED(hr)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE, "%!FUNC! Failed to create USB target device %!hresult!", hr); goto Exit; } m_FxUsbDevice = usbDevice; Exit: DriverSafeRelease(usbDevice); DriverSafeRelease(usbFactory); TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DEVICE, "%!FUNC! Exit"); return hr; }
To use the framework's USB I/O target objects, the client driver must first create the USB target device object. In the framework object model, the USB target device object is a child of the device object that represents a USB device. The USB target device object is implemented by the framework and performs all device-level tasks of a USB device, such as selecting a configuration.
In the preceding code example, the client driver queries the framework device object and gets an IWDFUsbTargetFactory pointer to the class factory that creates the USB target device object. By using that pointer, the client driver calls the IWDFUsbTargetDevice::CreateUsbTargetDevice method. The method creates the USB target device object and returns a pointer to the IWDFUsbTargetDevice interface. The method also selects the default (first) configuration and the alternate setting 0 for each interface in that configuration.
The template code stores the address of the USB target device object (received through the IWDFDriver::CreateDevice call) in a private data member of the device callback class and then releases that reference by calling DriverSafeRelease. The reference count of the USB target device object is maintained by the framework. The object is alive as long as the device object is alive. The client driver must release the reference in IPnpCallbackHardware::OnReleaseHardware.
After the client driver creates the USB target device object, the driver calls IWDFUsbTargetDevice methods to perform these tasks:
- Retrieve the device, configuration, interface descriptors, and other information such as device speed.
- Format and send I/O control requests to the default endpoint.
- Set the power policy for the entire USB device.
The following code example shows the implementation for IPnpCallbackHardware::OnReleaseHardware.
HRESULT CMyDevice::OnReleaseHardware( __in IWDFDevice * /* FxDevice */ ) { TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DEVICE, "%!FUNC! Entry"); if (m_FxUsbDevice != NULL) { m_FxUsbDevice->DeleteWdfObject(); m_FxUsbDevice = NULL; } TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DEVICE, "%!FUNC! Exit"); return S_OK; }
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 IoQueue.h and IoQueue.c.
IoQueue.h
The header file IoQueue.h declares the queue callback class.
class CMyIoQueue : public CComObjectRootEx<CComMultiThreadModel>, public IQueueCallbackDeviceIoControl { public: DECLARE_NOT_AGGREGATABLE(CMyIoQueue) BEGIN_COM_MAP(CMyIoQueue) COM_INTERFACE_ENTRY(IQueueCallbackDeviceIoControl) END_COM_MAP() CMyIoQueue() : m_FxQueue(NULL), m_Device(NULL) { } ~CMyIoQueue() { // empty } HRESULT Initialize( __in IWDFDevice *FxDevice, __in CMyDevice *MyDevice ); static HRESULT CreateInstanceAndInitialize( __in IWDFDevice *FxDevice, __in CMyDevice *MyDevice, __out CMyIoQueue** Queue ); HRESULT Configure( VOID ) { return S_OK; } // IQueueCallbackDeviceIoControl virtual VOID STDMETHODCALLTYPE OnDeviceIoControl( __in IWDFIoQueue *pWdfQueue, __in IWDFIoRequest *pWdfRequest, __in ULONG ControlCode, __in SIZE_T InputBufferSizeInBytes, __in SIZE_T OutputBufferSizeInBytes ); private: IWDFIoQueue * m_FxQueue; CMyDevice * m_Device; };
In the preceding code example, the client driver declares the queue callback class. When instantiated, the object is partnered with the framework queue object that handles the way requests are dispatched to the client driver. The class defines two methods that create and initialize the framework queue object. The static method CreateInstanceAndInitialize instantiates the queue callback class and then calls the Initialize method that creates and initializes the framework queue object. It also specifies the dispatch options for the queue object.
HRESULT CMyIoQueue::CreateInstanceAndInitialize( __in IWDFDevice *FxDevice, __in CMyDevice *MyDevice, __out CMyIoQueue** Queue ) { CComObject<CMyIoQueue> *pMyQueue = NULL; HRESULT hr = S_OK; TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_QUEUE, "%!FUNC! Entry"); hr = CComObject<CMyIoQueue>::CreateInstance( &pMyQueue ); if (FAILED(hr)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_QUEUE, "%!FUNC! Failed to create instance %!hresult!", hr); goto Exit; } hr = pMyQueue->Initialize(FxDevice, MyDevice); if (FAILED(hr)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_QUEUE, "%!FUNC! Failed to initialize %!hresult!", hr); goto Exit; } *Queue = pMyQueue; Exit: TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_QUEUE, "%!FUNC! Exit"); return hr; }
The following code example shows the implementation of the Initialize method.
HRESULT CMyIoQueue::Initialize( __in IWDFDevice *FxDevice, __in CMyDevice *MyDevice ) { IWDFIoQueue *fxQueue = NULL; HRESULT hr = S_OK; IUnknown *unknown = NULL; TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_QUEUE, "%!FUNC! Entry"); assert(FxDevice != NULL); assert(MyDevice != NULL); hr = this->QueryInterface(__uuidof(IUnknown), (void **)&unknown); if (FAILED(hr)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_QUEUE, "%!FUNC! Failed to query IUnknown interface %!hresult!", hr); goto Exit; } hr = FxDevice->CreateIoQueue(unknown, FALSE, // Default Queue? WdfIoQueueDispatchParallel, // Dispatch type TRUE, // Power managed? FALSE, // Allow zero-length requests? &fxQueue); // I/O queue DriverSafeRelease(unknown); if (FAILED(hr)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_QUEUE, "%!FUNC! Failed to create framework queue."); goto Exit; } hr = FxDevice->ConfigureRequestDispatching(fxQueue, WdfRequestDeviceIoControl, TRUE); if (FAILED(hr)) { TraceEvents(TRACE_LEVEL_ERROR, TRACE_QUEUE, "%!FUNC! Failed to configure request dispatching %!hresult!.", hr); goto Exit; } m_FxQueue = fxQueue; m_Device= MyDevice; Exit: DriverSafeRelease(fxQueue); TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_QUEUE, "%!FUNC! Exit"); return hr; }
In the preceding code example, the client driver creates the framework queue object. The framework provides the queue object to handle the request flow to the client driver.
To create the object, the client driver calls IWDFDevice::CreateIoQueue on the IWDFDevice reference obtained in a previous call to IWDFDriver::CreateDevice.
In the IWDFDevice::CreateIoQueue call, the client driver specifies certain configuration options before the framework creates queues. Those options determine whether the queue is power-managed, allows zero-length requests, and acts as the default queue for the driver. The client driver provides this set of information:
-
Reference to its queue callback class
Specifies an IUnknown pointer to its queue callback class. This creates a partnership between the framework queue object and the client driver's queue callback object. When the I/O Manager receives a new request from an application, it notifies the framework. The framework then uses the IUnknown pointer to invoke the public methods exposed by the queue callback object.
-
Default or secondary queue
The queue must be either the default queue or a secondary queue. If the framework queue object acts as the default queue, then all requests are added to the queue. A secondary queue is dedicated to a specific type of request. If the client driver requests a secondary queue, then the driver must also call the IWDFDevice::ConfigureRequestDispatching method to indicate the type of request that the framework must put in the specified queue. In the template code, the client driver passes FALSE in the bDefaultQueue parameter. That instructs the method to create a secondary queue and not the default queue. It later calls IWDFDevice::ConfigureRequestDispatching to indicate that the queue must have only device I/O control requests (see the example code in this section).
-
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 that the client driver can receive a 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. In the template code, the client driver requests for a parallel dispatch mode.
-
Power-managed queue
The framework queue object must be synchronized with the PnP and power state of the device. If the device is not in Working state, the framework queue object stops dispatching all requests. When the device is in Working state, the queue object resumes dispatching. In a power-managed queue, the synchronization is performed by the framework; otherwise the client drive must handle that task. In the template code, the client requests a power-managed queue.
-
Zero-length requests allowed
A client driver can instruct the framework to complete I/O requests with zero-length buffers instead of putting them in the queue. In the template code, the client requests the framework to complete such requests.
A single framework queue object can handle several types of requests, such as read, write, and device I/O control, and so on. A client driver based on the template code can process only device I/O control requests. For that, the client driver's queue callback class implements the IQueueCallbackDeviceIoControl interface and its IQueueCallbackDeviceIoControl::OnDeviceIoControl method. This allows the framework to invoke the client driver's implementation of IQueueCallbackDeviceIoControl::OnDeviceIoControl when the framework processes a device I/O control request.
For other types of requests, the client driver must implement the corresponding IQueueCallbackXxx interface. For example, if the client driver wants to handle read requests, the queue callback class must implement the IQueueCallbackRead interface and its IQueueCallbackRead::OnRead method. For information about the types of requests and callback interfaces, see I/O Queue Event Callback Functions.
The following code example shows the IQueueCallbackDeviceIoControl::OnDeviceIoControl implementation.
VOID STDMETHODCALLTYPE CMyIoQueue::OnDeviceIoControl( __in IWDFIoQueue *FxQueue, __in IWDFIoRequest *FxRequest, __in ULONG ControlCode, __in SIZE_T InputBufferSizeInBytes, __in SIZE_T OutputBufferSizeInBytes ) { UNREFERENCED_PARAMETER(FxQueue); UNREFERENCED_PARAMETER(ControlCode); UNREFERENCED_PARAMETER(InputBufferSizeInBytes); UNREFERENCED_PARAMETER(OutputBufferSizeInBytes); HRESULT hr = S_OK; TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_QUEUE, "%!FUNC! Entry"); if (m_Device == NULL) { // We don't have pointer to device object TraceEvents(TRACE_LEVEL_ERROR, TRACE_QUEUE, "%!FUNC!NULL pointer to device object."); hr = E_POINTER; goto Exit; } // // Process the IOCTLs // Exit: FxRequest->Complete(hr); TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_QUEUE, "%!FUNC! Exit"); return; }
Let's see how the queue mechanism works. To communicate with the USB device, an application first opens a handle to the device and sends a device I/O control request by calling 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 notifies the framework. The framework creates a framework request object and adds it to the framework queue object. In the template code, because the queue object was created with the WdfIoQueueDispatchParallel flag, the callback is invoked as soon as the request is added to the queue.
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 that 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.
Driver Entry source code
In the template code, driver entry is implemented in the Dllsup.cpp.
Dllsup.cpp
After the include section, a GUID constant for the client driver is declared. That GUID must match the GUID in the driver's installation file (INF).
const CLSID CLSID_Driver =
{0x079e211c,0x8a82,0x4c16,{0x96,0xe2,0x2d,0x28,0xcf,0x23,0xb7,0xff}};
The next block of code declares the class factory for the client driver.
class CMyDriverModule : public CAtlDllModuleT< CMyDriverModule > { }; CMyDriverModule _AtlModule;
The template code uses ATL support to encapsulate complex COM code. The class factory inherits the template class CAtlDllModuleT that contains all the necessary code for creating the client driver.
The following code snippet shows the implementation of DllMain
extern "C" BOOL WINAPI DllMain( HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved ) { if (dwReason == DLL_PROCESS_ATTACH) { WPP_INIT_TRACING(MYDRIVER_TRACING_ID); g_hInstance = hInstance; DisableThreadLibraryCalls(hInstance); } else if (dwReason == DLL_PROCESS_DETACH) { WPP_CLEANUP(); } return _AtlModule.DllMain(dwReason, lpReserved); }
If your client driver implements the DllMain function, Windows considers DllMain to be the entry point for the client driver module. Windows calls DllMain after loading the client driver module in WUDFHost.exe. Windows calls DllMain again just before Windows unloads the client driver in memory. DllMain can allocate and free global variables at the driver level. In the template code, the client driver initializes and releases the resources required for WPP tracing and invokes the ATL class' DllMain implementation.
For information about how to write your DllMain, see Implementing DllMain.
The following code snippet shows the implementation of DllGetClassObject.
STDAPI
DllGetClassObject(
__in REFCLSID rclsid,
__in REFIID riid,
__deref_out LPVOID FAR* ppv
)
{
return _AtlModule.DllGetClassObject(rclsid, riid, ppv);
}
In the template code the class factory and DllGetClassObject are implemented in ATL. The preceding code snippet simply invokes the ATL DllGetClassObject implementation. In general, DllGetClassObject must perform the following tasks:
- Ensure that the CLSID passed by the framework is the GUID for your client driver. The framework retrieves the CLSID for the client driver from the driver's INF file. While validating, make sure that the specified GUID matches the one that you provided in the INF.
- Instantiate the class factory implemented by the client driver. In the template code this is encapsulated by the ATL class.
- Get a pointer to the IClassFactory interface of the class factory and return the retrieved pointer to the framework.
After the client driver module is loaded in memory, the framework calls the driver-supplied DllGetClassObject function. In the framework's call to DllGetClassObject, the framework passes the CLSID that identifies the client driver and requests a pointer to the IClassFactory interface of a class factory. The client driver implements the class factory that facilitates the creation of the driver callback. Therefore, your client driver must contain at least one class factory. The framework then calls IClassFactory::CreateInstance and requests an IDriverEntry pointer to the driver callback class.
Exports.def
In order for the framework to call DllGetClassObject, the client driver must export the function from a .def file. The file is already include in the Visual Studio project.
; Exports.def : Declares the module parameters.
LIBRARY "MyUSBDriver_UMDF_.DLL"
EXPORTS
DllGetClassObject PRIVATE
In the preceding code snippet from Export.def included with the driver project, the client provides the the name of the driver module as the LIBRARY, and DllGetClassObject under EXPORTS. For more information, see Exporting from a DLL Using DEF Files.