[Previous] [Next]

Other Configuration Functionality

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.

Filtering Resource Requirements

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;
  }

  1. The parameters for this request include a list of I/O resource requirements. These would be derived from the device's configuration space, the registry, or wherever the bus driver happens to find them.
  2. Higher-level drivers might have already filtered the resources by adding additional ones to the original list. If so, they set the IoStatus.Information field to point to the expanded requirements list structure.
  3. If there's no filtered list, we will extend the original list. If there's a filtered list, we'll extend that.
  4. Theoretically, several alternative lists of requirements could exist, but dealing with that situation is beyond the scope of this simple example.
  5. We need to add any resources before we pass the request down the stack. First we allocate a new requirements list and copy the old requirements into it.
  6. Taking care to preserve the preexisting order of the descriptors, we add our own resource description. In this example, we're adding a resource that's private to the driver.
  7. We store the address of the expanded list of requirements in the IRP's IoStatus.Information field, which is where lower-level drivers and the PnP system will be looking for it. If we just extended an already filtered list, we need to release the memory occupied by the old list.
  8. We pass the request down using the same ForwardAndWait helper function that we used for IRP_MN_START_DEVICE. If we weren't going to modify any resource descriptors on the IRP's way back up the stack, we could just call DefaultPnpHandler here and propagate the returned status.
  9. When we complete this IRP, whether we indicate success or failure, we must take care not to modify the Information field of the I/O status block: it might hold a pointer to a resource requirements list that some driver—maybe even ours!—installed on the way down. The PnP Manager will release the memory occupied by that structure when it's no longer needed.

Device Usage Notifications

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.

DeviceUsageTypePaging

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.

DeviceUsageTypeDumpFile

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:

DeviceUsageTypeHibernation

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.

Controller and Multifunction Devices

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.

Overall Architecture

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.

Creating Child Device Objects

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;
  }

  1. CHILDTYPEA and CHILDTYPEB are additional flag bits for the flags member that begins the common device extension. If you were writing a true bus driver, you wouldn't create the child PDOs here—you'd enumerate your actual hardware in response to an IRP_MN_QUERY_DEVICE_RELATIONS and create the PDOs then.
  2. We're creating a named device object here, but we're asking the system to automatically generate the name by supplying the FILE_AUTOGENERATED_DEVICE_NAME flag in the DeviceCharacteristics argument slot.

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.

Telling the PnP Manager About Our Children

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);
  }

  1. This IRP can concern several types of relations besides the bus relations we're interested in here. We simply delegate these other queries to the bus driver for the underlying hardware bus.
  2. Here, we allocate a structure that will contain two device object pointers. The DEVICE_RELATIONS structure ends in an array with a dimension of 1, so we need only add on the size of an additional pointer when we calculate the amount of memory to allocate.
  3. We call ObReferenceObject to increment the reference counts associated with each of the device objects we put into the DEVICE_RELATIONS array. The PnP Manager will dereference the objects at an appropriate time.
  4. We need to pass this request down to the real bus driver in case it or some lower filter knows additional facts that we didn't know. This IRP uses an unusual protocol for pass-down and completion. You set the IoStatus as shown here if you actually handle the IRP; otherwise, you leave the IoStatus alone. Note the use of the Information field to contain a pointer to the DEVICE_RELATIONS structure. In other situations we've encountered in this book, the Information field has always held a number.

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.

PDO Handling of PnP Requests

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;
  }

  1. We're going to send the repeater IRP to the topmost filter driver in the stack to which our FDO belongs. This service routine returns the address of the topmost device object, and it also adds a reference to the object to prevent the Object Manager from deleting the object for the time being.
  2. When we allocate the IRP, we create an extra stack location in which we can record some context information for the completion routine we're going to install. The DeviceObject pointer we place in this extra location becomes the first argument to the completion routine.
  3. Here, we initialize the first real stack location, which is the one that the topmost driver in the FDO stack will receive. Then we install our completion routine. This is an instance in which we cannot use the standard IoCopyCurrentIrpStackLocationToNext macro to copy a stack location: we're dealing with two separate I/O stacks.
  4. We need to plan ahead for how we're going to deal with the possibility that the parent device stack doesn't actually handle this repeater IRP. Our later treatment will depend on exactly which minor function of IRP we're repeating in a way I'll describe later on. Mechanically, what we do is calculate a Boolean value—I called it needsvote—and pass it as the context argument to our completion routine.
  5. You always initialize the status field of a new PnP IRP to hold the special value STATUS_NOT_SUPPORTED. The Driver Verifier will bugcheck if you don't.
  6. This statement is how we release our reference to the topmost device object in the FDO stack.
  7. We save the address of the original IRP here.
  8. This short section sets the completion status for the original IRP. Refer to the following main text for an explanation of what's going on here.
  9. We allocated the repeater IRP, so we need to delete it.
  10. We can complete the original IRP now that the FDO driver stack has serviced its clone.
  11. We must return STATUS_MORE_PROCESSING_REQUIRED because the IRP whose completion we dealt with—the repeater IRP—has now been deleted.

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.

Handling Device Removal

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.

Handling IRP_MN_QUERY_ID

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);
  }

  1. The instance identifier is a single string value that uniquely identifies a device of a particular type on a bus. Using a constant such as "0000" will not work if more than one device of the parent type can appear in the computer.
  2. The device identifier is a single string of the form "enumerator\type" and basically supplies two components in the name of the hardware registry key. Our ChildA device's hardware key will be in …\Enum\Mulfunc\*WCO0604\0000, for example.
  3. The hardware identifiers are strings that uniquely identify a type of device. In this case, I just made up the pseudo-EISA (Extended Industry Standard Architecture) identifiers *WCO0604 and *WCO0605.

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!

Handling IRP_MN_QUERY_DEVICE_RELATIONS

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

Handling Child Device Resources

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.

PnP Notifications

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.

Extensions to WM_DEVICECHANGE

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.

Knowing When to Close a Device Handle

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.

Notifications to Windows 2000 Services

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.

Kernel-Mode Notifications

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, &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, &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.

Custom Notifications

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, &notify, 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.