INTERFACEs can contain both input and output parameters…and not just function pointers

When you look at the documentation for an INTERFACE and IRP_MN_QUERY_INTERFACE, it mentions that the INTERFACE structure is the input provided to the interface provider (set the by the driver querying for the interface) and the remainder of the interface structure is considered output (set by the driver completing the query interface request). What it doesn't says is that the interface can contain more input parameters then just the INTERFACE related fields. Furthermore, the interface can contain additional datatypes other then function pointers. This can be a very powerful design pattern, the querying driver can provide the implementer of the interface its own callbacks and data in addition to acquiring functions and data from the interface implementer.

First, let's take a previous example of REENUMERATE_SELF_INTERFACE_STANDARD which is a classic example of input and output parameters in an interface. In this case the structure reflects what the documentation says. The querying driver fills in the INTERFACE related fields and the bus driver fills in the SurpriseRemoveAndReenumerateSelf field.

 typedef struct _REENUMERATE_SELF_INTERFACE_STANDARD {
    //
    // generic interface header
    //
    USHORT Size; 
    USHORT Version; 
    PVOID Context; 
    PINTERFACE_REFERENCE InterfaceReference; 
    PINTERFACE_DEREFERENCE InterfaceDereference; 
    //
    // Self-reenumeration interface
    //
    PREENUMERATE_SELF SurpriseRemoveAndReenumerateSelf; 
} REENUMERATE_SELF_INTERFACE_STANDARD, *PREENUMERATE_SELF_INTERFACE_STANDARD;

Next, let's look at an interface which has additional input parameters. I will try to put this into a real world context so that it is more concrete then creating fictional interfaces. Let's say that you have a piece of hardware that has multiple functions which share a common set of hardware resources (registers, interrupts, DMA, etc) between all of the functions. The shared hardware resources must be managed by an entity other then each function (which means that you cannot use mf.sys to split the functions apart). In this scenario, you would write a bus driver and enumerate a child PDO stack for every function on the device. But now you have a couple of design problems that you must solve:

  1. How is the parent FDO going to notify a function that something asynchronous occurred (like an interrupt for a specific function)?
  2. How is each child going to know what hardware resources to use?

An interface which contains both input and output parameters can solve both problems. I will first present the data structures and types and then explain them in detail

 typedef PVOID INTERRUPT_CONTEXT;
typedef VOID (*PFN_MY_ACQUIRE_INTERRUPT_LOCK)(INTERRUPT_CONTEXT Context);
typedef VOID (*PFN_MY_RELEASE_INTERRUPT_LOCK)(INTERRUPT_CONTEXT Context);

typedef BOOLEAN (*PFN_FUNCTION_ISR)(PVOID Context);
 
typedef struct _MY_RESOURCES_INTERFACE {
    //
    // generic interface header
    //
    USHORT Size; 
    USHORT Version; 
    PVOID Context; 
    PINTERFACE_REFERENCE InterfaceReference; 
    PINTERFACE_DEREFERENCE InterfaceDereference; 

    //
    // Additional input parameters
    //
    // ISR routine for this function.  The FDO will call this routine when the ISR
    // fires for this function.
    //
    PFN_FUNCTION_ISR IsrRoutine;
    PVOID IsrRoutineContext;

    //
    // Output parameters
    //
    // The start and length of the assigned resources for this function.  These 
    // resources are exclusive to the function and are not shared
    //
    PUCHAR ResourcesStart;
    ULONG ResourcesLength;

    PFN_MY_ACQUIRE_INTERRUPT_LOCK AcquireInterruptLock;
    PFN_MY_RELEASE_INTERRUPT_LOCK ReleaseInterruptLock;
    INTERRUPT_CONTEXT InterruptContxt

} MY_RESOURCES_INTERFACE, *P MY_RESOURCES_INTERFACE;

The "additional" input parameters answer the first question of how is the FDO going to notify the function's stack that an interrupt occurred. The FDO for the function provides the IsrRoutine and IsrRoutineContext values and the bus driver stores these values away in the bus driver FDO context. For instance, the function's FDO code to initialize the structure before sending it down the stack would look like this

 BOOLEAN FunctionIsr(PVOID Context)
{
    WDFDEVICE device = (WDFDEVICE) Context;
    //...
    return TRUE;
}
 MY_RESOURCES_INTERFACE interface;
PDEVICE_CONTEXT pDevContext = GetDevExt(Device);
 
RtlZeroMemory(&interface, sizeof(inteface));
interface.Size = sizeof(interface);
interface.Version = 1;

interface.IsrRoutine = FunctionIsr;
interface.IsrRoutineContext = Device;

After the query interface request has been successfully sent down the stack synchronously, the function's FDO can retrieve the output parameters.

 pDevExt->ResourcesStart = interface.ResourcesStart;
pDevExt->ResourcesLength = interface.ResourcesLength;

pDevExt->AcquireInterruptLock = interface.AcquireInterruptLock;
pDevExt->ReleaseInterruptLock = interface.ReleaseInterruptLock;
pDevExt->InterruptContext = interface.InterruptContext;

The output parameters answer the second question of "how is the function going to know which resources to use? In this example, there is only one set of registers. You could easily add more fields or additional functions to provide a richer interface to retrieve a dynamic set of registers if needed. When the bus driver processes this query interface request in its PDO, it will store the input parameters IsrRoutine and IsrRoutineContext in its extension and assign values to all of the output parameters

 VOID ParentAcquireInterruptLock(INTERRUPT_CONTEXT Context) { WdfInterrupteAcquireLock((WDFINTERRUPT) Context); }
VOID ParentReleaseInterruptLock(INTERRUPT_CONTEXT Context) { WdfInterrupteReleaseLock((WDFINTERRUPT) Context); }
 
 PPDO_EXTENSION pPdoExt = GetPdoExt(Device);
PPARENT_EXTENSION pParentDevExt = GetParentExt(WdfPdoGetParent(Device));
 
 // store input parameters
pParentDevExt->Functions[pPdoExt->FunctionIndex].IsrRoutine = pInterface->IsrRoutine;
pParentDevExt->Functions[pPdoExt->FunctionIndex].IsrRoutineContext = pInterface->IsrRoutineContext;
 
 // set output parameters
pInterface->AcquireInterruptLock = ParentAcquireInterruptLock;
pInterface->ReleaseInterruptLock = ParentReleaseInterruptLock;
pInterface->InterruptContext = (INTERRUPT_CONTEXT) pParentDevExt->Interrupt;

pInterface ->ResourcesStart = pParentExt->Functions[pPdoExt->FunctionIndex].ResourcesStart;
pInterface ->ResourcesLength = pParentExt->Functions[pPdoExt->FunctionIndex].ResourcesLength;

So, hopefully this gives you a glimpse of what you can do with an interface between drivers in a device stack. It can be a very powerful design pattern that can solve some difficult problems. Could you have done this with a more private mechanism like an internal IOCTL? Yes, you could, but it would be more work and it is much easier to use the existing mechanism rather than invent your own.