OSRLogo
OSRLogoOSRLogoOSRLogo x Subscribe to The NT Insider
OSRLogo
x

Everything Windows Driver Development

x
x
x
GoToHomePage xLoginx
 
 

    Thu, 14 Mar 2019     118020 members

   Login
   Join


 
 
Contents
  Online Dump Analyzer
OSR Dev Blog
The NT Insider
The Basics
File Systems
Downloads
ListServer / Forum
  Express Links
  · The NT Insider Digital Edition - May-June 2016 Now Available!
  · Windows 8.1 Update: VS Express Now Supported
  · HCK Client install on Windows N versions
  · There's a WDFSTRING?
  · When CAN You Call WdfIoQueueP...ously

Buddy Drivers - Methods for Driver to Driver Communication

When you have a complex or wide-ranging set of functionality to implement in kernel mode, it is often useful to break that functionality down into multiple drivers.  These drivers will need to communicate with each other, either by sending IRPs from one driver to the other, or even having one driver call entry points made accessible in the other driver.  In this article, we will explore two of the best, but also the least well-known, techniques for driver-to-driver communication in Win2K.  All these techniques work for Windows 2000, Windows XP, and Windows .NET server, all the functions are fully documented, and all the prototypes appear in wdm.h.  So, let’s get started!

 

Method 1: Locating Device Objects

Back before plug and play, when you could specify exactly when each driver in the system would start, life was simple.  If you had a pair of drivers that needed to communicate with each other, you just made the driver that started second look for a named Device Object created by the driver that started first (See example in Figure 1).

 

//

// Get a pointer to our cooperating driver's device object

//

RtlInitUnicodeString(&buddyName, L"\\Device\\FirstDriver");

 

code = IoGetDeviceObjectPointer(&buddyName,

                               FILE_ALL_ACCESS,

                               &devExt->BuddyFileObject,

                               &devExt->BuddyDevice);

 

//

// If the other driver's not loaded, we have to error out!

//

if( !NT_SUCCESS(code) )  {

 

    //

    // Can’t find our buddy!

    //       

#if DBG

    DbgPrint("SecondDriver: IoGetDevObj failed.  0x%x\n", code);

#endif

 

}

 

Figure 1 – Locating Device Objects Pre Plug & Play

 

While this technique can still sort of be made to work in Win2K/XP/.NET, there are some ugly limitations.  The first limitation is that NT with PnP does not provide any guarantee of device load order within startup times.  So, if you want to use this technique you would have to (for example) start the first driver at boot start time, and the second driver at system start time.  Yuck!  Or else (even uglier) have the first driver start a thread that polls, calling IoGetDeviceObjectPointer(), looking for the second driver to start.  And what if you don’t create a named Device Object at all in your WDM driver, opting instead for registering a device interface (using IoRegister DeviceInterface())?

 

One potentially elegant solution to this problem is the creation of a bus driver.  You have the first driver act as a bus driver, and cause the second driver to be loaded through the use of IRP_MN_QUERY_DEVICE_RELATIONS for BusRelations.  Then, not only can the first driver find the second driver’s Device Object easily, but the second driver can also easily find the first driver’s Device Object.  However, creating a bus driver just for this purpose introduces additional complexity that’s probably not needed.  It’s a bit like swatting a fly with an MX-missile...sure, it works, but it’s not really what the mechanism is intended for.  And, anyhow, there’s a better and much easier way.

 

Using PnP Notification

The easiest way of finding out when a device (or, more accurately, a device interface registered by a driver) arrives is by using PnP notification.  And, yes, it is fully documented in the DDK.  Your driver can register to be called-back by the PnP Manager on occurrence of particular PnP events by using the function IoRegisterPlugPlayNotification().  For example, check out the following function call, which we’ll refer to as Figure 2.

 

