Last reviewed and updated: 10 August 2020
While putting together some new material for our Advanced WDF Driver Development seminar, we began taking yet another look at one of our favorite topics around here: driver to driver communication. As with every other topic related to Windows driver development, the seemingly innocuous task of calling from one driver into another quickly devolves into an endless series of options, features, and arcane WDM trivia.
However, a shining light came out of these discussions: the venerable Bus Interface architecture is still alive and kicking in Windows. Not only does it provide a clean, well defined method of driver to driver communication, but WDF provides helper routines to make producing or consuming one of these interfaces a breeze.
What’s a Bus Interface?
As the name would imply, a Bus Interface is a standard way for a Bus driver to provide a procedure call interface to its children. A Bus driver can have multiple Bus Interfaces, each of which is identified via a GUID. Consumers of the Interface query for the interface using PnP IRPs and are returned a data structure defined by the Bus, which can include any data or functions that the Bus driver wants to share (Figure 1).
When a Bus driver provides a Bus Interface for its children, the expectation is that only drivers within the PDO’s device stack will consume the Interface. One reason for this is that it eliminates any possible race conditions in the teardown case. Stacks are destroyed from the top down, thus the PDO is always the last device to be deleted. If the Bus Interface is only consumed by drivers higher in the stack, then we don’t need to worry about anyone trying to use the Bus Interface after the PDO has been deleted.
Bus Interfaces Aren’t Just for Buses
Clearly Bus Interfaces are generically useful outside of Bus drivers, driver to driver communication is common and not all driver to driver communication scenarios simply involve interacting with your own PDO. Thankfully, the Bus driver support in the operating system is generic. Thus it is possible for a filter or FDO to process the appropriate PnP IRPs and publish their own Bus Interface. This means we could have a consumer using a Bus Interface produced by a different device stack (Figure 2).
It also means that we could consume a Bus Interface of a producer above our device in the stack (Figure 3).
For these reasons, the WDK documentation has taken to using the term Driver Defined Interface as opposed to Bus Interface, so expect to see documentation referring to this feature using both terms.
Beware that there is a hidden complexity in using a Bus Interface produced by a device other than your PDO: teardown. Either the consumer or the producer of the interface must guarantee that the producer will not be removed while the consumer is still using the Bus Interface. Failure to do so will result in the consumer calling into an unloaded driver, which will result in an immediate bugcheck.
Providing a Bus Interface in WDF
As with so many other things, WDF makes providing a Bus Interface for our function, filter, or physical device object a breeze. Thankfully, the Framework makes no distinction in its API regarding which type of device the producer is. Instead, the WDFDEVICE object provides the WdfDeviceAddQueryInterface method to add a Bus Interface to an existing WDFDEVICE:
_Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) NTSTATUS WdfDeviceAddQueryInterface( _In_ WDFDEVICE Device, _In_ PWDF_QUERY_INTERFACE_CONFIG InterfaceConfig );
The driver-provided WDF_QUERY_INTERFACE_CONFIG structure defines both the GUID and the interface structure to the Framework, which are both specified when the structure is initialized with WDF_QUERY_INTERFACE_CONFIG_INIT:
VOID FORCEINLINE WDF_QUERY_INTERFACE_CONFIG_INIT( _Out_ PWDF_QUERY_INTERFACE_CONFIG InterfaceConfig, _In_opt_ PINTERFACE Interface, _In_ CONST GUID* InterfaceType, _In_opt_ PFN_WDF_DEVICE_PROCESS_QUERY_INTERFACE_REQUEST EvtDeviceProcessQueryInterfaceRequest );
The Framework then responds to the PnP IRPs sent by the consumers, returning the driver defined interface structure. The driver may optionally specify an EvtDeviceProcessQuery Interface event processing callback to be notified any time a consumer requests the interface from the producer.
Contrary to what you might expect, the WDF_QUERY_INTERFACE_CONFIG_INIT macro does not simply take a PVOID and length for the interface structure. Instead, it takes a pointer to an O/S defined INTERFACE structure. This INTERFACE structure is a required header for any Bus Interface provided by a producer. The INTERFACE structure definition is:
typedef struct _INTERFACE { USHORT Size; USHORT Version; PVOID Context; PINTERFACE_REFERENCE InterfaceReference; PINTERFACE_DEREFERENCE InterfaceDereference; // interface specific entries go here } INTERFACE, *PINTERFACE;
Per the comment in the structure definition, this common header is followed by the driver-defined portion of the structure. The INTERFACE structure has the following required members:
Size – The overall size of the structure, including the driver-defined portions
Version – A version for the structure. The consumer of the interface requests which version of the structure they would like, thus it’s possible to have multiple versions of the same interface
Context – A per-interface instance context value for the producer
InterfaceReference – A producer provided routine to acquire a reference to the interface
InterfaceDereference – A producer provided routine to release a reference on the interface
While most of those members are self-explanatory, the InterfaceReference and InterfaceDereference members require a bit of explanation.
According to the documentation, any time the producer exports the interface to a consumer, the InterfaceReference routine must be called by the producer. Likewise, once the consumer is done with the interface, the consumer must call the InterfaceDereference routine.
These referencing and dereferencing callbacks can serve two purposes. One is that the producer may have some per-interface instance state that must be torn down when the consumer is finished. Imagine a scenario where the producer allocates memory each time an interface is returned to a consumer. In this case, the producer must free the memory when the reference count on the interface goes to zero.
The other purpose of these callbacks is for the case of a consumer that is using an interface provided by a device other than their own PDO. Remember that in these cases we have to worry about potential teardown issues, as the I/O Manager does not happen to prevent the producer from being removed before the consumer. In these cases, these callbacks may be leveraged to prevent the producer from unloading until the last consumer releases its reference. This can unfortunately be a bit trickier than it sounds. The producer must be careful to not trigger its own unload from directly within the dereference callback. If it does, the producer runs the risk of unmapping the code for the dereference callback while it is still executing within the callback.
For most drivers, the tearing down of the interface does not require the producer to undo any state. If possible, it can be easier to solve the cross stack teardown problems in the consumer (e.g. by maintaining an open File Object to the producer’s Device Object). Thus, for many drivers the InterfaceDereference routine becomes an empty routine that does nothing. However, the presence of both the reference and dereference routines is still required by the O/S. Thankfully, the Framework developers anticipated this situation and provide default “no op” routines for your driver to use for both InterfaceRereference (WdfDeviceInterfaceReferenceNoOp)and InterfaceDereference (WdfDeviceInterfaceDereferenceNoOp).
We can now see all of the steps involved in being a Bus Interface provider in a WDF driver:
- Define a GUID for your Bus Interface
- Define our own custom INTERFACE structure
- Initialize WDF_QUERY_INTERFACE_CONFIG structure with WDF_QUERY_INTERFACE_CONFIG_INIT
- Call WdfDeviceAddQueryInterface to associate the interface with our device
The Framework then takes care of absolutely everything else. Pretty cool, eh? Let’s now see these steps in action.
The Framework then takes care of absolutely everything else. Pretty cool, eh? Let’s now see these steps in action.
Producer Step 1: Define a GUID for your Bus Interface
Nothing too shocking here, we define a GUID using the standard DEFINE_GUID macro provided with the WDK (below). Just don’t forget to include initguid.h!:
// {9671F9BD-F7A7-495c-AA84-74FEBCD07934} DEFINE_GUID(GUID_NV2BUDDY_BUS_INTERFACE, 0x9671f9bd, 0xf7a7, 0x495c, 0xaa, 0x84, 0x74, 0xfe, 0xbc, 0xd0, 0x79, 0x34);
Producer Step 2: Define a custom INTERFACE structure
In our driver, we’d like to let other drivers directly call a routine to write to our device. Thus, for version one of our Bus Interface we’ll provide a single write routine. Our driver entirely controls the function prototype of our write routine, thus we can require the consumer to pass any parameters that we wish. In our case, we’ll be sure to have the consumer pass the INTERFACE structure back to us as the first parameter. This allows us to retrieve the Context member that we supplied the consumer when they queried for the Bus Interface. The full definition of our custom interface structure is:
typedef NTSTATUS (*PNV2BUDDY_WRITE)( _In_ PINTERFACE InterfaceHeader, _In_ PVOID WriteBuffer, _In_ size_t WriteBufferLength, _Out_ size_t *BytesWritten ); typedef struct _NV2BUDDY_BUS_INTERFACE { // // Standard interface header, must be present // INTERFACE InterfaceHeader; // // Our driver supplied routines // PNV2BUDDY_WRITE Nv2BuddyWrite; }NV2BUDDY_BUS_INTERFACE, *PNV2BUDDY_BUS_INTERFACE; #define NV2BUDDY_BUS_INTERFACE_VERSION 1
Producer Step 3: Initialize a WDF_QUERY_INTERFACE_ CONFIG structure
We initialize our structure by calling WDF_QUERY_INTERFACE_ CONFIG_INIT, which requires a pointer to our interface structure as well as our interface GUID. We already have our interface GUID defined in a header, so we just need to define and initialize a NV2BUDDY_BUS_INTERFACE structure.
Note that initializing this structure comes in two phases. First, we’ll initialize the common, O/S-defined header. For our interface’s Context member we’ll supply a handle to our WDFDEVICE object. We’ll also use the WDF-supplied dummy routines for our reference counting needs (see below):
NV2BUDDY_BUS_INTERFACE busInterface; PINTERFACE interfaceHeader; // // Set up the common interface header // interfaceHeader = &busInterface.InterfaceHeader; interfaceHeader->Size = sizeof(NV2BUDDY_BUS_INTERFACE); interfaceHeader->Version = NV2BUDDY_BUS_INTERFACE_VERSION; interfaceHeader->Context = (PVOID)device; // // We don't pay any particular attention to the reference // counting of this interface, but we MUST specify routines for // it. Luckily the framework provides dummy routines // interfaceHeader->InterfaceReference = WdfDeviceInterfaceReferenceNoOp; interfaceHeader->InterfaceDereference = WdfDeviceInterfaceDereferenceNoOp;
Next, we’ll initialize the driver-specific portion of the interface structure. For this we’ll simply fill in the write routine to be an existing routine in our driver:
// // Now we can fill in our bus interface callbacks // busInterface.Nv2BuddyWrite = Nv2BuddyInterfaceWrite;
We’re now ready to initialize a WDF_QUERY_INTERFACE_ CONFIG structure with WDF_QUERY_INTERFACE_CONFIG_INIT:
WDF_QUERY_INTERFACE_CONFIG queryInterfaceConfig; WDF_QUERY_INTERFACE_CONFIG_INIT(&queryInterfaceConfig, interfaceHeader, &GUID_NV2BUDDY_BUS_INTERFACE, WDF_NO_EVENT_CALLBACK);
Producer Step 4: Call WdfDeviceAddQueryInterface
Now for the easy part. We simply need to call WdfDeviceAddQueryInterface to associate the interface with our WDFDEVICE. Again, remember that the typeof device does not matter, it could be a function, filter, or physical device:
// // Add the interface! // status = WdfDeviceAddQueryInterface(device, &queryInterfaceConfig); if (!NT_SUCCESS(status)) { #if DBG DbgPrint("WdfDeviceAddQueryInterface failed 0x%0x\n", status); #endif return(status); }
Consuming a Bus Interface in KMDF
On to the consumer! For the consumer, we ask the Framework to query an interface for us. When we do this we can either query for an interface within our own stack with WdfFdoQueryForInterface or within a different stack with WdfIoTargetQueryForInterface.
The WdfFdoQueryForInterface method (below) queries the current device stack for an interface that we identify via GUID. The interface query is sent to the top of the device stack, thus it’s possible to retrieve a Bus Interface of a device above the given device.
_Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) NTSTATUS WdfFdoQueryForInterface( _In_ WDFDEVICE Fdo, _In_ LPCGUID InterfaceType, _Out_ PINTERFACE Interface, _In_ USHORT Size, _In_ USHORT Version, _In_opt_ PVOID InterfaceSpecificData );
The WdfIoTargetQueryForInterface method (below) performs the exact same operation, though, as the name implies, it requires a handle to a Remote I/O Target representing a device in a different device stack.
_Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) NTSTATUS WdfIoTargetQueryForInterface( _In_ WDFIOTARGET IoTarget, _In_ LPCGUID InterfaceType, _Out_ PINTERFACE Interface, _In_ USHORT Size, _In_ USHORT Version, _In_opt_ PVOID InterfaceSpecificData );
Each of these APIs requires a size and version for the Bus Interface to be queried, which the Framework will use to validate that the consumer is requesting a version of the interface that is supported by the producer. They also take an optional InterfaceSpecificData parameter that can be used to pass information about the requested interface to the producer. For most Bus Interfaces this member will be NULL, but it may be something that could be useful in your drivers. Note that the producer must register an EvtDeviceProcessQueryInterface Request event processing callback to receive this parameter.
Once the consumer has successfully called either of these APIs, it can begin using the returned interface structure to call into the producer. We can see an example of querying and using a remote Bus Interface:
NV2BUDDY_BUS_INTERFACE buddyBusInterface; status = WdfIoTargetQueryForInterface( DevContext->BuddyTarget, &GUID_NV2BUDDY_BUS_INTERFACE, (PINTERFACE)&buddyBusInterface, sizeof(NV2BUDDY_BUS_INTERFACE), NV2BUDDY_BUS_INTERFACE_VERSION, NULL); if (!NT_SUCCESS(status)){ #if DBG DbgPrint("WdfIoTargetQueryForInterface failed 0x%0x\n", status); #endif return(status); } // // Call the write routine! // status = (*buddyBusInterface.Nv2BuddyWrite)( &buddyBusInterface.InterfaceHeader, inputBuffer, inputBufferLength, &bytesWritten);
When the consumer has finished using the interface, it must be sure to call the InterfaceDereference member of the returned INTERFACE structure. This is the signal to the producer that the consumer has finished using the interface. As a debugging aid, it’s recommended to zero the interface structure after it has been released. This will trigger an immediate bugcheck if the consumer inadvertently uses the interface after it has been released. The figure below demonstrates releasing the interface:
PNV2BUDDY_BUS_INTERFACE buddyBusInterface; PINTERFACE interfaceHeader; buddyBusInterface = &DevContext->BuddyBusInterface; interfaceHeader = &buddyBusInterface->InterfaceHeader; // // Deref the interface! // (*interfaceHeader->InterfaceDereference)(interfaceHeader->Context); // // And zero it for debugging purposes // RtlZeroMemory(buddyBusInterface, sizeof(NV2BUDDY_BUS_INTERFACE));
If the consumer is taking on the responsibility for ensuring the producer does not unload, at this point the consumer might also release their reference on the producer (e.g. by closing a Remote I/O Target to the device).
Bi-Directional Bus Interfaces
Up to this point we’ve only been discussing a strict producer and consumer relationship: the producer provides a structure, the consumer retrieves the structure and calls into the producer. If you’re using WDF, by default this is the onlybehavior you can achieve. The Framework will copy the producer’s custom interface structure into the consumer’s output buffer, resulting in the consumer receiving a private copy of the interface. The contents of the consumer’s buffer are ignored on input, and any subsequent modifications made to the buffer are seen only by the consumer.
To override this behavior, the producer may set the ImportInterface member of the WDF_QUERY_INTERFACE_ CONFIG structure to TRUE. That’s a little strange, but what it means is the producer may now import members of the custom interface buffer that the consumer provides. If this member is TRUE, the producer must also specify an EvtDeviceProcessQuery InterfaceRequest event processing callback to receive the consumer’s buffer. During this callback the producer may read from the consumer’s interface structure, write to the consumer’s interface structure, or both.
Conclusion
We continue to find Bus Interfaces useful here at OSR and hopefully this article convinced you to take a first (or second) look. The WDF support makes them easy to produce and consume, though please keep in mind the issue of teardown if you’re not simply using an interface provided by your PDO.