Up to this point I've talked about the important concepts you need to know to write a hardware device driver. To close the chapter, I'll discuss two less important minor function codes—IRP_MN_FILTER_RESOURCE_REQUIREMENTS and IRP_MN_DEVICE_USAGE_NOTIFICATION—that you might need to process in a practical driver. Then I'll discuss how to write a miniature bus driver to support nonstandard controller or multifunction devices. Finally, I'll mention how you can register to receive notifications about PnP events that affect other devices besides your own.
NOTE
Other flavors of PnP requests exist that I haven't discussed in this chapter because it's not my purpose to simply reiterate the DDK reference manuals. For example, it's potentially useful to be able to export a direct call interface to other drivers, but you probably don't need to in any garden-variety situation. I'm therefore not going to provide a sample or an explanation of IRP_MN_QUERY_INTERFACE. I'll mention IRP_MN_QUERY_CAPABILITIES in Chapter 8, on power management, to which it's most relevant.
Sometimes the PnP Manager is misinformed about the resource requirements of your driver. This can occur because of hardware and firmware bugs, mistakes in the INF file for a legacy device, or other reasons. The system provides an escape valve in the form of the IRP_MN_FILTER_RESOURCE_REQUIREMENTS request, which affords you a chance to examine and possibly alter the list of resources before the PnP Manager embarks on the arbitration and assignment process that culminates in your receiving a start device IRP.
When you receive a filter request, the FilterResourceRequirements substructure of the Parameters union in your stack location points to an IO_RESOURCE_REQUIREMENTS_LIST data structure that lists the resource requirements for your device. In addition, if any of the drivers above you have processed the IRP and modified the resource requirements, the IoStatus.Information field of the IRP will point to a second IO_RESOURCE_REQUIREMENTS_LIST, which is the one from which you should work. Your overall strategy will be as follows: If you wish to add a resource to the current list of requirements, you do so in your dispatch routine. Then you pass the IRP down the stack synchronously—that is, by using the ForwardAndWait method you use with a start device request. When you regain control, you can modify any of the resource descriptions that appear in the list.
Here is a brief and not very useful example that illustrates the mechanics of the filtering process:
1 2 3 4 5 6 7 8 9 |
NTSTATUS HandleFilterResources(PDEVICE_OBJECT fdo, PIRP Irp) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); PIO_RESOURCE_REQUIREMENTS_LIST original = stack->Parameters .FilterResourceRequirements.IoResourceRequirementList; PIO_RESOURCE_REQUIREMENTS_LIST filtered = (PIO_RESOURCE_REQUIREMENTS_LIST) Irp->IoStatus.Information; PIO_RESOURCE_REQUIREMENTS_LIST source = filtered ? filtered : original; if (source->AlternativeLists != 1) return DefaultPnpHandler(fdo, Irp); ULONG sizelist = source->ListSize; PIO_RESOURCE_REQUIREMENTS_LIST newlist = (PIO_RESOURCE_REQUIREMENTS_LIST) ExAllocatePool(PagedPool, sizelist + sizeof(IO_RESOURCE_DESCRIPTOR)); if (!newlist) return DefaultPnpHandler(fdo, Irp); RtlCopyMemory(newlist, source, sizelist); newlist->ListSize += sizeof(IO_RESOURCE_DESCRIPTOR); PIO_RESOURCE_DESCRIPTOR resource = &newlist->List[0].Descriptors[newlist->List[0].Count++]; RtlZeroMemory(resource, sizeof(IO_RESOURCE_DESCRIPTOR)); resource->Type = CmResourceTypeDevicePrivate; resource->ShareDisposition = CmResourceShareDeviceExclusive; resource->u.DevicePrivate.Data[0] = 42; Irp->IoStatus.Information = (ULONG_PTR) newlist; if (filtered) ExFreePool(filtered); NTSTATUS status = ForwardAndWait(fdo, Irp); if (NT_SUCCESS(status)) { // stuff } Irp->IoStatus.Status = status; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; } |
Disk drivers (and the drivers for disk controllers) in particular sometimes need to know extrinsic facts about how they're being used by the operating system, and the IRP_MN_DEVICE_USAGE_NOTIFICATION request provides a means to gain that knowledge. The I/O stack location for the IRP contains two parameters in the Parameters.UsageNotification substructure. See Table 6-4. The InPath value (a Boolean) indicates whether the device is in the device path required to support that usage, and the Type value indicates one of several possible special usages.
Table 6-4. Fields in the Parameters.UsageNotification substructure of an I/O stack location.
Parameter | Description |
---|---|
InPath | TRUE if device is in the path of the Type usage; FALSE if not |
Type | Type of usage to which the IRP applies |
In the subdispatch routine for the notification, you should have a switch statement (or other logic) that differentiates among the notifications you know about. In most cases you'll pass the IRP down the stack. Consequently, a skeleton for the subdispatch function is as follows:
NTSTATUS HandleUsageNotification(PDEVICE_OBJECT fdo, PIRP Irp) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); DEVICE_USAGE_NOTIFICATION_TYPE type = stack->Parameters.UsageNotification.Type; BOOLEAN inpath = stack->Parameters.UsageNotification.InPath; switch (type) { case DeviceUsageTypeHibernation: ... Irp->IoStatus.Status = STATUS_SUCCESS; break; case DeviceUsageTypeDumpFile: ... Irp->IoStatus.Status = STATUS_SUCCESS; break; case DeviceUsageTypePaging: ... Irp->IoStatus.Status = STATUS_SUCCESS; break; default: break; } return DefaultPnpHandler(fdo, Irp); } |
Set the Status field of the IRP to STATUS_SUCCESS for only the notifications that you explicitly recognize as a signal to the bus driver that you've processed them. The bus driver will assume that you didn't know about—and therefore didn't process—a notification for which you don't set STATUS_SUCCESS.
You may know that your device can't support a certain kind of usage. Suppose, for example, that some fact that only you know prevents your disk device from being used to store a hibernation file. In such a case, you should fail the IRP if it specifies the InPath value:
... case DeviceUsageTypeHibernation: if (inpath) return CompleteRequest(Irp, STATUS_UNSUCCESSFUL, 0); |
In the remainder of this section, I'll briefly describe each of the current usage types.
The InPath TRUE notification indicates that a paging file will be opened on the device. The InPath FALSE notification indicates that a paging file has been closed. Generally, you should maintain a counter of the number of paging files you've been notified about. While any paging file remains active, you'll fail queries for STOP and REMOVE functions. In addition, when you receive the first paging notification, make sure that your dispatch routines for READ, WRITE, DEVICE_CONTROL, PNP, and POWER requests are locked into memory. (Refer to the information on driver paging in "User and Kernel Mode Address Spaces" in Chapter 3, "Basic Programming Techniques," for more information.) You should also clear the DO_POWER_PAGABLE flag in your device object to force the Power Manager to send you power IRPs at DISPATCH_LEVEL. To be safe, I'd also suggest nullifying any idle-notification registration you might have made. (See Chapter 8 for a discussion of idle detection.)
NOTE
In Chapter 8, "Power Management," I'll discuss how to set the DO_POWER_PAGABLE flag in a device object. You need to be sure that you never clear this flag while a device object under yours has the flag set. You would want to clear the flag only in a completion routine, after the lower-level drivers have cleared their own flags. You need a completion routine anyway because you must undo anything you did in your dispatch routine if the IRP fails in the lower layers.
The InPath TRUE notification indicates that the device has been chosen as the repository for a crash dump file should one be necessary. The InPath FALSE notification cancels that. Maintain a counter of TRUE minus FALSE notifications. While the counter is nonzero:
The InPath TRUE notification indicates that the device has been chosen to hold the hibernation state file should one be written. The InPath FALSE notification cancels that. You should maintain a counter of TRUE minus FALSE notifications. Your response to system power IRPs that specify the PowerSystemHibernate state will be different than normal because your device will be used momentarily to record the hibernate file. Elaboration of this particular feature of disk drivers is beyond the scope of this book.
Two categories of devices don't fit neatly into the PnP framework I've described so far. These categories are controller devices, which manage a collection of child devices, and multifunction devices, which have several functions on one card. These kinds of devices are similar in that their correct management entails the creation of multiple device objects with independent I/O resources.
It's very easy in Windows 2000 to support PCI, PCMCIA (Personal Computer Memory Card International Association), and USB devices that conform to their respective bus standards for multifunction devices. The PCI bus driver automatically recognizes PCI multifunction cards. For PCMCIA multifunction devices, you can follow the detailed instructions in the DDK for designating MF.SYS as the function driver for your multifunction card; MF.SYS will enumerate the functions on your card and thereby cause the PnP Manager to load individual function drivers. The USB hub driver will normally load separate function drivers for each interface on a one-configuration device.
Except for USB, the original release of Windows 98 lacks the multifunction support that Windows 2000 provides. In Windows 98, to deal with controller or multifunction devices, or to deal with nonstandard devices, you'll need to resort to more heroic means. You'll supply a function driver for your main device and supply separate function drivers for the child devices that connect to the main device. The main device's function driver will act like a miniature bus driver by enumerating the child devices and providing default handling for PnP and power requests. Writing a full-fledged bus driver is a large undertaking, and I don't intend to attempt a description of the process here. I will, however, describe the basic mechanisms you use for enumerating child devices. This information will allow you to write drivers for controller or multifunction devices that don't fit the standard molds provided by Microsoft.
In Chapter 2, Figure 2-2 illustrates the topology of device objects when a parent device, such as bus driver, has children. Controller and multifunction devices use a similar topology. The parent device plugs into a standard bus. The driver for the standard bus detects the parent, and the PnP Manager configures it just like any ordinary device—up to a point. After it starts the parent device, the PnP Manager sends a Plug and Play request with the minor function code IRP_MN_QUERY_DEVICE_RELATIONS to learn the so-called bus relations of the parent device. This query occurs for all devices, actually, because the PnP Manager doesn't know yet whether the device has children.
In response to the bus relations query, the parent device's function driver locates or creates additional device objects. Each of these objects becomes the PDO at the bottom of the stack for one of the child devices. The PnP Manager will go on to load the function and filter drivers for the child devices, whereupon you end up with a picture like that in Figure 2-2.
The driver for the parent device has to play two roles. In one role, it's the functional device object (FDO) driver for the controller or multifunction device. In the other role, it's the PDO driver for its child devices. In its FDO role, it handles PnP and power requests in the way function drivers normally handle them. In its PDO role, however, it acts as the driver of last resort for PnP and power requests.
Somewhere along the way, perhaps at the time it processes IRP_MN_START_DEVICE, the parent driver, in its FDO role, needs to create one or more physical device objects for its children, and it needs to keep track of them for later. The only major complication at this early stage is this: both the FDO and all the PDOs belong to the same driver object, which means that IRPs directed to any of these device objects will come to one set of dispatch routines. The driver needs to handle PnP and power IRPs differently for FDOs and PDOs. Consequently, you need to provide a way for a dispatch function to easily distinguish between an FDO and one of the child PDOs. I dealt with this complication by defining two device extension structures with a common beginning, as follows:
// The FDO extension: typedef struct _DEVICE_EXTENSION { ULONG flags; ... } DEVICE_EXTENSION, *PDEVICE_EXTENSION; // The PDO extension: typedef struct _PDO_EXTENSION { ULONG flags; ... } PDO_EXTENSION, *PPDO_EXTENSION; // The common part: typedef struct _COMMON_EXTENSION { ULONG flags; } COMMON_EXTENSION, *PCOMMON_EXTENSION; #define ISPDO 0x00000001 |
The dispatch routine for IRP_MJ_PNP then looks like this:
NTSTATUS DispatchPnp(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PCOMMON_EXTENSION pcx = (PCOMMON_EXTENSION) DeviceObject->DeviceExtension; if (pcx->flags & ISPDO) return DispatchPnpPdo(DeviceObject, Irp); else return DispatchPnpFdo(DeviceObject, Irp); } |
MULFUNC, which is available on the companion disc, is a very lame multifunction device: it has just two children, and we always know what they are. I just called them A and B. MULFUNC executes the following code—with more error checking than what I'm showing you here—at IRP_MN_START_DEVICE time to create PDOs for A and B:
1 2 |
NTSTATUS StartDevice(PDEVICE_OBJECT fdo, ...) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; CreateChild(pdx, CHILDTYPEA, &pdx->ChildA); CreateChild(pdx, CHILDTYPEB, &pdx->ChildB); return STATUS_SUCCESS; } NTSTATUS CreateChild(PDEVICE_EXTENSION pdx, ULONG flags, PDEVICE_OBJECT* ppdo) { PDEVICE_OBJECT child; IoCreateDevice(pdx->DriverObject, sizeof(PDO_EXTENSION), NULL, FILE_DEVICE_UNKNOWN, FILE_AUTOGENERATED_DEVICE_NAME, FALSE, &child); PPDO_EXTENSION px = (PPDO_EXTENSION) child->DeviceExtension; px->flags = ISPDO | flags; px->DeviceObject = child; px->Fdo = pdx->DeviceObject; child->Flags &= ~DO_DEVICE_INITIALIZING; *ppdo = child; return STATUS_SUCCESS; } |
The end result of the creation process is two pointers to device objects (ChildA and ChildB) in the device extension for the parent device's FDO.
The PnP Manager inquires about the children of every device by sending an IRP_MN_QUERY_DEVICE_RELATIONS request with a type code of BusRelations. Wearing its FDO hat, the parent driver responds to this request with code like the following:
1 2 3 4 |
NTSTATUS HandleQueryRelations(PDEVICE_OBJECT fdo, PIRP Irp) { PDEVICE_EXTENSION pdx = ...; PIO_STACK_LOCATION stack = ...; if (stack->Parameters.QueryDeviceRelations.Type != BusRelations) return DefaultPnpHandler(fdo, Irp); PDEVICE_RELATIONS newrel = (PDEVICE_RELATIONS) ExAllocatePool(PagedPool, sizeof(DEVICE_RELATIONS) + sizeof(PDEVICE_OBJECT)); newrel->Count = 2; newrel->Objects[0] = pdx->ChildA; newrel->Objects[1] = pdx->ChildB; ObReferenceObject(pdx->ChildA); ObReferenceObject(pdx->ChildB); Irp->IoStatus.Information = (ULONG_PTR) newrel; Irp->IoStatus.Status = STATUS_SUCCESS; return DefaultPnpHandler(fdo, Irp); } |
I glossed over an additional complication in the preceding code fragment that you'll notice in the code sample. An upper filter might have already installed a list of device objects in the IoStatus.Information field of the IRP. We must not lose that list. Rather, we must extend it by adding our own two device object pointers.
The PnP Manager automatically sends a query for bus relations at start time. You can force the query to be sent by calling this service function:
IoInvalidateDeviceRelations(pdx->Pdo, BusRelations); |
You would make this call if you detected the arrival or departure of one of your child devices, for example.
Wearing its PDO driver hat, the parent driver must handle Plug and Play IRPs in a way that's very different from how a function driver would handle them. Table 6-5 summarizes the requirements using a shorthand to describe the actions to be taken.
Table 6-5. PDO driver handling of PnP requests.
PnP Request | How Handled |
---|---|
IRP_MN_START_DEVICE | Succeed |
IRP_MN_QUERY_REMOVE_DEVICE | Succeed |
IRP_MN_REMOVE_DEVICE | Succeed |
IRP_MN_CANCEL_REMOVE_DEVICE | Succeed |
IRP_MN_STOP_DEVICE | Succeed |
IRP_MN_QUERY_STOP_DEVICE | Succeed |
IRP_MN_CANCEL_STOP_DEVICE | Succeed |
IRP_MN_QUERY_DEVICE_RELATIONS | Special processing |
IRP_MN_QUERY_INTERFACE | Ignore |
IRP_MN_QUERY_CAPABILITIES | Delegate |
IRP_MN_QUERY_RESOURCES | Succeed |
IRP_MN_QUERY_RESOURCE_REQUIREMENTS | Succeed |
IRP_MN_QUERY_DEVICE_TEXT | Succeed |
IRP_MN_FILTER_RESOURCE_REQUIREMENTS | Succeed |
IRP_MN_READ_CONFIG | Delegate |
IRP_MN_WRITE_CONFIG | Delegate |
IRP_MN_EJECT | Delegate |
IRP_MN_SET_LOCK | Delegate |
IRP_MN_QUERY_ID | Special processing |
IRP_MN_QUERY_PNP_DEVICE_STATE | Delegate |
IRP_MN_QUERY_BUS_INFORMATION | Delegate |
IRP_MN_DEVICE_USAGE_NOTIFICATION | Delegate |
IRP_MN_SURPRISE_REMOVAL | Succeed |
Any other | Ignore |
The parent should simply succeed many PnP IRPs without doing any particular processing:
NTSTATUS SucceedRequest(PDEVICE_OBJECT pdo, PIRP Irp) { Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } |
The only remarkable feature of this short subroutine is that it doesn't change the IoStatus.Information field of the IRP. The PnP Manager always initializes this field in some way before launching an IRP. In some cases, the field might be altered by a filter driver or the function driver to point to some data structure or another. It would be incorrect for the PDO driver to alter the field.
The parent driver can ignore certain IRPs. Ignoring an IRP is similar to failing it with an error code, except that the driver doesn't change the IRP's status fields:
NTSTATUS IgnoreRequest(PDEVICE_OBJECT pdo, PIRP Irp) { NTSTATUS status = Irp->IoStatus.Status; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; } |
A miniature bus driver such as the one I'm discussing can simply delegate some PnP requests to the real bus driver that lies underneath the parent device's FDO. Delegation in this case is not quite as simple as just calling IoCallDriver because by the time we receive an IRP as a PDO driver, the I/O stack is generally exhausted. We must therefore create what I call a repeater IRP that we can send to the driver stack we occupy as FDO driver:
1 2 3 4 5 6 7 8 9 10 11 |
NTSTATUS RepeatRequest(PDEVICE_OBJECT pdo, PIRP Irp) { PPDO_EXTENSION pdx = (PPDO_EXTENSION) pdo->DeviceExtension; PDEVICE_OBJECT fdo = pdx->Fdo; PDEVICE_EXTENSION pfx = (PDEVICE_EXTENSION) fdo->DeviceExtension; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); PDEVICE_OBJECT tdo = IoGetAttachedDeviceReference(fdo); PIRP subirp = IoAllocateIrp(tdo->StackSize + 1, FALSE); PIO_STACK_LOCATION substack = IoGetNextIrpStackLocation(subirp); substack->DeviceObject = tdo; substack->Parameters.Others.Argument1 = (PVOID) Irp; IoSetNextIrpStackLocation(subirp); substack = IoGetNextIrpStackLocation(subirp); RtlCopyMemory(substack, stack, FIELD_OFFSET(IO_STACK_LOCATION, CompletionRoutine)); substack->Control = 0; BOOLEAN needsvote = <I'll explain later>; IoSetCompletionRoutine(subirp, OnRepeaterComplete, (PVOID) needsvote, TRUE, TRUE, TRUE); subirp->IoStatus.Status = STATUS_NOT_SUPPORTED; IoMarkIrpPending(Irp); IoCallDriver(tdo, subirp); return STATUS_PENDING } NTSTATUS OnRepeaterComplete(PDEVICE_OBJECT tdo, PIRP subirp, PVOID needsvote) { ObDereferenceObject(tdo); PIO_STACK_LOCATION substack = IoGetCurrentIrpStackLocation(subirp); PIRP Irp = (PIRP) substack->Parameters.Others.Argument1; if (subirp->IoStatus.Status == STATUS_NOT_SUPPORTED) { if (needsvote) Irp->IoStatus.Status = STATUS_UNSUCCESSFUL; } else Irp->IoStatus = subirp->IoStatus; IoFreeIrp(subirp); IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_MORE_PROCESSING_REQUIRED; } |
The preceding code deals with a rather complex problem that afflicts the various PnP IRPs that MULFUNC is repeating on the parent device stack. The PnP Manager initializes PnP IRPs to contain STATUS_NOT_SUPPORTED. It can tell whether any driver actually handled one of these IRPs by examining the ending status. If the IRP completes with STATUS_NOT_SUPPORTED, the PnP Manager can deduce that no driver did anything with the IRP. If the IRP completes with any other status, the PnP Manager knows that some driver deliberately either failed or succeeded the IRP but didn't simply ignore it.
A driver like MULFUNC that creates a PnP IRP must follow the same convention by initializing IoStatus.Status to STATUS_NOT_SUPPORTED. As I remarked, the Driver Verifier will bugcheck if you forget to do this. But this initialization gives rise to the following problem: suppose one of the devices in the child stack (that is, above the PDO for the child device) changes IoStatus.Status to another value before passing a particular IRP down to us in our role as PDO driver. We will create a repeater IRP, pre-initialized with STATUS_NOT_SUPPORTED, and pass it down the parent stack (that is, the stack to which we belong in our role as FDO driver). If the repeater IRP completes with STATUS_NOT_SUPPORTED, what status should we use in completing the original IRP? It shouldn't be STATUS_NOT_SUPPORTED, because that would imply that none of the child-stack drivers processed the IRP (but one did, and changed the main IRP's status). That's where the needsvote flag comes in.
For some of the IRPs we repeat, we don't care whether a parent driver actually processes the IRP. We say (actually, the Microsoft developers say) that the parent drivers don't need to "vote" on the IRP. If you look carefully at OnRepeaterComplete, you'll see that we don't change the main IRP's ending status in this case. For other of the IRPs we repeat, we can't provide a real answer if the parent stack drivers ignore the IRP. For these IRPs, on which the parent must "vote," we fail the main IRP with STATUS_UNSUCCESSFUL. To see which IRPs belong to the "needs vote" class and which IRPs don't, take a look at RepeatRequest in the MULFUNC sample (specifically, in PlugPlayPdo.cpp).
If one of the parent drivers actually does process the repeater IRP, however, we copy the entire IoStatus field, which includes both the Status and Information values, into the main IRP. The Information field might contain the answer to a query, and this copy step is how we pass the answer upwards.
I did one other slightly subtle thing in RepeatRequest, and that is that I marked the IRP pending and returned STATUS_PENDING. Most PnP IRPs complete synchronously so that the call to IoCallDriver will most likely cause immediate completion of the IRP. So why mark the IRP pending and cause the I/O Manager unnecessary pain in the form of needing to schedule an APC as part of completing the main IRP? The reason is that if we don't return STATUS_PENDING from our dispatch function—recall that RepeatRequest is running as a subroutine below the dispatch function for IRP_MJ_PNP—we must return the exact same value that we use when we complete the IRP. Only our completion routine knows which value this will actually be after checking for STATUS_NOT_SUPPORTED and checking the needsvote flag.
The PnP Manager is aware of the parent-child relationship between a parent's FDO and its children PDOs. Consequently, when the user removes the parent device, the PnP Manager automatically removes all the children. Oddly enough, though, the parent driver should not normally delete a child PDO when it receives an IRP_MN_REMOVE_DEVICE. The PnP Manager expects PDOs to persist until the underlying hardware is gone. A multifunction driver would therefore not delete the children PDOs until it's told to delete the parent FDO. A bus driver, however, would delete a child PDO when it receives IRP_MN_REMOVE_DEVICE after failing to report the device during an enumeration.
MULFUNC deletes the children PDOs when it processes the remove device event for its own FDO.
If you're trying to provide for a controller-type device (as opposed to the nonstandard multifunction device I provided an example of), your controller driver needs some additional logic to actually enumerate devices. I've omitted that logic because my sample device's children are always present if the main device is present. And don't forget to restore power to your controller before trying to do the enumeration.
The most important of the PnP requests that a parent driver handles is IRP_MN_QUERY_ID. The PnP Manager issues this request in several forms to determine which device identifiers it will use to locate the INF file for a child device. You respond by returning (in IoStatus.Information) a MULTI_SZ value containing the requisite device identifiers. The MULFUNC device has two children with the (bogus) device identifiers *WCO0604 and *WCO0605—the fourth and fifth drivers for Chapter 6, you see. It handles the query in the following way:
1 2 3 |
NTSTATUS HandleQueryId(PDEVICE_OBJECT pdo, PIRP Irp) { PPDO_EXTENSION pdx = (PPDO_EXTENSION) pdo->DeviceExtension; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); PWCHAR idstring; switch (stack->Parameters.QueryId.IdType) { case BusQueryInstanceID: idstring = L"0000"; break; case BusQueryDeviceID: if (pdx->flags & CHILDTYPEA) idstring = LDRIVERNAME L"\\*WCO0604"; else idstring = LDRIVERNAME L"\\*WCO0605"; break; case BusQueryHardwareIDs: if (pdx->flags & CHILDTYPEA) idstring = L"*WCO0604"; else idstring = L"*WCO0605"; break; default: return CompleteRequest(Irp, STATUS_NOT_SUPPORTED, 0); } ULONG nchars = wcslen(idstring); ULONG size = (nchars + 2) * sizeof(WCHAR); PWCHAR id = (PWCHAR) ExAllocatePool(PagedPool, size); wcscpy(id, idstring); id[nchars + 1] = 0; return CompleteRequest(Irp, STATUS_SUCCESS, (ULONG_PTR) id); } |
NOTE
Be sure to use your own name in place of MULFUNC if you construct a device identifier in the manner I showed you here. To emphasize that you shouldn't just copy my sample program's name in a hard-coded constant, I wrote the code to use the manifest constant LDRIVERNAME, which is defined in the DRIVER.H file in the MULFUNC project.
The Windows 98 PnP Manager will tolerate your supplying the same string for a device identifier as you do for a hardware identifier, but the Windows 2000 PnP Manager won't. I learned the hard way to supply a made-up enumerator name in the device ID. Calling IoGetDeviceProperty to get the PDO's enumerator name leads to a bug check because the PnP Manager ends up working with a NULL string pointer. Using the parent's enumerator name—ROOT in the case of the MULFUNC sample—leads to the bizarre result that the PnP Manager brings the child devices back after you delete the parent!
The last PnP request to consider is IRP_MN_QUERY_DEVICE_RELATIONS. Recall that the FDO driver answers this request by providing a list of child PDOs for a bus relations query. Wearing its PDO hat, however, the parent driver need only answer a request for the so-called target device relation by providing the address of the PDO:
NTSTATUS HandleQueryRelations(PDEVICE_OBJECT pdo, PIRP Irp) { PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); NTSTATUS status = Irp->IoStatus.Status; if (stack->Parameters.QueryDeviceRelations.Type == TargetDeviceRelation) { PDEVICE_RELATIONS newrel = (PDEVICE_RELATIONS) ExAllocatePool(PagedPool, sizeof(DEVICE_RELATIONS)); newrel->Count = 1; newrel->Objects[0] = pdo; ObReferenceObject(pdo); status = STATUS_SUCCESS; Irp->IoStatus.Information = (ULONG_PTR) newrel; } Irp->IoStatus.Status = status; IoCompleteRequest(Irp, IO_NO_INCREMENT); return |
If your device is a controller type, the child devices that plug into it presumably claim their own I/O resources. If you have an automated way to discover the devices' resource requirements, you can return a list of them in response to an IRP_MN_QUERY_RESOURCE_REQUIREMENTS request. If there is no automated way to discover the resource requirements, the child device's INF file should have a LogConfig section to establish them.
If you're dealing with a multifunction device, chances are that the parent device claims all the I/O resources that the child functions use. If the child functions have separate WDM drivers, you have to devise a way to separate the resources by function and let each function driver know which ones belong to it. This is not simple. The PnP Manager normally tells a function driver about its resource assignments in an IRP_MN_START_DEVICE request. (See the detailed discussion in the next chapter.) There's no normal way for you to force the PnP Manager to use some of your resources instead of the ones it assigns, though. Note that responding to a requirements query or a filter request doesn't help because those requests deal with requirements that the PnP Manager will then go on to satisfy using new resources.
Microsoft's MF.SYS driver deals with resource subdivision by using some internal interfaces with the system's resource arbitrators that aren't accessible to us as third-party developers. There are two different ways of subdividing resources: one that works in Windows 2000 and another one that works in Windows 98. Since we can't do what MF.SYS does, we need to find some other way to suballocate resources owned by the parent device. I haven't actually tried to implement either of the two suggestions I'm about to float, but I'm interested in hearing from any reader who carries these ideas further.
If you can control all of the child device function drivers, your parent driver could export a direct-call interface. Child drivers would obtain a pointer to the interface descriptor by sending an IRP_MN_QUERY_INTERFACE request to the parent driver. They would call functions in the parent driver at start device and stop device time to obtain and release resources that the parent actually owns.
If you can't modify the function drivers for your child devices, I believe you could solve the resource subdivision problem by installing a tiny upper filter—see Chapter 9—above each of the child device's FDOs. The only purpose of the filter is to plug in a list of assigned resources to each IRP_MN_START_DEVICE. The filter could communicate via a direct-call interface with the parent driver.
Windows 2000 and Windows 98 provide a way to notify both user-mode and kernel-mode components of particular Plug and Play events. Windows 95 has a WM_DEVICECHANGE message that user-mode programs could process to monitor, and sometimes control, hardware and power changes in the system. The newer operating systems build on WM_DEVICECHANGE to allow user-mode programs to easily detect when some driver enables or disables a registered device interface. Kernel-mode drivers can also register for similar notifications.
NOTE
Refer to the documentation for WM_DEVICECHANGE, RegisterDeviceNotification, and UnregisterDeviceNotification in the Platform SDK. I'll give you examples of using this message and these APIs, but I won't explain all possible uses of them. Some of the illustrations that follow also assume you're comfortable programming with Microsoft Foundation Classes.
An application with a window can subscribe for WM_DEVICECHANGE messages related to a specific interface GUID (globally unique identifier). Here's an example, drawn from the AUTOLAUNCH sample described in Chapter 12, "Installing Device Drivers," of how to do this:
int CAutoLaunch::OnCreate(LPCREATESTRUCT csp) { DEV_BROADCAST_DEVICEINTERFACE filter = {0}; filter.dbcc_size = sizeof(filter); filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; filter.dbcc_classguid = GUID_AUTOLAUNCH_NOTIFY; HDEVNOTIFY hNotification = RegisterDeviceNotification(m_hWnd, (PVOID) &filter, DEVICE_NOTIFY_WINDOW_HANDLE); ... } |
The key statement here is the call to RegisterDeviceNotification, which asks the PnP Manager to send our window a WM_DEVICECHANGE message whenever anyone enables or disables a GUID_AUTOLAUNCH_NOTIFY interface. So, suppose a device driver calls IoRegisterDeviceInterface with this interface GUID during its AddDevice function. We're asking to be notified when that driver calls IoSetDeviceInterfaceState to either enable or disable that registered interface.
NOTE
The Platform SDK documentation tells you to call UnregisterDeviceNotification to unregister the notification handle you get back from RegisterDeviceNotification. You should certainly do so in Windows 2000, but not in Windows 98. Although Windows 98 supports RegisterDeviceNotification as a way to subscribe for WM_DEVICECHANGE messages pertaining to a specific device interface, UnregisterDeviceNotification seems to destabilize the system. Just calling this function led to a number of random crashes during my own testing. I eventually just stopped calling UnregisterDeviceNotification and nothing bad seemed to happen as a result.
The handler for WM_DEVICECHANGE messages would be something like this:
BOOL CAutoLaunch::OnDeviceChange(UINT evtype, DWORD dwData) { _DEV_BROADCAST_HEADER* dbhdr = (_DEV_BROADCAST_HEADER*) dwData; if (!dbhdr || dbhdr->dbcd_devicetype != DBT_DEVTYP_DEVICEINTERFACE) return TRUE; PDEV_BROADCAST_DEVICEINTERFACE p = (PDEV_BROADCAST_DEVICEINTERFACE) dbhdr; CString devname = p->dbcc_name; if (evtype == DBT_DEVICEARRIVAL) <handle arrival> else if (evtype == DBT_DEVICEREMOVECOMPLETE) <handle removal> return TRUE; } |
This handler ignores all messages that don't pertain to device interfaces. The devname variable will be the symbolic link name for the device that's arriving or departing. (This is the same name you obtain with SetupDiGetDeviceInterfaceDetail and pass to CreateFile.) Refer to Chapter 12 for details about how you can use various SetupDiXxx APIs to learn interesting information about the new device.
The PnP Manager won't be able to remove your device object while an application has a handle open. To permit removal to occur, your driver has to somehow induce applications with open handles to close them. A variation on the device interface notification change message considered in the previous section comes to your rescue here.
Once the application has a handle to your device, it should call RegisterDeviceNotification to register for handle notifications. (See TESTDLG.CPP in the TEST subdirectory of the PNPEVENT sample on the companion disc.)
DEV_BROADCAST_HANDLE filter = {0}; filter.dbch_size = sizeof(filter); filter.dbch_devicetype = DBT_DEVTYP_HANDLE; filter.dbch_handle = m_hDevice; // the device handle HDEVNOTIFY hNotify = RegisterDeviceNotification(m_hWnd, &filter, DEVICE_NOTIFY_WINDOW_HANDLE); |
Now the application can be on the lookout for a WM_DEVICECHANGE with an event code (wParam ) equal to DBT_DEVICEQUERYREMOVE and a devicetype of DBT_DEVTYP_HANDLE. That message means that the interface is about to be disabled, and you should therefore close your handles. You should also unconditionally return TRUE from your message handler.
NOTE
According to the Platform SDK documentation, you can return BROADCAST_QUERY_DENY in response to a DBT_DEVICEQUERYREMOVE message. This special return value supposedly means you don't want the device removed or disabled after all. I've encountered wildly different results from attempting this in various versions of Windows 98 and Windows 2000. I would recommend that you program applications to always succeed this query.
Windows 2000 service programs can also subscribe for PnP notifications. The service should call RegisterServiceCtrlHandlerEx to register an extended control handler function. Then it can register for service control notifications about device interface changes. For example, take a look at the following code (and see the AUTOLAUNCH sample).
DEV_BROADCAST_DEVICEINTERFACE filter = {0}; filter.dbcc_size = sizeof(filter); filter.dbcc_devicetype = DBT_DEVTYPE_DEVICEINTERFACE; filter.dbcc_classguid = GUID_AUTOLAUNCH_NOTIFY; m_hNotification = RegisterDeviceNotification(m_hService, (PVOID) &filter, DEVICE_NOTIFY_SERVICE_HANDLE); |
Here, m_hService is a service handle provided by the service manager when it starts your service, and DEVICE_NOTIFY_SERVICE_HANDLE indicates that you're registering for service control notifications instead of window messages. After receiving a SERVICE_CONTROL_STOP command, you want to unregister the notification handle:
UnregisterDeviceNotification(m_hNotification); |
When a PnP event involving the interface GUID occurs, the system calls your extended service control handler function:
DWORD _ _stdcall HandlerEx(DWORD ctlcode, DWORD evtype, PVOID evdata, PVOID context) { } |
where ctlcode will equal SERVICE_CONTROL_DEVICEEVENT, evtype will equal DBT_DEVICEARRIVAL or one of the other DBT_Xxx codes, evdata will be the address of a Unicode version of the DEV_BROADCAST_DEVICEINTERFACE structure, and context will be whatever context value you specified in your call to the RegisterServiceCtrlHandlerEx function.
WDM drivers can use IoRegisterPlugPlayNotification to subscribe for interface and handle notifications. Here's an exemplary statement from the PNPMON sample driver that registers for notifications about the arrival and departure of an interface GUID designated by an application—PNPMON's TEST.EXE in this case—via an I/O control (IOCTL) operation:
status = IoRegisterPlugPlayNotification (EventCategoryDeviceInterfaceChange, PNPNOTIFY_DEVICE_INTERFACE_INCLUDE_EXISTING_INTERFACES, &p->guid, pdx->DriverObject, (PDRIVER_NOTIFICATION_CALLBACK_ROUTINE) OnPnpNotify, reg, ®->InterfaceNotificationEntry); |
The first argument indicates that we want to receive notifications whenever something enables or disables a specific interface GUID. The second argument is a flag indicating that we want to receive callbacks right away for all instances of the interface GUID that are already enabled. This flag allows us to start after some or all of the drivers that export the interface in question and still receive notification callbacks about those interfaces. The third argument is the interface GUID in question. In this case, it comes to us via an IOCTL from an application. The fourth argument is the address of our driver object. The PnP Manager adds a reference to the object so that we can't be unloaded while we have any notification handles outstanding. The fifth argument is the address of a notification callback routine. The sixth argument is a context parameter for the callback routine. In this case, I specified the address of a structure (reg) that contains information relative to this registration call. The seventh and final argument gives the address of a variable where the PnP Manager should record a notification handle. We will eventually call IoUnregisterPlugPlayNotification with the notification handle.
You need to call IoUnregisterPlugPlayNotification to close the registration handle. Since IoRegisterPlugPlayNotification adds a reference to your driver object, it won't do you any particular good to put this call in your DriverUnload routine. DriverUnload won't be called until the reference count drops to 0, which will never happen if DriverUnload itself has the unregistration calls. This problem isn't hard to solve—you just need to pick an appropriate time to unregister, such as when you notice the last interface of a particular type being removed or in response to an IOCTL request from an application.
Given a symbolic link name for an enabled interface, you can also request notifications about changes to the device named by the link. For example:
PUNICODE_STRING SymbolicLinkName; // input to this process PDEVICE_OBJECT DeviceObject; // an output PFILE_OBJECT FileObject; // another output IoGetDeviceObjectPointer(&SymbolicLinkName, 0, &FileObject, &DeviceObject); IoRegisterPlugPlayNotification(EventCategoryTargetDeviceChange, 0, FileObject, pdx->DriverObject, (PDRIVER_NOTIFICATION_CALLBACK_ROUTINE) OnPnpNotify, reg, ®->HandleNotificationEntry); |
You shouldn't put this code inside your PnP event handler, by the way. IoGetDeviceObjectPointer internally performs an open operation for the named device object. A deadlock might occur if the target device were to perform certain kinds of PnP operations. You should instead schedule a work item by calling IoQueueWorkItem. Chapter 9 has more information about work items. The PNPMON sample driver illustrates how to use a work item in this particular situation.
The notifications that result from these registration calls take the form of a call to the callback routine you specified:
NTSTATUS OnPnpNotify(PPLUGPLAY_NOTIFICATION_HEADER hdr, PVOID Context) { ... return STATUS_SUCCESS; } |
The PLUGPLAY_NOTIFICATION_HEADER structure is the common header for several different structures that the PnP Manager uses for notifications:
typedef struct _PLUGPLAY_NOTIFICATION_HEADER { USHORT Version; USHORT Size; GUID Event; } PLUGPLAY_NOTIFICATION_HEADER, *PPLUGPLAY_NOTIFICATION_HEADER; |
The Event GUID indicates what sort of event is being reported to you. See Table 6-6. The DDK header file WDMGUID.H contains the definitions of these GUIDs.
Table 6-6. PnP notification GUIDs.
GUID Name | Purpose of Notification |
---|---|
GUID_HWPROFILE_QUERY_CHANGE | Okay to change to a new hardware profile? |
GUID_HWPROFILE_CHANGE_CANCELLED | Change previously queried about has been cancelled |
GUID_HWPROFILE_CHANGE_COMPLETE | Change previously queried about has been accomplished |
GUID_DEVICE_INTERFACE_ARRIVAL | A device interface has just been enabled |
GUID_DEVICE_INTERFACE_REMOVAL | A device interface has just been disabled |
GUID_TARGET_DEVICE_QUERY_REMOVE | Okay to remove a device object? |
GUID_TARGET_DEVICE_REMOVE_CANCELLED | Removal previously queried about has been cancelled |
GUID_TARGET_DEVICE_REMOVE_COMPLETE | Removal previously queried about has been accomplished |
If you receive either of the DEVICE_INTERFACE notifications, you can cast the hdr argument to the callback function as a pointer to the following structure:
typedef struct _DEVICE_INTERFACE_CHANGE_NOTIFICATION { USHORT Version; USHORT Size; GUID Event; GUID InterfaceClassGuid; PUNICODE_STRING SymbolicLinkName; } DEVICE_INTERFACE_CHANGE_NOTIFICATION, *PDEVICE_INTERFACE_CHANGE_NOTIFICATION; |
In the interface change notification structure, InterfaceClassGuid is the interface GUID, and SymbolicLinkName is the name of an instance of the interface that's just been enabled or disabled.
If you receive any of the TARGET_DEVICE notifications, you can cast the hdr argument as a pointer to this structure instead:
typedef struct _TARGET_DEVICE_REMOVAL_NOTIFICATION { USHORT Version; USHORT Size; GUID Event; PFILE_OBJECT FileObject; } TARGET_DEVICE_REMOVAL_NOTIFICATION, *PTARGET_DEVICE_REMOVAL_NOTIFICATION; |
where FileObject is the file object for which you requested notifications.
Finally, if you receive any of the HWPROFILE_CHANGE notifications, hdr will really be a pointer to this structure:
typedef struct _HWPROFILE_CHANGE_NOTIFICATION { USHORT Version; USHORT Size; GUID Event; } HWPROFILE_CHANGE_NOTIFICATION, *PHWPROFILE_CHANGE_NOTIFICATION; |
This doesn't have any more information than the header structure itself—just a different typedef name.
One way to use these notifications is to implement a filter driver for an entire class of device interfaces. (There is a standard way to implement filter drivers, either for a single driver or for a class of devices, based on setting entries in the registry. I'll discuss that subject in Chapter 9. Here, I'm talking about filtering all devices that register a particular interface, for which there's no other mechanism.) In your driver's DriverEntry routine, you'd register for PnP notifications about one or more interface GUIDs. When you receive the arrival notification, you use IoGetDeviceObjectPointer to open a file object and then register for target device notifications about the associated device. You also get a device object pointer from IoGetDeviceObjectPointer, and you can send IRPs to that device by calling IoCallDriver. Be on the lookout for the GUID_TARGET_DEVICE_QUERY_REMOVE notification because you have to dereference the file object before the removal can continue.
I'll close this chapter by explaining how a WDM driver can generate custom PnP notifications. To signal a custom PnP event, create an instance of the custom notification structure and call one of IoReportTargetDeviceChange or IoReportTargetDeviceChangeAsynchronous. The asynchronous flavor returns immediately. The synchronous flavor waits—a long time, in my experience—until the notification has been sent. The notification structure has this declaration:
typedef struct _TARGET_DEVICE_CUSTOM_NOTIFICATION { USHORT Version; USHORT Size; GUID Event; PFILE_OBJECT FileObject; LONG NameBufferOffset; UCHAR CustomDataBuffer[1]; } TARGET_DEVICE_CUSTOM_NOTIFICATION, *PTARGET_DEVICE_CUSTOM_NOTIFICATION; |
Event is the custom GUID you've defined for the notification. FileObject is NULL—the PnP Manager will be sending notifications to drivers who opened file objects for the same PDO as you specify in the IoReportXxx call. CustomDataBuffer contains whatever binary data you elect followed by Unicode string data. NameBufferOffset is -1 if you don't have any string data; otherwise, it's the length of the binary data that precedes the strings. You can tell how big the total data payload is by subtracting the field offset of CustomDataBuffer from the Size value.
Here's how PNPEVENT generates a custom notification when you press the Send Event button in the associated test dialog:
struct _RANDOM_NOTIFICATION : public _TARGET_DEVICE_CUSTOM_NOTIFICATION { WCHAR text[14]; }; ... _RANDOM_NOTIFICATION notify; notify.Version = 1; notify.Size = sizeof(notify); notify.Event = GUID_PNPEVENT_EVENT; notify.FileObject = NULL; notify.NameBufferOffset = FIELD_OFFSET(RANDOM_NOTIFICATION, text) - FIELD_OFFSET(RANDOM_NOTIFICATION, CustomDataBuffer); *(PULONG)(notify.CustomDataBuffer) = 42; wcscpy(notify.text, L"Hello, world!"); IoReportTargetDeviceChangeAsynchronous(pdx->Pdo, ¬ify, NULL, NULL); |
That is, PNPEVENT generates a custom notification whose data payload contains the number 42 followed by the string, Hello, world!.
Incidentally, if you want to use the asynchronous reporting API, which I recommend because it returns immediately, you must include NTDDK.H instead of WDM.H and you must link with both WDM.LIB and NTOSKRNL.LIB.
The notification shows up in any driver that registered for target device notifications pertaining to a file object for the same PDO. If your notification callback routine gets a notification structure with a nonstandard GUID in the Event field, you can expect that it's somebody's custom notification GUID. You need to understand what the GUID means before you go mucking about in the CustomDataBuffer!
User-mode applications are supposed to be able to receive custom event notifications, too, but I've not been able to get that to work.