code = IoRegisterPlugPlayNotification(

EventCategoryDeviceInterfaceChange,

               PNPNOTIFY_DEVICE_INTERFACE_INCLUDE_EXISTING_INTERFACES,

                     (LPGUID)&GUID_OSR_DRIVERA_INTERFACE,

                     DeviceObject->DriverObject,

                     PlugPlayCallback,

                     DeviceObject,

                     &devExt->NotificationEntry);

 

Figure 2 – IoRegisterPlugPlayNotification

 

In the function call above, our driver registers for the PnP event category EventCategoryDeviceInterfaceChange.  This causes our driver to be called back any time the indicated interface (GUID_OSR_DRIVERA_INTERFACE in the example) changes state.  So, when another driver in the system calls IoSetDeviceInterfaceState() specifying GUID_OSR_DRIVERA_INTERFACE as the interface, our driver gets called back.  In the example above our driver would be called back at function PlugPlayCallback, which has been specified as the fifth parameter on the function call.  A driver-defined context value is specified as the sixth parameter.  In this case, our driver has chosen to pass a pointer to its Device Object.  The last parameter provides storage for a PVOID that identifies this registration for notification.  This value is returned by IoRegisterPlugPlayNotification, and must be supplied by our driver when it later deregisters (such as during processing of a PnP remove).

 

I can hear you thinking: “That’s fine, as long as our driver gets loaded before that other driver calls IoSetDeviceInterfaceState().  But suppose our driver gets loaded after all the GUID_OSR_DRIVERA_INTERFACEs have been set to enabled?”

 

No problem.  That’s what the flag PNPNOTIFY_DEVICE_ INTERFACE_INCLUDE_EXISTING_INTERFACES is for.  When this flag is specified, in addition to the “normal” callback that occurs whenever an interface change occurs, our driver will also be called back once for each interface that has been already been enabled.  So, it doesn’t matter if our driver starts before or after the other driver.  It will be called back once for each enabled interface in any case.

 

Our driver’s callback function is shown in Figure 3.  When called back, the PnP Manager passes our driver a pointer to a DEVICE_INTERFACE_CHANGE_NOTIFICATION structure (shown in Figure 4).  This structure contains GUIDs that indicate the event and the interface on which the event occurred.  The structure also contains a pointer to a UNICODE_STRING that specifies the symbolic link name of the device on which the event occurred.  Note that the status that you return from your notification callback function, when used for DeviceInterfaceChange notifications, doesn’t matter. It’s ignored by the PnP Manager.

 

NTSTATUS

PlugPlayCallback(PVOID NotificationStructure,

                 PVOID Context)  {

    PDEVICE_INTERFACE_CHANGE_NOTIFICATION notify =

                                  (PDEVICE_INTERFACE_CHANGE_NOTIFICATION)NotificationStructure;

    PDEVICE_OBJECT deviceObject = (PDEVICE_OBJECT)Context;

 

    if(IsEqualGUID( (LPGUID)&(notify->Event),

                         (LPGUID)&GUID_DEVICE_INTERFACE_ARRIVAL)) {

        DbgPrint("DRIVERB:>>>> ARRIVAL of DRIVERA reported\n");

    }

   

    if(IsEqualGUID( (LPGUID)&(notify->Event),

                             (LPGUID)&GUID_DEVICE_INTERFACE_REMOVAL)) {

        DbgPrint("<<<< REMOVAL\n");

    }

 

    return(STATUS_SUCCESS);                       

}

 

Figure 3 – Callback Function

 

As you can see, it really is pretty easy to determine when a specific device interface is enabled or disabled. Another interesting use for IoRegisterPlugPlayNotification is to register for a callback to determine whenever a specific device receives a remove-type event.  This event category is Event CategoryTargetDeviceChange.  This event is particularly useful when you’re communicating with another driver, using IoCallDriver() for instance.  You can register for a notification of a TargetDeviceChange on the device that you’re communicating with, so you will be informed (via a callback) when that device is about to be removed.  This will allow you to dereference the target Device Object in a timely manner.

 

PnP Notification “Gotchas”

