HIDCLASS Minidrivers
As previously discussed, Microsoft supplies a driver (HIDUSB.SYS) for any USB device built according to the HID specification. This section describes how you can build a HIDCLASS minidriver for some other type of device that you want to have masquerade as HID.
DriverEntry
The DriverEntry function for a HIDCLASS minidriver is similar to that in a regular WDM driver, but only up to a point. In this routine, you initialize the DRIVER_OB JECT data structure with pointers to AddDevice and DriverUnload routines as well as to dispatch routines for just three types of I/O request packet (IRP): IRP_MJ_PNP, IRP_MJ_POWER, and IRP_MJ_INTERNAL_DEVICE_CON TROL. Then you build a HID_MINIDRIVER_REGISTRATION structure and call HidRegister Mini driver, which is one of the functions exported by HIDCLASS.SYS. Table 13-2 describes the fields in the HID_MINIDRIVER_REGI STRATION structure.
Field Name | Description |
Revision | (ULONG) Minidriver sets this field to HID_REVISION, which currently equals 1. |
DriverObject | (PDRIVER_OBJECT) Minidriver sets this field to the same value passed as the DriverObject argument to DriverEntry. |
RegistryPath | (PUNICODE_STRING) Minidriver sets this field to the same value passed as the RegistryPath argument to DriverEntry. |
DeviceExtensionSize | (ULONG) Size in bytes of the device extension structure used by the minidriver. |
DevicesArePolled | (BOOLEAN) TRUE if the minidriver s devices need to be polled for reports. FALSE if the devices spontaneously send reports when data becomes available. |
The only field whose meaning isn t completely straightforward is the DevicesArePolled flag. Most devices will spontaneously generate a report whenever the end user does something, and they ll notify the host via some sort of interrupt. For this kind of device, you set the DevicesArePolled flag to FALSE. HIDCLASS will then attempt to keep two IRPs (called ping-pong IRPs) active to read reports. Your minidriver is expected to queue the IRPs and complete them in order when the device interrupts.
Some devices don t spontaneously generate reports. For that kind of device, set the DevicesArePolled flag to TRUE. HIDCLASS will then issue IRPs to read reports in a timing loop. Your minidriver reads report data from the device only in response to each IRP. Higher-level components, such as an application using DirectX interfaces, can specify the polling interval. Think twice before setting DevicesArePolled to TRUE: most devices require it to be FALSE.
Here s a nearly complete example of the DriverEntry function in a HIDCLASS minidriver:
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { DriverObject->DriverExtension->AddDevice = AddDevice; DriverObject->DriverUnload = DriverUnload; DriverObject->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL] = DispatchInternalControl; DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp; DriverObject->MajorFunction[IRP_MJ_POWER] = DispatchPower; HID_MINIDRIVER_REGISTRATION reg; RtlZeroMemory(®, sizeof(reg)); reg.Revision = HID_REVISION; reg.DriverObject = DriverObject; reg.RegistryPath = RegistryPath; reg.DeviceExtensionSize = sizeof(DEVICE_EXTENSION); reg.DevicesArePolled = FALSE; // <== depends on your hardware return HidRegisterMinidriver(®); }
Driver Callback Routines
Most of the class/minidriver interfaces in Windows XP involve a set of callback routines that the minidriver specifies in a registration call made from DriverEntry. Most class drivers completely take over the management of the DRIVER_OBJECT while handling the registration call. This means that the class drivers each install their own AddDevice and DriverUnload functions and their own IRP dispatch routines. The class drivers then make calls to the minidriver callback routines to carry out vendor-specific actions.
HIDCLASS operates this way as well, but with a twist. When you call HidRegisterMinidriver, HIDCLASS installs its own function pointers in your DRIVER_OBJECT, just as most class drivers would. Instead of using a set of callback routines whose addresses your minidriver provides in the HID_MINI DRIVER_REGISTRATION structure (there are none), it remembers the Add Device and DriverUnload pointers and the addresses of your dispatch routines for PNP, POWER, and INTERNAL_DEVICE_CONTROL requests. These mini driver routines do not have exactly the same functions as like-named routines in regular WDM drivers, though. I ll explain in this section how to write these callback routines.
AddDevice Callback
The AddDevice callback in a HIDCLASS minidriver has a prototype similar to that of a regular AddDevice function:
NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT fdo);
There are two important differences between the minidriver callback and the regular function:
The device object argument refers to a function device object (FDO) that HIDCLASS has already created.
Prior to Windows XP, HIDCLASS ignored the return code from the minidriver callback.
Since HIDCLASS creates the FDO before calling your minidriver AddDevice callback, you don t need to call IoCreateDevice or, indeed, to do practically any of the things that you normally do in a WDM AddDevice function. Rather, you just need to initialize your device extension structure and return. Versions of Windows prior to Windows XP will ignore the return code from this callback. Consequently, if an error arises in your AddDevice callback, you need to set a flag in your device extension that you can inspect at StartDevice time:
typedef struct _DEVICE_EXTENSION { NTSTATUS AddDeviceStatus; } DEVICE_EXTENSION, *PDEVICE_EXTENSION;
Another issue to be aware of is that the FDO s DeviceExtension pointer is the address of an extension structure that s private to HIDCLASS. The first few members of that private structure are mapped by the HID_DEVICE_EXTENSION structure in the DDK:
typedef struct _HID_DEVICE_EXTENSION { PDEVICE_OBJECT PhysicalDeviceObject; PDEVICE_OBJECT NextDeviceObject; PVOID MiniDeviceExtension; } HID_DEVICE_EXTENSION, *PHID_DEVICE_EXTENSION;
To find your device extension, you must follow this pointer chain:
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) ((PHID_DEVICE_EXTENSION) (fdo->DeviceExtension))->MiniDeviceExtension;
You use similar constructions to get the PDO address and to get what I call the LowerDeviceObject in this book. (HIDCLASS calls it the NextDeviceObject.) Being such a lazy typist, I usually define macros to make my life easier while I m writing the minidriver:
#define PDX(fdo) ((PDEVICE_EXTENSION) ((PHID_DEVICE_EXTENSION) \ ((fdo)->DeviceExtension))->MiniDeviceExtension) #define PDO(fdo) (((PHID_DEVICE_EXTENSION) ((fdo)->DeviceExtension)) \ ->PhysicalDeviceObject) #define LDO(fdo) (((PHID_DEVICE_EXTENSION) ((fdo)->DeviceExtension)) \ ->NextDeviceObject)
Using these macros and the preceding fragment of a DEVICE_EXTENSION structure, your minidriver s AddDevice callback might look like this:
NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT fdo) { PDEVICE_EXTENSION pdx = PDX(fdo); NTSTATUS status = STATUS_SUCCESS; <initialization code for DEVICE_EXTENSION members> pdx->AddDeviceStatus = status; // <== whatever is left over return status; // in case you're running in >= XP }
The point of returning a real status code from AddDevice is that in Windows XP and later systems, HIDCLASS will fail its own AddDevice call if you do, and that will short-circuit the initialization of your device. But since HIDCLASS ignores the code in earlier versions of the operating system, you need to provide a way for your StartDevice function to return an error code.
DriverUnload Callback
HIDCLASS calls your minidriver DriverUnload callback as a subroutine from its own DriverUnload routine. If you created any global objects in your DriverEntry routine, you ll clean those up in the DriverUnload callback.
DispatchPnp Callback
You specify the DispatchPnp callback as if it were the dispatch function for IRP_MJ_PNP, by setting an array element in the driver object s MajorFunction table. HIDCLASS calls your callback as a subroutine while handling Plug and Play IRPs of various types. Your callback routine can perform most of the same operations that a function driver would perform for this type of IRP. See Chapter 6 for full details. There are two exceptions:
Your IRP_MN_START_DEVICE handler needs to test the error flag set by your AddDevice callback (I called it AddDeviceStatus in the earlier fragment) and to fail the IRP if the flag indicates an error. This is how you cope with the fact that HIDCLASS ignores the return code from AddDevice in versions of Windows prior to Windows XP.
Your IRP_MN_REMOVE_DEVICE handler does not call IoDetach Device or IoDeleteDevice. Instead, it should simply release any resources that were allocated by the AddDevice callback. HIDCLASS itself will take care of detaching and deleting the FDO.
The HIDFAKE sample driver uses GENERIC.SYS. Its DispatchPnp routine therefore looks like this:
NTSTATUS DispatchPnp(PDEVICE_OBJECT fdo, PIRP Irp) { return GenericDispatchPnp(PDX(fdo)->pgx, Irp); }
Apart from using the PDX macro to locate the device extension structure, this code is the same as would appear in a regular function driver that uses GENERIC.SYS. The RemoveDevice, StartDevice, and StopDevice functions are different from regular ones, though:
VOID RemoveDevice(PDEVICE_OBJECT fdo) { } NTSTATUS StartDevice(PDEVICE_OBJECT fdo, PCM_PARTIAL_RESOURCE_LIST raw, PCM_PARTIAL_RESOURCE_LIST translated) { PDEVICE_EXTENSION pdx = PDX(fdo); if (!NT_SUCCESS(pdx->AddDeviceStatus)) return pdx->AddDeviceStatus; return STATUS_SUCCESS; } VOID StopDevice(PDEVICE_OBJECT fdo, BOOLEAN oktouch) { }
HIDFAKE itself has no code at the points labeled A, B, and C. If you use this sample as a template for your own minidriver, you ll write code to do the following:
Clean up any resources (such as memory, lookaside lists, and so on) allocated in AddDevice. HIDFAKE has no such resources.
Configure the device as discussed in previous chapters. HIDFAKE has no hardware and therefore has nothing to do in this step.
Deconfigure the device by reversing the steps done in StartDevice. Since HIDFAKE does nothing in StartDevice, it doesn t need to do anything here either.
DispatchPower Callback
You specify the DispatchPower callback as if it were the dispatch routine for IRP_MJ_POWER, by setting an array element in the driver object s MajorFunction table. HIDCLASS calls your callback as a subroutine while handling power IRPs of various types. In most cases, your callback should simply pass the IRP down to the next driver without performing any other actions because HIDCLASS contains all the power-management support needed by typical devices (including WAIT_WAKE support).
If you set the DevicesArePolled flag to FALSE in your call to HidRegisterMinidriver, HIDCLASS will cancel its ping-pong IRPs before forwarding a power request that reduces power. If you have simply piggybacked on these IRPs to send requests further down the PnP stack, you therefore won t need to worry about cancelling them. If you have cached pointers to these IRPs somewhere, you should provide a cancel routine.
NOTE
If your minidriver uses GENERIC.SYS, consider using the GenericCacheControlRequest and GenericUncacheControlRequest functions to keep track of IRPs that you pend. These functions include race-safe cancel logic.
Here s an example of the DispatchPower callback in a HIDCLASS minidriver:
NTSTATUS DispatchPower(PDEVICE_OBJECT fdo, PIRP Irp) { PDEVICE_EXTENSION pdx = PDX(fdo); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); IoCopyCurrentIrpStackLocationToNext(Irp); if (stack->MinorFunction == IRP_MN_SET_POWER && stack->Parameters.Power.Type == DevicePowerState) { DEVICE_POWER_STATE newstate = stack->Parameters.Power.State.DeviceState; if (newstate == PowerDeviceD0) { IoSetCompletionRoutine(Irp, (PIO_COMPLETION_ROUTINE) PowerUpCompletionRoutine, (PVOID) pdx, TRUE, TRUE, TRUE); } else if (pdx->devpower == PowerDeviceD0) { // TODO save context information, if any pdx->devpower = newstate; } } return PoCallDriver(LDO(fdo), Irp); } NTSTATUS PowerUpCompletionRoutine(PDEVICE_OBJECT fdo, PIRP Irp, PDEVICE_EXTENSION pdx) { // TODO restore device context without blocking this thread pdx->devpower = PowerDeviceD0; return STATUS_SUCCESS; }
You needn t do anything special with any power IRP except a SET_POWER for a device power state.
When restoring power, you install a completion routine before forwarding the IRP down the stack.
When removing power, you save any context information before forwarding the IRP. To deal with the possibility that HIDCLASS might lower power in steps (for example, first D2 and later D3), you also need to keep track of the current device power state. Whether or not your device has context information to save, this is also the time to cancel any subsidiary IRPs that your driver has issued, terminate polling threads, and so on. HIDCLASS will be calling you at PASSIVE_LEVEL in a system thread that you re allowed to block if necessary while performing these tasks.
As usual, you call PoCallDriver to forward the IRP. You need not call PoStartNextPowerIrp because HIDCLASS has already done so.
The completion routine is called only after the bus driver completes a Set-D0 operation. Your device has now been repowered, and you can reverse the steps you performed when you removed power. Since you re potentially running at DISPATCH_LEVEL and in an arbitrary thread, however, you must perform these steps without blocking the current thread.
DispatchInternalControl Callback
You specify the DispatchInternalControl callback as if it were the dispatch routine for IRP_MJ_INTERNAL_DEVICE_CONTROL, by setting an array element in the driver object s MajorFunction table. HIDCLASS calls your callback routine as a subroutine in order to obtain reports and other information or to provide instructions to your minidriver. You can program this callback as if it were an ordinary IRP dispatch routine handling the control codes listed in Table 13-3.
Internal Control Code | Description |
IOCTL_GET_PHYSICAL_DESCRIPTOR | Gets USB-standard physical descriptor |
IOCTL_HID_GET_DEVICE_ATTRIBUTES | Returns information about device as if it were USB |
IOCTL_HID_GET_DEVICE_DESCRIPTOR | Returns a USB-standard HID descriptor |
IOCTL_HID_GET_FEATURE | Reads a feature report |
IOCTL_HID_GET_INDEXED_STRING | Returns a USB-standard string descriptor |
IOCTL_HID_GET_STRING | Returns a USB-standard string descriptor |
IOCTL_HID_GET_REPORT_DESCRIPTOR | Returns a USB-standard report descriptor |
IOCTL_HID_READ_REPORT | Reads a report conforming to the report descriptor |
IOCTL_HID_SEND_IDLE_NOTIFICATION | Idles the device (new in Windows XP) |
IOCTL_HID_SET_FEATURE | Writes a feature report |
IOCTL_HID_WRITE_REPORT | Writes a report |
NOTE
The list of minidriver control operations in the DDK differs from the one presented here. I relied on a particular version of the source code for HIDCLASS in compiling this list. It s possible that the DDK documentation is based on earlier versions or on support that was originally planned but never implemented.
I ll discuss these control operations in detail in the next section of this chapter. They all share several common features, however:
In general, HIDCLASS can call your DispatchInternalControl callback at any interrupt request level (IRQL) less than or equal to DISPATCH_LEVEL and in an arbitrary thread. These facts imply that your callback, and all the data objects it uses, must be in nonpaged memory. Furthermore, you cannot block the calling thread. If you create subsidiary IRPs to communicate with your hardware, you must use asynchronous IRPs. Finally, any driver to which you send an IRP directly from this callback must be able to tolerate receiving the IRP at DISPATCH_LEVEL. As a point of information, the standard SERIAL.SYS driver allows IRPs at DISPATCH_LEVEL, and the USB bus driver allows you to send USB request blocks (URBs) at DISPATCH_LEVEL as well.
Most of the control operations use METHOD_NEITHER, which means that the input and output data buffers are found in the stack Parameters.DeviceIoControl.Type3InputBuffer and the IRP UserBuffer fields, respectively.
The control operations are heavily oriented toward the USB specification for HID devices. If you re writing a HIDCLASS minidriver, it s probably because you have either a nonstandard USB device or some other type of device altogether. You ll therefore have to fit your device into the USB model. For example, you ll have to come up with dummy vendor and product identifier values, dummy string descriptors, and so on.
The IRPs you receive in this callback have sufficient IO_STACK_LOCATION slots for you to pass the IRP down the PnP stack to your own bus driver. This fact allows you to piggyback on the control IRP to carry out a device-specific function that requires an IRP.
Here s a skeleton for coding this callback function in a minidriver:
#pragma LOCKEDCODE NTSTATUS DispatchInternalControl(PDEVICE_OBJECT fdo, PIRP Irp) { PDEVICE_EXTENSION pdx = PDX(fdo); NTSTATUS status = STATUS_SUCCESS; ULONG info = 0; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength; ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength; ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; PVOID buffer = Irp->UserBuffer; switch (code) { case IOCTL_HID_XXX: break; default: status = STATUS_NOT_SUPPORTED; break; } if (status != STATUS_PENDING) CompleteRequest(Irp, status, info); return status; }
Internal IOCTL Interface
The major interface between HIDCLASS and a minidriver is through the DispatchInternalControl callback summarized at the end of the preceding section on callback routines. In this section, I ll describe how to perform each of the control operations, in the order in which HIDCLASS normally presents them. Note that HIDCLASS doesn t invoke this callback at all until after the minidriver successfully completes an IRP_MN_START_DEVICE request.
IOCTL_HID_GET_DEVICE_ATTRIBUTES
HIDCLASS sends an IOCTL_HID_GET_DEVICE_ATTRIBUTES request as part of its processing of the IRP_MN_START_DEVICE request and conceivably, at other times, to obtain information that a USB device records in its device descriptor. The UserBuffer field of the IRP points to an instance of the following structure, which you should complete:
typedef struct _HID_DEVICE_ATTRIBUTES { ULONG Size; USHORT VendorID; USHORT ProductID; USHORT VersionNumber; USHORT Reserved[11]; } HID_DEVICE_ATTRIBUTES, * PHID_DEVICE_ATTRIBUTES;
For example, you can complete the structure in the context of the skeletal DispatchInternalControl routine shown earlier:
case IOCTL_HID_GET_DEVICE_ATTRIBUTES: { if (cbout < sizeof(HID_DEVICE_ATTRIBUTES)) { status = STATUS_BUFFER_TOO_SMALL; break; } #define p ((PHID_DEVICE_ATTRIBUTES) buffer) RtlZeroMemory(p, sizeof(HID_DEVICE_ATTRIBUTES)); p->Size = sizeof(HID_DEVICE_ATTRIBUTES); p->VendorID = 0; p->ProductID = 0; p->VersionNumber = 1; #undef p info = sizeof(HID_DEVICE_ATTRIBUTES); break; }
If your device is simply a nonstandard USB device, it s obvious which values you should supply for the VendorID, ProductID, and VersionNumber fields of this structure: the idVendor, idProduct, and bcdDevice fields from the real device descriptor. If your device isn t a USB device, you have to come up with dummy values. I used 0, 0, and 1, respectively, in this code fragment, and those choices will suffice for every type of HID device except a joystick. For a joystick device, you need to pick unique values that match what you specify in the OEM registry subkey you create for the joystick. I have no advice about how to pick those values.
Opening a HID Collection in User Mode
Opening a HID collection handle in user mode is simplicity itself if you assign unique values to the VendorID and ProductID fields of the HID_DEVICE_ATTRIBUTES structure. Suppose, for example, that your company owns USB vendor ID 0x1234 and that you have assigned product ID 0x5678 to your device. You ll use those values when answering the IOCTL_HID_GET_DEVICE_ATTRIBUTES request.An MFC application that uses the CDeviceList class mentioned in Chapter 2 can open a handle to one of the collections exposed by your driver with code like the following (see the TEST program accompanying the HIDFAKE sample driver):
HANDLE CtestDlg::FindFakeDevice() { GUID hidguid; HidD_GetHidGuid(&hidguid); CDeviceList devlist(hidguid); int ndevices = devlist.Initialize(); for (int i = 0; i < ndevices; ++i) { HANDLE h = CreateFile(devlist.m_list[i].m_linkname, 0, FILE_SHARE_READ FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (h == INVALID_HANDLE_VALUE) continue; HIDD_ATTRIBUTES attr = {sizeof(HIDD_ATTRIBUTES)}; BOOLEAN okay = HidD_GetAttributes(h, &attr); CloseHandle(h); if (!okay) continue; if (attr.VendorID != HIDFAKE_VID attr.ProductID != HIDFAKE_PID) continue; return CreateFile(devlist.m_list[i].m_linkname, GENERIC_READ GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); } return INVALID_HANDLE_VALUE; }
Use HidD_GetHidGuid to determine the interface globally unique identifier (GUID) for HID devices.
We enumerate all HID devices. In practice, the enumeration doesn t include standard devices such as mice and keyboards.
Opening a handle this way (with no access rights and allowing full sharing) allows us to issue queries. Unlike most device drivers, HIDCLASS pays close attention to the access rights and sharing attributes specified with IRP_MJ_CREATE, and we re taking advantage of the flexibility that that behavior creates by opening a handle to a device that might not actually be accessible with a normal open.
HidD_GetAttributes returns an attribute structure derived from the HID_DEVICE_ATTRIBUTES filled in by the minidriver.
This is the crucial statement in this example. If the device and product ID match what we re looking for, we ll quit scanning devices and open a real handle to this one.
This call to CreateFile opens an exclusive, nonoverlapped handle for reading and writing. This is the right action for HIDFAKE s test applet to perform. You may have different requirements for sharing, access rights, and overlapped I/O. Note that the call to CreateFile might fail, even if the earlier one succeeded, if another application has snuck in to open a handle.
Your application logic can get more complicated if your device has more than one top-level collection or if you need to provide for more than one instance of your hardware.
An entirely different approach to reading input from HID devices is available through the WM_INPUT window message and related user-mode APIs. This facility is new with Windows XP, and I didn t try it out. Maybe for the third edition .
It isn t possible to open a handle to a mouse or keyboard collection because the system input thread has opened these devices exclusively. Furthermore, these devices don t appear in the enumeration of the HID interface GUID. (HIDCLASS doesn t advertise the HID interface GUID for keyboard and mice so as to prevent some random user-mode program from opening those devices before the system s own raw input thread can do so.) It does you no good to register a private interface GUID for your device because HIDCLASS will fail an IRP_MJ_CREATE directed to the main device object. Consequently, there is no way to communicate with a custom mouse or keyboard driver using standard methods.
IOCTL_HID_GET_DEVICE_DESCRIPTOR
HIDCLASS sends an IOCTL_HID_GET_DEVICE_DESCRIPTOR request as part of its processing of the IRP_MN_START_DEVICE request and conceivably, at other times, in order to obtain a description of the device s HID characteristics. The UserBuffer field of the IRP points to an instance of a USB-standard HID descriptor structure, which you should complete:
typedef struct _HID_DESCRIPTOR { UCHAR bLength; UCHAR bDescriptorType; USHORT bcdHID; UCHAR bCountry; UCHAR bNumDescriptors; struct _HID_DESCRIPTOR_DESC_LIST { UCHAR bReportType; USHORT wReportLength; } DescriptorList [1]; } HID_DESCRIPTOR, * PHID_DESCRIPTOR;
Notwithstanding the apparent generality of this structure, HIDCLASS currently reserves sufficient space for only one element in the DescriptorList array, and it has to be the report descriptor. The Microsoft developers recommend that you nevertheless inspect the size of the output buffer and arrange your code to copy any additional descriptors such as a physical descriptor that you might have.
Your job in the minidriver is to fill in the descriptor structure as if you were a USB-standard HID device. For example:
case IOCTL_HID_GET_DEVICE_DESCRIPTOR: { #define p ((PHID_DESCRIPTOR) buffer) if (cbout < sizeof(HID_DESCRIPTOR)) { status = STATUS_BUFFER_TOO_SMALL; break; } RtlZeroMemory(p, sizeof(HID_DESCRIPTOR)); p->bLength = sizeof(HID_DESCRIPTOR); p->bDescriptorType = HID_HID_DESCRIPTOR_TYPE; p->bcdHID = HID_REVISION; p->bCountry = 0; p->bNumDescriptors = 1; p->DescriptorList[0].bReportType = HID_REPORT_DESCRIPTOR_TYPE; p->DescriptorList[0].wReportLength = sizeof(ReportDescriptor); #undef p info = sizeof(HID_DESCRIPTOR); break; }
The only aspect of this code that isn t going to be the same from one driver to the next is the length you specify for the wReportLength member of the single DescriptorList entry you provide. This value should be the length of whatever real or dummy report descriptor you ll deliver in response to the IOCTL_HID_GET_REPORT_DESCRIPTOR request.
NOTE
The bCountry code in the HID descriptor is the language, if any, to which the device is localized. According to section 6.2.1 of the HID specification, this value is entirely optional. If you were imitating a keyboard with localized keycaps, for example, you might specify a nonzero value for this field.
IOCTL_HID_GET_REPORT_DESCRIPTOR
HIDCLASS sends an IOCTL_HID_GET_REPORT_DESCRIPTOR request as part of its processing of the IRP_MN_START_DEVICE request and conceivably, at other times, in order to obtain a USB-standard HID report descriptor. The UserBuffer field of the IRP points to a buffer as large as you indicated would be necessary in your reply to an IOCTL_HID_GET_DEVICE_DESCRIPTOR request.
Suppose you have a static data area named ReportDescriptor that contains a report descriptor in standard format. You could handle this request this way:
case IOCTL_HID_GET_REPORT_DESCRIPTOR: { if (cbout < sizeof(ReportDescriptor)) { status = STATUS_BUFFER_TOO_SMALL; break; } RtlCopyMemory(buffer, ReportDescriptor, sizeof(ReportDescriptor)); info = sizeof(ReportDescriptor); break; }
Your first step in building the report descriptor is to design the report layout. The USB specification for HID devices makes it seem that you re pretty much free to design any sort of report you want, with the implication that Windows will somehow just figure out what to do with the resulting data. In my experience, however, you really don t have that much freedom. The Windows components that process keyboard and mouse input are used to receiving reports that meet certain expectations. Applications, such as games, differ greatly in their ability to decode joystick reports. I ve also found that the HIDPARSE driver, which HIDCLASS uses to parse a HID descriptor, is rather fussy about which apparently conforming descriptors it will actually accept. Consequently, my advice is to closely mimic an existing Microsoft device when designing reports for common devices.
One of the options when you save your work in the HID Tool is to create a C-language header file, like this one (corresponding to the descriptor shown in Figure 13-3):
char ReportDescriptor[64] = { 0x05, 0x05, // USAGE_PAGE (Gaming Controls) 0x09, 0x03, // USAGE (Gun Device ) 0xa1, 0x01, // COLLECTION (Application) 0xa1, 0x02, // COLLECTION (Logical) 0x85, 0x01, // REPORT_ID (1) 0x05, 0x09, // USAGE_PAGE (Button) 0x09, 0x01, // USAGE (Button 1) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x75, 0x01, // REPORT_SIZE (1) 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x75, 0x07, // REPORT_SIZE (7) 0x81, 0x03, // INPUT (Cnst,Var,Abs) 0xc0, // END_COLLECTION 0xa1, 0x02, // COLLECTION (Logical) 0x85, 0x02, // REPORT_ID (2) 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x30, // USAGE (X) 0x25, 0xff, // LOGICAL_MAXIMUM (-1) 0x75, 0x20, // REPORT_SIZE (32) 0xb1, 0x02, // FEATURE (Data,Var,Abs) 0xc0, // END_COLLECTION 0xa1, 0x02, // COLLECTION (Logical) 0x85, 0x03, // REPORT_ID (3) 0x05, 0x09, // USAGE_PAGE (Button) 0x09, 0x01, // USAGE (Button 1) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x75, 0x01, // REPORT_SIZE (1) 0xb1, 0x02, // FEATURE (Data,Var,Abs) 0x75, 0x07, // REPORT_SIZE (7) 0xb1, 0x03, // FEATURE (Cnst,Var,Abs) 0xc0, // END_COLLECTION 0xc0 // END_COLLECTION };
You can simply include this header file in your driver to define the ReportDescriptor you return from IOCTL_HID_GET_REPORT_DESCRIPTOR.
IOCTL_HID_READ_REPORT
IOCTL_HID_READ_REPORT is the workhorse operation of a HIDCLASS mini driver. HIDCLASS issues this request to obtain a raw HID report. HIDCLASS uses the raw reports to satisfy IRP_MJ_READ and IOCTL_HID_GET_INPUT_REPORT requests issued to it from higher-level components, including user-mode applications that call ReadFile, HidD_GetInputReport, IDirectInputDevice8::GetDeviceData, or IDirectInputDevice8::Poll.
A minidriver can employ any of several strategies to provide reports:
If your device is a programmed I/O (PIO) type of device attached to a traditional bus such as Peripheral Component Interconnect (PCI), you can perhaps perform hardware abstraction layer (HAL) function calls to derive data for a structured report. You ll then immediately complete the IOCTL_HID_READ_REPORT request.
If your device attaches to a traditional bus and uses a hardware interrupt to notify the host when report data is available, you need to implement a scheme for satisfying requests with reports when they become available. Using an interlocked list lets you read and save report data in an interrupt service routine (ISR). Other schemes would require your ISR to queue a deferred procedure call (DPC), which would then read and save report data.
If your device is a nonstandard USB device, you can perhaps submit a single URB to derive data from which you can compose a structured report. You can piggyback the URB on the IOCTL_HID_READ_REPORT request if your device s raw report is no bigger than the report HIDCLASS is expecting. In this case, your dispatch routine will presumably allocate memory for the URB from nonpaged memory, install a completion routine, and forward the IRP down the PnP stack to the USB bus driver. Your completion routine will free the URB, reformat the report data and set IoStatus.Information equal to the size of the reformatted report, and return STATUS_SUCCESS to allow the IRP to complete.
In still other situations, you may need to pend the IOCTL_HID_READ_REPORT request while you perform one or more I/O operations to fetch raw data from your device, which you then reformat into the desired report packet. With this design, you have the usual issues associated with caching a pointer to the IOCTL_HID_READ_REPORT request in a cancel-safe way and with cancelling whatever subsidiary IRPs you create.
No matter what scheme you devise, your driver will implement this IRP by filling the UserBuffer buffer with a report. For example:
case IOCTL_HID_READ_REPORT: { if (cbout < <size of report>) { status = STATUS_BUFFER_TOO_SMALL; break; } <obtain report data> RtlCopyMemory(buffer, <report>, <size of report>); info = <size of report>; break; }
Bear in mind that if your report descriptor includes more than one report, the report data you return to HIDCLASS begins with a 1-byte report identifier.
IOCTL_HID_WRITE_REPORT
HIDCLASS issues the IOCTL_HID_WRITE_REPORT request to service IRP_MJ_WRITE and IOCTL_HID_SET_OUTPUT_REPORT requests issued from a higher-level component, such as a user-mode application that calls WriteFile, HidD_SetOutputReport, or IDirectInputDevice8::SendDeviceData.
Output reports are commonly used to set indicators of various kinds, such as LEDs and panel displays. Your job in a minidriver is to transmit the output report data to the device or to simulate the operation of a HID device receiving such a report by some means. USB devices implement the class-specific control-pipe command Set_Report_Request (or else they define an interrupt-out endpoint) for receiving output reports, but your architecture may call for a different approach.
Unlike other HIDCLASS internal control operations, IOCTL_HID_WRITE_REPORT uses METHOD_BUFFERED. This means that the AssociatedIrp.SystemBuffer field of the IRP contains the address of the output data and that the Parameters.DeviceIoControl.OutputBufferLength field of the IO_STACK_LOCATION contains its length.
IOCTL_HID_GET_FEATURE and IOCTL_HID_SET_FEATURE
HIDCLASS issues the IOCTL_HID_GET_FEATURE and IOCTL_HID_SET_FEA TURE requests in order to read or write a so-called feature report. An application might trigger these requests by calling HidD_GetFeature or HidD_SetFeature, respectively.
You can embed feature reports within a report descriptor. According to the HID specification, feature reports are useful for getting and setting configuration information rather than for polling the device on a regular basis. With a USB-standard device, the driver uses Get_Report_Request and Set_Report_Request class-specific commands to implement this functionality. In the minidriver for some other kind of HID device, you need to provide some sort of analogue if your report descriptor includes feature reports.
These I/O control (IOCTL) operations are also the way Microsoft would prefer you perform out-of-band communication between an application and a HID minidriver. Bear in mind that HIDCLASS doesn t allow anyone to open a handle to the device itself (handles may be opened only to top-level collections) and fails any nonstandard control operations that it happens to receive. Without resorting to sleazy methods, as to which I won t say anything, there is actually no other way for an application and a HIDCLASS minidriver to communicate.
The output buffer for this request is an instance of the following structure:
typedef struct _HID_XFER_PACKET { PUCHAR reportBuffer; ULONG reportBufferLen; UCHAR reportId; } HID_XFER_PACKET, *PHID_XFER_PACKET;
HIDCLASS uses the same structure for both GET_FEATURE and SET_FEATURE requests, and it sets Irp->UserBuffer to point to it in both cases too. In fact, the only difference between the two requests is that the length of the structure (a constant) is in the InputBufferLength parameter for SET_FEATURE and in the OutputBufferLength parameter for GET_FEATURE. (You won t even care about this difference. Since HIDCLASS is a trusted kernel-mode caller, there s no particular reason to validate the length of this parameter structure.)
Your job when handling one of these requests is to decode the reportId value, which designates one of the feature reports your driver supports. For a GET_FEATURE request, you should place up to reportBufferLen bytes of data in the reportBuffer buffer and complete the IRP with IoStatus.Information set to the number of bytes you copy. For a SET_FEATURE request, you should extract reportBufferLen bytes of data from the reportBuffer buffer.
Here s a skeleton for handling these two requests:
case IOCTL_HID_GET_FEATURE: { #define p ((PHID_XFER_PACKET) buffer) switch (p->reportId) { case FEATURE_CODE_XX: if (p->reportBufferLen < sizeof(FEATURE_REPORT_XX)) { status = STATUS_BUFFER_TOO_SMALL; break; } RtlCopyMemory(p->reportBuffer, FeatureReportXx, sizeof(FEATURE_REPORT_XX)); info = sizeof(FEATURE_REPORT_XX); break; } break; #undef p } case IOCTL_HID_SET_FEATURE: { #define p ((PHID_XFER_PACKET) buffer) switch (p->reportId) { case FEATURE_CODE_YY: if (p->reportBufferLen > sizeof(FEATURE_REPORT_YY)) { status = STATUS_INVALID_PARAMETER; break; } RtlCopyMemory(FeatureReportYy, p->reportBuffer, p->reportBufferLen); break; } break; #undef p }
CAUTION
When your driver supports feature reports, you ll normally be using report identification bytes to identify the different feature reports and your input and output reports. In that case, the reportBuffer buffer begins with a single-byte identifier, which will equal the reportId value in the HID_XFER_PACKET structure HIDCLASS makes that so. The count in reportBufferLen includes the identifier byte. When you don t use report identifiers, however, reportId will be 0, reportBuffer won t have room for an identifier byte, and the reportBufferLen count won t include an identifier byte. This arrangement is true even though the caller of HidD_GetFeature or HidD_SetFeature supplies a buffer that does include a zero identification byte. To put it another way, you copy the actual feature report data beginning at reportBuffer + 1 if you re using report identifiers but beginning at reportBuffer if you re not using report identifiers.
In these fragments, FEATURE_CODE_XX and FEATURE_CODE_YY are placeholders for manifest constants that you would define to correspond to feature report identifiers in your device s scheme. FEATURE_REPORT_XX and FEATURE_REPORT_YY are structures that include an identifier byte and the actual report data, and FeatureReportXx and FeatureReportYy are instances of those structures.
IOCTL_GET_PHYSICAL_DESCRIPTOR
HIDCLASS sends an IOCTL_GET_PHYSICAL_DESCRIPTOR when a higher-level component requests the physical descriptor for a device. Physical descriptors provide information about which part or parts of the body activate one or more controls on a device. If you have a nonstandard HID device for which this request is relevant, you ll need to support the request by returning a dummy descriptor meeting the HID specification, Section 6.2.3. For example:
case IOCTL_GET_PHYSICAL_DESCRIPTOR: { if (cbout < sizeof(PhysicalDescriptor)) { status = STATUS_BUFFER_TOO_SMALL; break; } PUCHAR p = (PUCHAR) MmGetSystemAddressForMdlSafe(Irp->MdlAddress); if (!p) { status = STATUS_INSUFFICIENT_RESOURCES; break; } RtlCopyMemory(p, PhysicalDescriptor, sizeof(PhysicalDescriptor)); info = sizeof(PhysicalDescriptor); break; }
Note that this IOCTL uses METHOD_OUT_DIRECT instead of METHOD_NEITHER.
In addition, bear in mind the following statement in the HID specification: Physical descriptors are entirely optional. They add complexity and offer very little in return for most devices.
IOCTL_HID_GET_STRING
HIDCLASS sends an IOCTL_HID_GET_STRING to retrieve a string describing the manufacturer, product, or serial number of a device. A user-mode application can trigger this IOCTL by calling HidD_GetManufacturerString, HidD_Get ProductString, or HidD_GetSerialNumberString. The strings correspond to optional strings specified by the device descriptor of a standard USB device. A parameter to the operation indicates which string you should return, and in which language.
A skeleton for handling this control operation is as follows:
case IOCTL_HID_GET_STRING: { #define p ((PUSHORT) \ stack->Parameters.DeviceIoControl.Type3InputBuffer) USHORT istring = p[0]; LANGID langid = p[1]; #undef p PWCHAR string = NULL; switch (istring) { case HID_STRING_ID_IMANUFACTURER: string = <manufacturer name>; break; case HID_STRING_ID_IPRODUCT: string = <product name>; break; case HID_STRING_ID_ISERIALNUMBER: string = <serial number>; break; } if (!string) { status = STATUS_INVALID_PARAMETER; break; } ULONG lstring = wcslen(string); if (cbout < lstring * sizeof(WCHAR)) { status = STATUS_INVALID_BUFFER_SIZE; break; } RtlCopyMemory(buffer, string, lstring * sizeof(WCHAR)); info = lstring * sizeof(WCHAR); if (cbout >= info + sizeof(WCHAR)) { ((PWCHAR) buffer)[lstring] = UNICODE_NULL; info += sizeof(WCHAR); } break; }
Some of the key points about this code fragment are these:
Like most other minidriver IOCTL requests, this one uses METHOD_NEITHER. In the context of the DispatchInternalControl callback presented earlier, buffer is the output buffer to be filled.
The serial number, if there is one, should be unique for each device.
The minidriver should fail the request with STATUS_INVALID_PARAM ETER if an invalid string index appears and STATUS_INVALID_BUF FER_SIZE if a buffer is supplied but is too small to hold the entire string.
The minidriver returns the whole string or nothing. It appends a null terminator to the string if the output buffer is big enough.
The DDK doesn t specify what to do if the requested language isn t one of the ones your device or minidriver happens to support. I would suggest failing the request with STATUS_DEVICE_DATA_ERROR to mimic what a real USB device is supposed to do. If, however, the unsupported language is 0x0409 (American English), I recommend returning a default string of some kind perhaps even the first language in your list of supported languages because HIDCLASS always uses 0x0409 for the language id parameter in Windows XP and earlier versions of the system.
IOCTL_HID_GET_INDEXED_STRING
HIDCLASS sends an IOCTL_HID_GET_INDEXED_STRING to retrieve a string whose USB-standard index and language identifiers are specified. A user-mode program can trigger this IOCTL by calling HidD_GetIndexedString. You handle this request much like IOCTL_HID_GET_STRING except for these two points:
This control operation uses a curious mix of two buffering methods: the input data containing the string index and the language id is in stack->Parameters.DeviceIoControl.Type3InputBuffer (as would be true of a METHOD_NEITHER request), and the output buffer is described by the memory descriptor list (MDL) at Irp->MdlAddress, as would be true of a METHOD_OUT_DIRECT request.
The string index in the low-order 16 bits of the Type3InputBuffer is a USB-standard string index instead of a constant such as HID_STRING_ID_IMANUFACTURER.
The purpose of this request is to allow applications to retrieve string values corresponding to string usages in a HID report. USB devices may make up to 255 string values accessible in this way. With a nonstandard USB device or a non-USB device, your minidriver needs to provide an analogue if the report descriptor contains string usages.
IOCTL_HID_SEND_IDLE_NOTIFICATION_REQUEST
HIDCLASS sends an IOCTL_HID_SEND_IDLE_NOTIFICATION_REQUEST to power down an idle device. With a real USB device, this request dovetails with the USB selective suspend feature discussed in the preceding chapter.
The input buffer for this METHOD_NEITHER request is an instance of the following structure:
typedef struct _HID_SUBMIT_IDLE_NOTIFICATION_CALLBACK_INFO { HID_SEND_IDLE_CALLBACK IdleCallback; PVOID IdleContext; } HID_SUBMIT_IDLE_NOTIFICATION_CALLBACK_INFO, *PHID_SUBMIT_IDLE_NOTIFICATION_CALLBACK_INFO;
where HID_SEND_IDLE_CALLBACK is declared as follows:
typedef void (*HID_IDLE_CALLBACK)(PVOID Context);
Note that this structure is identical in layout and meaning to the one used with USB selective suspend. In fact, if your device happened to be a USB device, you could just forward the IRP down the stack after changing the function code:
case IOCTL_HID_SEND_IDLE_NOTIFICATION_REQUEST: { IoCopyCurrentIrpStackLocationToNext(Irp); stack = IoGetNextIrpStackLocation(Irp); stack->Parameters.DeviceIoControl.IoControlCode = IOCTL_INTERNAL_USB_SUBMIT_IDLE_NOTIFICATION; return IoCallDriver(pdx->LowerDeviceObject, Irp); }
If your device is not a USB device, however, you should call back right away to HIDCLASS and complete the IRP, as shown here:
case IOCTL_HID_SEND_IDLE_NOTIFICATION_REQUEST: { PHID_SUBMIT_IDLE_NOTIFICATION_CALLBACK_INFO p = (PHID_SUBMIT_IDLE_NOTIFICATION_CALLBACK_INFO) stack->Parameters.DeviceIoControl.Type3InputBuffer; (*p->IdleCallback)(p->IdleContext); break; }
Calling back tells HIDCLASS that it can immediately power the device down.