Last reviewed and updated: 10 August 2020
Windows 8 introduced many changes to the driver ecosystem: A new OS for Windows Phone, a heightened focus on power efficiency, and support for Simple Peripheral Bus devices being some of the most notable. Less noticed, tucked-in as part of KMDF 1.11 (and conspicuously missing from the summary of changes in the WDK documentation), was the addition of the ability for WDF drivers to define their own custom Object types.
A Quiet Introduction
The fact that this new feature came without a great deal of fanfare from our friends in Redmond wasn’t much of a surprise. Custom Object types are primarily intended for use by Class Extension developers, and several new such Extensions were introduced as part of Windows 8 (among them SPBCx, SerCx, GpioClx). The idea behind these Class Extensions (or “Framework Extensions” as the WDK docs sometimes label them) is that they extend the scope of WDF beyond just its native Objects and provide support for additional devices types and architectures. A Class Extension is implemented as a DLL, and when you build a driver that uses a Class Extension, you link it both with the WDF Framework Library and the Class Extension Library into what the WDK docs somewhat oddly refer to as a “driver triple.”
While we’ve had the ability to create custom WDF Objects for years now, there hasn’t been any discussion about their use or example of how to create them. Until now.
The Need to Extend the Framework
I think we all agree that WDF has taken hold and finally supplanted WDM as the primary interface for writing Windows drivers. As new or unique device architectures are supported by Windows, it makes sense to extend the capabilities of WDF to support these new or specialized device types. This shortens the required “ramp-up” time for driver devs by allowing them to apply already familiar WDF design patterns to the development of drivers for additional types of devices.
The initial way extensions to the Framework were implemented was by changing the core code of the Framework itself. USB support in WDF is the primary example of this approach. USB support in WDF gives us a big pile of USB-specific APIs, and even a unique pair of WDF I/O Target types.
However, adding specific extensions to KMDF and UMDF for every additional device architecture to be supported wasn’t a reasonable long-term plan. First, there are only a limited number of WDF developers, and their job is to keep the Framework running smoothly from release to release. Second, adding support for a new device type typically requires deep knowledge of that device architecture. That means the right group of folks to add that support are the folks who are experts in the device technology, not folks who are experts in KMDF or UMDF internals. Clearly, a way to add support for additional device types without having to alter the mainline WDF code was required. And thus, Class Extensions were born.
Today we have all manner of Class Extensions, including some very clever ones from the network group. But that’s a subject for another article. Let’s turn our attention back to the topic of creating custom Objects.
You Can Create Your Own Object
Regardless of why the facility was created, WDF (both KMDF and UMDF) now allows developers to define their own custom Object types. More precisely, it allows developers to create custom Object types that are based on other, preexisting, WDF Objects types (including the generic WDFOBJECT). It’s almost like deriving a child class from a base class. But in C and not C++.
The beauty of custom WDF Objects is that, regardless of what you call them or how you use them, they are actual native WDF Objects. That means, when properly implemented, they can be used anyplace a WDF Object can be used. For example, you can store a custom Object in a WDFCOLLECTION. A user of your custom Object can allocate one (or more) WDF Contexts that are associated with your Object and, just like native WDF Objects, the Framework will manage the lifetime of that Context.
The differences between your custom Object and the underlying native WDF Object on which its based are:
- Your custom Object can (and almost always will) have its own internal data area. This is like a WDF Context, but it is not accessible by users of your custom Object.
- Your custom Object can support custom methods that you devise, including custom Event Processing Callbacks.
In implementing a custom WDF Object, you choose the native WDF Object that best matches the characteristics that you want your Object to have or the way that you want your Object to be used. For example, if you want your Object to be able to represent an I/O operation and to be able to be stored on a WDFQUEUE, you would base your Object on the native WDFREQUEST. Note that you optionally can expose these capabilities of your custom Object to the eventual user of your custom Object. And, of course, because this is WDF, your custom Object could even represent a group of multiple WDF Objects, with one parented on the other. Bottom line, the system is extremely flexible.
No question, custom WDF Objects can be both useful and fun. Later, I’ll talk more about when I think it’s most appropriate to create custom WDF Objects. But, for now, suffice it to say that properly implementing a custom WDF Object takes a reasonable amount of effort and is probably something you only want to do if you plan to create a custom Object type that you’d want to re-use across multiple projects. Sort of like a Class Extension.
How To “Roll Your Own”
I tend to think of the process of defining custom WDF Objects as comprising two parts:
1) Declaring the Object and its handle – This provides all the necessary naming and glue for the custom Object.
2) Providing the Custom Infrastructure — These are the macros and functions that you create, following the established WDF design patterns, to support the use and operation of your Object.
To illustrate the process, we’re going to create our own custom WDF Object. By the way, an important best practice to follow is to never name custom Objects starting with WDF. Names starting with WDF are indicative of Object supported directly by the Framework. Thus, we have very unimaginatively named the example custom Object type that we’ll be using in this article OSRCUSTOM.
Step 1: Declaring the Object and Its Handle
Recall that, in WDF, the data type that contains a handle to a WDF Object is the name of that Object type. So, the datatype WDFDEVICE is the handle to a WDF Device Object and the datatype WDFTIMER is the handle to a WDF Timer Object.
Given this naming convention, we implement Step 1 (above) by declaring the handle and data type of our new custom Object:
// // OSR Custom Declaration // DECLARE_HANDLE(OSRCUSTOM); WDF_DECLARE_CUSTOM_TYPE(OSRCUSTOM);
In this code, we declare a WDF Data Type and handle type named OSRCUSTOM. WDF very cleverly takes care of all the necessary declarations and mess for you. And with that, Step 1 “Declaring the Object and its handle”, is complete.
The WDF_DECLARE_CUSTOM_TYPE macro is pretty interesting, if you take the time to look at its definition. While it’s very simple to use, it’s the piece of code that defines a clever little function that you’ll use to instantiate your custom Object later on when you call WdfObjectAddCustomTypeWithData. But, more about that later. Let’s not get too far ahead of ourselves.
The DECLARE_HANDLE directive allows users who are going to use our Object to declare and use Object handle variables in the usual WDF way (shown here as part of an EvtIoDeviceControl Event Processing Callback):
VOID NothingEvtDeviceControl(WDFQUEUE Queue, WDFREQUEST Request, size_t OutputBufferLength, size_t InputBufferLength, ULONG IoControlCode) { PNOTHING_DEVICE_CONTEXT devContext; WDFDEVICE device; NTSTATUS status; OSRCUSTOM custom; device = WdfIoQueueGetDevice(Queue);
Easy, right? Two macro invocations and we’re done! Well, yes, at least so far. But Step 2 is where the real work takes place.
Step 2: Adding the Infrastructure
In Step 2 we add the infrastructure, or what I like to refer to as the “WDF Gloss”, to your custom Object type. As part of this step, you create macros and functions that implement and facilitate common WDF design patterns, and that allow your Object to do useful things.
Let’s continue with the development of our OSRCUSTOM Object. Because it’s just an example, the OSRCUSTOM Object doesn’t implement anything that’s actually useful. It implements a counter. Using the Object you can:
- Provide an initial value of the “Counter” property when you instantiate the Object.
- Specify an Event Processing Callback (when you instantiate the Object) that is invoked whenever the Object’s Counter property is incremented to an integer multiple of ten. We call this the Object’s EvtOsrCustomInformCount Event Processing Callback.
- Increment the Object’s Counter property (by invoking a method)
- Get the current value of the Object’s Counter property (by invoking a method)
We base our OSRCUSTOM Object on the generic WDFOBJECT, because there are no other Object-type “features” that we need. But, as mentioned previously, you can base your custom Object on any native WDF Object type your wish. If you wanted to implement some sort of special timer, for example, you could base your custom Object on WDFTIMER.
When crafting your new Object, it’s important to follow as much of the established WDF design pattern as possible. For example, in creating our OSRCUSTOM Object, the user should be able to follow the standard 3 step pattern for WDF Object Instantiation: (1) Specify WDF Object Attributes, (2) Specify Object-specific configuration using a XXX_CONFIG structure, (3) Instantiate the Object using a XxxxCreate function (where Xxxx is the Object name). Of course (or, perhaps I should say, unfortunately), there’s nothing in the process of defining a custom WDF Object that forces you to follow these conventions. But one of the major features of WDF is its consistency. You can leverage that feature by making your custom Object “work” as much like a native WDF Object as possible.
The steps necessary for a user to instantiate our OSRCUSTOM Object should look pretty familiar to any WDF driver developer (See Figure 1, below).
devContext = NothingGetContextFromDevice(device); if (devContext->OsrCustom == nullptr) { OSR_CUSTOM_CONFIG customConfig; OSRCUSTOM newCustom; DbgPrint("Creating new 'Custom'!\n"); OSR_CUSTOM_THING_CONFIG_INIT(&customConfig,22); // Config pointer, Initial Counter value status = OsrCustomCreate(device, &customConfig, WDF_NO_OBJECT_ATTRIBUTES, &newCustom); if (!NT_SUCCESS(status)) { DbgPrint("OsrCustomCreate failed 0x%0x\n", status); goto done; } devContext->OsrCustom = newCustom; goto done; }
The code in Figure 1, checks to see if an OSRCUSTOM Object has already been created, and if it has not it creates one using the standard WDF Object creation pattern. There’s an OSR_CUSTOM_CONFIG structure that’s used as the Object’s configurator. That structure is initialized by a macro named OSR_CUSTOM_CONFIG_INIT. The _INIT macro takes both a pointer to the configurator and a value that will be used to initialize the OSRCUSTOM’s Counter property.
The OSRCUSTOM Object is then instantiated by calling OsrCustomCreate, passing:
- The handle to an associated WDFDEVICE (which will be the parent of the OSRCUSTOM)
- A pointer to the initialized OSR_CUSTOM_CONFIG structure
- An optional pointer to a WDF_OBJECT_ATTRIBUTES structure (which is not supplied in the example)
- A pointer of type OSRCUSTOM into which to return the handle of the newly instantiated Object, if the call to OsrCustomCreate is successful.
The code to support this is simple, but it requires some thought. We can start by looking at how the configurator is handled (Figure 2, below).
typedef struct _OSR_CUSTOM_CONFIG { LONG CounterStartValue; PEVT_OSR_CUSTOM_INFORM_COUNT EvtOsrInformCountTens; } OSR_CUSTOM_CONFIG, *POSR_CUSTOM_CONFIG; VOID FORCEINLINE OSR_CUSTOM_THING_CONFIG_INIT(_Out_ POSR_CUSTOM_CONFIG Config, _In_ LONG CounterStart) { RtlZeroMemory(Config, sizeof(OSR_CUSTOM_CONFIG)); Config->CounterStartValue = CounterStart; }
In Figure 2, we provide the declaration of the OSR_CUSTOM_CONFIG structure, which includes a field that contains the starting value for the Counter, as well as a pointer to an optional EvtOsrInformCount Event Processing Callback. The OSR_CUSTOM_COINFIG_INIT function does the expected initialization of the structure (zeroing it) and fills-in the CounterStartValue field.
The code that implements OsrCustomCreate is a bit more involved, but still… not difficult. You can see this code is Figure 3.
_Must_inspect_result_ _IRQL_requires_max_(DISPATCH_LEVEL) NTSTATUS OsrCustomCreate(_In_ WDFDEVICE Device, _In_ POSR_CUSTOM_CONFIG Config, _In_opt_ PWDF_OBJECT_ATTRIBUTES ObjectAttributes, _Out_ POSRCUSTOM Object) { WDFOBJECT object; NTSTATUS status; PVOID data; POSRCUSTOM_INTERNAL_DATA customData; WDF_OBJECT_ATTRIBUTES objAtts; // If no Object Attributes provided by the user, supply the default // if (ObjectAttributes == WDF_NO_OBJECT_ATTRIBUTES) { WDF_OBJECT_ATTRIBUTES_INIT(&objAtts); ObjectAttributes = &objAtts; } // Override whatever the user specifies for Parent // ObjectAttributes->ParentObject = Device; status = WdfObjectCreate(ObjectAttributes,&object); if (!NT_SUCCESS(status)) { DbgPrint("WdfObjectCreate for object failed 0x%0x\n", status); goto done; } data = ExAllocatePoolWithTag(NonPagedPoolNx,sizeof(OSRCUSTOM_INTERNAL_DATA),'crso'); if (data == nullptr) { DbgPrint("ExAllocatePoolWithTag for object data failed\n"); status = STATUS_INSUFFICIENT_RESOURCES; goto done; } customData = (POSRCUSTOM_INTERNAL_DATA)data; RtlZeroMemory(customData, sizeof(OSRCUSTOM_INTERNAL_DATA)); customData->Counter = Config->CounterStartValue; customData->EvtOsrCustomInformCount = Config->EvtOsrCustomInformCount; status = WdfObjectAddCustomTypeWithData(object, OSRCUSTOM, (ULONG_PTR)data, NULL, EvtWdfDestroyCustom); if (!NT_SUCCESS(status)) { DbgPrint("WdfObjectAddCustomTypeWithData for object failed 0x%0x\n", status); goto done; } DbgPrint("New thing created, with Counter Starting value = %u\n", customData->Counter); status = STATUS_SUCCESS; *Object = (OSRCUSTOM)object; done: return status; }
In Figure 3, you can see that we create a WDF_OBJECT_ATTRIBUTES structure if one is not provided by the caller. Because it’s key to make your Object creation process as much like that of a native WDF Object as possible, it’s important for you to follow the usual WDF procedures with respect to Object creation. This means allowing the caller to specify _OBJECT_ATTRIBUTES that will accompany the instance of your Object. This does not, however, mean that you must allow any combination of _OBJECT_ATTRIBUTES that the caller provides. For example, we force the ParentObject field to the handle of the WDFDEVICE that’s passed-in to the creator. It is not unusual in WDF for an Object to require a specific parent. As a side note, it’s interesting that there does not appear to be any function that allows you to check the type of a passed-in WDF Object handle (or to even check if the handle is valid at all). It would be nice to have this, to allow us to validate that the user did indeed pass us a valid handle to a WDFDEVICE.
Using the WDF_OBJECT_ATTRIBUTES structure, we instantiate a generic WDFOBJECT by calling WdfObjectCreate. Again, we could have based our custom Object on any native WDF Object type. And, by so doing, our custom Object would effectively adopt the characteristics of that native Object.
After instantiating the native generic WDFOBJECT, we allocate a private data area that will be stored with our custom Object instance. This private data area (OSRCUSTOM_INTERNAL_DATA, shown in Figure 4) is like a WDF Object Context, but is not accessible by our user. We then initialize the private data area. Finally, we call WdfObjectAddCustomTypeWithData, providing a handle to the native Object type, the data type of my custom Object, a pointer to the private data area that we allocated from pool, and a pointer to an EvtDestroyCallback in which we simply return the custom data area we allocated from pool. You can see the EvtDestroyCallback we created for OSRCUSTOM in Figure 5, below.
typedef struct _OSRCUSTOM_INTERNAL_DATA { LONG Counter; PEVT_OSR_CUSTOM_INFORM_COUNT EvtOsrCustomInformCount; } OSRCUSTOM_INTERNAL_DATA, *POSRCUSTOM_INTERNAL_DATA; VOID EvtWdfDestroyCustom( _In_ WDFOBJECT Object) { PVOID data; POSRCUSTOM_INTERNAL_DATA internalData; data = (PVOID)WdfObjectGetCustomTypeData(Object, OSRCUSTOM); if (data == nullptr) { DbgPrint("WdfObjectGetCustomTypeData for object data failed\n"); goto done; } internalData = (POSRCUSTOM_INTERNAL_DATA)data; DbgPrint("Count at free is %u\n", internalData->Counter); ExFreePool(data); done: return; }
And So It Goes
Now that you’ve provided all the stuff necessary to configure and instantiate your custom Object, all you need to do is provide some methods that operate on or using your Object. Our OSRCUSTOM Object provides two such methods:
- OsrCustomIncrementCount – Increments the Count on the Object
- OsrCustomGetCount – Retrieves the Count on the Object
The code for OsrCustomIncrementCount is shown in Figure 6.
NTSTATUS OsrCustomIncrementCount(OSRCUSTOM Object) { NTSTATUS status; POSRCUSTOM_INTERNAL_DATA customData; customData = (POSRCUSTOM_INTERNAL_DATA)WdfObjectGetCustomTypeData(Object, OSRCUSTOM); if (customData == nullptr) { DbgPrint("WdfObjectGetCustomTypeData for object data failed\n"); status = STATUS_INVALID_ADDRESS; goto done; } InterlockedIncrement(&customData->Counter); DbgPrint("Thing count is now %u\n", customData->Counter); // // If the count has reached another boundary of 10, call the Event // Processing Callback to let the user know. // if ((customData->Counter % 10) == 0) { if (customData->EvtOsrCustomInformCount!= NULL) { (customData->EvtOsrCustomInformCount)(Object); } } status = STATUS_SUCCESS; done: return status; }
There are three (slightly) interesting things to review in Figure 6. First, you can see that we retrieve a pointer to the private data area that’s stored with the instance of our Object using the function WdfObjectGetCustomTypeData. Next, note that we implement the counter increment operation using an interlocked increment operation. We do this because, according to the WDF rules, all WDF Objects are internally thread safe… no other locks are required to keep them sane. Finally, note that we invoke the user-provided EvtOsrCustomInformCount Event Processing Callback if the counter is incremented to a multiple of ten. We do this directly in-line here… you might choose to do this in a more sophisticated manner if required.
Finally, we have the OsrCustomGetCount property function, shown in Figure 7.
LONG OsrCustomGetCount(OSRCUSTOM Object) { NTSTATUS status; POSRCUSTOM_INTERNAL_DATA internalData; internalData =(POSRCUSTOM_INTERNAL_DATA)WdfObjectGetCustomTypeData(Object, OSRCUSTOM); // // GET cannot fail... so... // ASSERT(internalData != nullptr); return internalData->Counter; }
Again, it’s important to remember to follow the standard WDF rules when you implement these functions. Remember “Set” and “Get” functions can never fail, and thus do not return NTSTATUS. We follow that pattern here, returning the value of Count as the output from the function.
But Why?
So now you know how to create custom WDF Object types. Let’s talk a bit about how you can use these and why you might want to create one.
In its most basic form, custom WDF Object types provide a way to derive a unique Object type from a native base WDF Object. Because the Framework implements a C Language interface, we can’t do this through simple inheritance. However, by using the WDF custom Object type mechanism we can create new Objects that have their own, private data areas, their own methods, and even their own Event Processing Callbacks. And, the new Objects that we create will still have all the attributes of the underlying Object type on which we based them. No matter how you look at it, when you create a new WDF Object type, you’re creating a child Object of an existing, base, native WDF Object type.
I’d argue, however, that due to the complexity of the interface and the infrastructure that you need to create to make your Object work like an ordinary WDF Object, simply wanting to casually create your own flavor of WDFREQUEST for use in your custom driver probably is not a good way to use the WDF custom Object capability.
Rather, I’d suggest that the best use of this facility is to create common infrastructural Objects that your team can use across drivers. This might be to support specific device types or architectures, like Microsoft does with Framework Class Extensions. Or it might be to support specific attributes or mechanisms of processing for your device(s). Above all, for me the differentiating factor that determines whether I should create a custom WDF Object is if the specialized Object type that I want to create is generic and useful across multiple drivers. I can’t justify the work, and the required infrastructure, for any other reason.