While PnP Notification is pretty cool, there are one or two “gotchas” of which  you should be aware.  As an example, let’s once again consider our pair of drivers that need to communicate.  DriverB in the pair registers a PnP Notification routine for DeviceInterfaceChange, specifying that it wants to be called for existing interfaces, just as shown in Figure 2.

 

Version

 

Size

 

Event

 

InterfaceClassGuid

 

SymbolicLinkName

 

        Version                  Size

Event

InterfaceClassGuid

SymbolicLinkName

Figure 4 — DEVICE_INTERFACE_

CHANGE_NOTIFICATION

 

Because the whole point of registering the notification is for the two drivers to communicate, once DriverB has been notified that DriverA’s interface is enabled, DriverB probably wants to get a pointer to DriverA’s Device Object (so it can send IRPs to DriverA using IoCallDriver).  The “natural” way to do this is shown in Figure 5.

 

NTSTATUS

PlugPlayCallback(PVOID NotificationStructure,

                 PVOID Context)  {

     PDEVICE_INTERFACE_CHANGE_NOTIFICATION notify = PDEVICE_INTERFACE_CHANGE_NOTIFICATION)NotificationStructure;

     PDEVICE_OBJECT deviceObject = (PDEVICE_OBJECT)Context;

     PMYDEVICE_EXTENSION devExt = deviceObject->DeviceExtension;

 

    if(IsEqualGUID( (LPGUID)&(notify->Event),(LPGUID)&GUID_DEVICE_INTERFACE_ARRIVAL)) {

                                  DbgPrint("DRIVERB:>>>> ARRIVAL of DRIVERA reported\n");

        

        //

        // You MIGHT think the right thing to do would be the following.

        // But THIS CAN CAUSE A SYSTEM HANG.  DO NOT DO THIS.

        //

 code = IoGetDeviceObjectPointer(notify->SymbolicLinkName,

                           FILE_ALL_ACCESS,

                           &devExt->BuddyFileObject,

                           &devExt->BuddyDevice);

 

  }

   

  return(STATUS_SUCCESS);                       

}

 

Figure 5 – The wrong way to write a PnP Notification Callback

 

And, while you’d be partially right, you do get a pointer to a device object using its name by calling IoGetDeviceObject Pointer, you’d also get bitten by one of the conditions of PnP Notification routines.  As it very clearly states in the documentation:

 

A callback routine must not open the device directly. If the provider of the interface causes blocking PnP events, the notification callback routine can cause a deadlock if it tries to open the device in the callback thread.

 

When you call IoGetDeviceObjectPointer, you’re actually issuing an open (IRP_MJ_CREATE) for the specified device.  That’s why you get back a File Object pointer, in addition to the Device Object pointer that you wanted.  So, the proper thing to do is queue a work item that does the call to IoGetDeviceObjectPointer, as shown in Figure 6.

 

NTSTATUS

PlugPlayCallback(PVOID NotificationStructure,

                 PVOID Context)  {

    PDEVICE_INTERFACE_CHANGE_NOTIFICATION notify = (PDEVICE_INTERFACE_CHANGE_NOTIFICATION)NotificationStructure;

    PDEVICE_OBJECT deviceObject = (PDEVICE_OBJECT)Context;

    PIO_WORKITEM workItem;

    PMY_WORK_CONTEXT myWorkContext;

 

    if(IsEqualGUID( (LPGUID)&(notify->Event),

                         (LPGUID)&GUID_DEVICE_INTERFACE_ARRIVAL)) {

        DbgPrint("DRIVERB:>>>> ARRIVAL of DRIVERA reported\n");

       

        workItem = IoAllocateWorkItem(deviceObject);

 

        myWorkContext = (PMY_WORK_CONTEXT)ExAllocatePoolWithTag(NonPagedPool,

                                               sizeof(MY_WORK_CONTEXT),

                                               'cWbD');

        if(!myWorkContext)  {

            IoFreeWorkItem(workItem);

            return(STATUS_SUCCESS);

        }

 

        myWorkContext->Item = workItem;

        myWorkContext->SymbolicLinkName = notify->SymbolicLinkName;

 

        IoQueueWorkItem(workItem,

                    ArrivalWorker,

                    DelayedWorkQueue,

                    workItem);

    }

   

    if(IsEqualGUID( (LPGUID)&(notify->Event),

                             (LPGUID)&GUID_DEVICE_INTERFACE_REMOVAL)) {

        DbgPrint("<<<< REMOVAL\n");

    }

 

    return(STATUS_SUCCESS);                       

}

 

Figure 6 – Queuing a work item on interface arrival notification

 

When you create your work item, don’t forget to use the IoAllocateWorkItem() and IoQueueWorkItem(), instead of ExInitializeWorkItem() and ExQueueWorkItem() which are obsolete. 

 

It’s in this worker routine that you should call IoGetDeviceObjectPointer() with the passed symbolic link name.  You’ll get back a pointer to the Device Object and also a pointer to a File Object. You can use this File Object pointer to specify a PnP Notification callback for EventCategory TargetDeviceChange, so your driver will be called back when the device to which you’ve gotten a pointer is being removed, as we discussed briefly above.

 

Method 2: Callbacks

There’s an even more clever method of doing driver-driver communication in Win2K/XP/.NET.  That method entails the use of callback objects.

 

Callback objects were created to allow a generic mechanism for notifying drivers of events.  For example, the system creates PowerState and SetSystemTime callback objects.  A driver can register for one of these callbacks, and its callback function will be called when the associated event occurs.

 

Happily for us, the developers provided us with a mechanism to create callbacks of our own.  Depending on your specific requirements, this mechanism could even eliminate the need for you to determine when your buddy driver is loaded.  Setting up and using callbacks is a three step process:

 

1.   You “create” (or open) a named Callback Object.

2.   You register a callback function that is to be called whenever the previously opened Callback Object is notified.

3.   You notify the previously opened Callback Object.  This causes any callback functions that have been registered for that Object to be called.

 

Creating, Registering, and Notifying Callbacks

On startup, each of your cooperating drivers call ExCreate Callback(), as shown in Figure 7.

 

RtlInitUnicodeString(&callbackName, L"\\Callback\\DriverBCB");

 

InitializeObjectAttributes(&objAttrib,

                           &callbackName,

                           OBJ_CASE_INSENSITIVE | OBJ_PERMANENT,

                           NULL,

                           NULL);

 

code = ExCreateCallback(&devExt->CallbackObj,

                          &objAttrib,

                          TRUE,

                          TRUE);

if(!NT_SUCCESS(code) )  {

    DbgPrint("Create Callback failed 0x%0x\n", code);                   

}

 

Figure 7 – ExCreateCallback()

 

Callback Objects are always named, and should be located in the Object Manger’s Callback directory.  When you initialize the object’s attributes, you must specify OBJ_PERMANENT.  When each driver calls ExCreateCallback() it should specify that the object should be created if it does not already exist (by setting the third parameter to the function to TRUE, as shown in Figure 7). As a result, the cooperating drivers may start in any order.

 

The last (fourth) parameter to ExCreateCallback() can be particularly useful.  Specifying TRUE for this parameter when the callback is created allows multiple callback routines to be registered for this callback object.  In other words, if you specify TRUE, multiple drivers can each register one or more functions to be called when this Callback Object is notified.

 

The only thing that can be slightly confusing about this mechanism is that ExCreateCallback() only builds or opens the specified Callback Object and returns a pointer to the callback object that is either created or opened as a result of the call.  It does not register a function to be called.  When a driver wants to register a function that will be called back when a previously opened Callback Object is notified, it calls ExRegisterCallback().  This is shown in Figure 8.

 

devExt->Callback = ExRegisterCallback(devExt->CallbackObj,

                                    MyCallbackFunction,

                                    devExt);

 

if(devExt->Callback == NULL)  {

    DbgPrint("Register Callback failed??\n");

}

 

Figure 8 – Registering for a Callback

 

In Figure 8, the driver registers the function named MyCallbackFunction to be called whenever the Callback Object pointed to by the contents of devExt->CallbackObj is notified.  ExRegisterCallback() returns a callback registration handle, which must be saved so it can be used to deregister the callback function by calling ExUnregister Callback().  The third parameter of ExRegisterCallback() allows the driver to specify a context value that is passed to the callback function.  As with any such pointer in an NT driver, this is typically a pointer to either the Device Object or Device Extension (but, of course, it could be anything).  A sample callback function is shown in Figure 9.

 

VOID

MyCallbackFunction(PVOID Context, PVOID Arg1, PVOID Arg2)  {

     ULONG iteration = (ULONG)Arg1;

 

     DbgPrint("*** Callback in Driver B called... Iteration = %d\n",

              iteration);

}

 

Figure 9 – Sample Callback Function

 

The first argument passed to the callback function when it is called is the Context value specified when ExRegister Callback() was called.  The other two parameters are provided when the callback is notified.

 

To cause the callbacks to called, you use ExNotifyCallback() as shown in Figure 10.

 

ExNotifyCallback(devExt->CallbackObj,

(PVOID)iteration,

(PVOID)dataValue2);

 

Figure 10 – ExNotifyCallback()

 

In Figure 10, the driver requests that all the registered callback functions for the Callback Object pointed to by devExt->CallbackObj be called.  To each of these functions, it passes the two additional arguments shown (“iteration” and “dataValue2”).

 

Using Callbacks

My experience using Callback Objects indicates that all the callbacks are called synchronously, before my call to ExNotifyCallback() returns. Obviously, this is an implementation detail, and not something you should count on.  According to the DDK documentation, the system calls the callback functions in the order in which they were registered, and at the IRQL at which ExNotifyCallback() is called. That sounds pretty cool to me.

 

Aside from remembering that you need to create the Callback Object itself with OBJ_PERMANENT, just about the only “trick” that you’ll need is to remember to unregister any callback functions that are registered.  Depending on how your driver is structured, you might want to do this in your STOP_DEVICE processing.  Also, you should dereference any Callback Objects that you’ve opened or created, simply by calling ObDereferenceObject.

 

Wrapping Up

So there are two extremely useful mechanisms for driver-to-driver communication.  They’re both documented, all the necessary functions are in wdm.h, and they all work in Windows 2000 as well as Windows XP and .NET Server.  Given their flexibility, it’s amazing that these two mechanisms are not better known.  Then again, so much has been added to the DDK starting with Windows 2000 that it can be hard to keep up.  That’s why we’re here to help!

 

 

 

User Comments
Rate this article and give us feedback. Do you find anything missing? Share your opinion with the community!
Post Your Comment

"RE: your article does help me much"
>a small question to confirm: IoGetDeviceObjectPointer accept both >"symbolic link name" and "device object name", right ?

Correct.

-scott

04-Jan-10, Scott Noone


"your article does help me much"
a small question to confirm: IoGetDeviceObjectPointer accept both "symbolic link name" and "device object name", right ?

Rating:
04-Jan-10, Wilson Hu


"When to create callback object"
When should we create callback object using ExCreateCallback()? Is it in AddDevice? Or in IRP_MN_START_DEVICE?

Rating:
31-Mar-09, Venkata Rajesh Kumar Prava


"ExNotifyCallback"
I enjoyed your article regarding using ExNotifyCallback. It is exactly what I am looking for to use communicating between SCSI Miniport drivers. One driver to be used as a Virtual Miniport and the other as the physical HBA miniport.

Your instructions for implementing were clear and I had no problems building upon your example.

However, I did find a problem that I am interested in your feedback. I discovered that if ExNotifyCallback is called from a AdapterControl routine (raised IRQL) then the call to ExNotifyCallback seems to hang. I am not sure exactly where it is hung up.

Your article states that the callback is called at the caller's IRQL, but it appears to me that the caller must be at passive level or it will hang. Any comments?

Thanks, Dave

Rating:
22-May-07, David Eaves


Post Your Comments.
Print this article.
Email this article.
bottom nav links