Last reviewed and updated: 10 August 2020
If you’ve been paying attention to the driver space at all since the release of Windows 8, you’ve probably noticed the term Universal Driver. What you may not know is what it means, why you should care, or how the Universal Driver concept is evolving, as the Windows platform and systems it supports evolve.
The Road to OneCore
Back in the days before Windows 8, Microsoft began the task of componentizing Windows both at the OS level and the level of support APIs. It was a long and difficult journey. I’ll spare you the details, but an actual, componentized, version of Windows finally made its appearance as part of Win10 1607 (RS1, the Anniversary Update). The componentized OS is referred to as OneCore, and the componentized application support is referred to as OneCoreUAP (Universal Application Platform).
What makes this interesting to driver writers is that, for the first time, there is now a single core Windows component that runs on all manner of platforms: Desktops, Servers, tablets, IoT appliances, Surface Hub, Xbox, and even phones. If you could, ah, find a Windows Phone. But that’s a different story.
Because of Windows’ strong architectural underpinnings, devs can write a single driver that will potentially support a vast array of different hardware. No re-writes, no conditional code. Just retarget, rebuild, and you’re good to go.
Sound good? Let’s talk about how you can get there.
Universal Driver DDIs
Universal Drivers can be either KMDF, WDM, or UMDF 2 drivers. The primary constraint to writing a Universal Driver using one of these models is that you must restrict the functions your driver calls to those that are part of OneCore (for kernel-mode drivers) or OneCoreUAP (for user-mode drivers). These Device Driver Interfaces (DDIs) and APIs are identified in the MSDN docs as being supported by the “Universal” target platform. See Figure 1 for an example.
For kernel-mode drivers, sticking to the universal DDIs is easy. In fact, it’s pretty hard to find a DDI that’s not Universal. When you do find one, it’s probably not a function you want to call (or should call) anyways. For example, when’s the last time you called KeGetCurrentProcessorNumber, eh? Well, it hasn’t worked correctly since the introduction of Windows 7 (with the introduction of processor groups). Anyhow, it’s a DDI that is not Universal. Interestingly enough, even lots of the DDIs that were long-ago relegated to NTDDK.H are part of the Universal DDI set. For example, MmIsAddressValid is one of those DDIs that doesn’t do what most people think it does, and was moved into NTDDK.H back in the stone age. But, it’s a Universal DDI. Yay. I think.
The MSDN docs list a few surprising DDI families as not being Universal, such as the Remove Lock set of DDIs (IoAcquireRemoveLock, IoReleaseRemoveLockAndWait, and their friends). However, the good news is that MSDN appears to be wrong about these. There’s a file that’s part of the WDK that lists (in XML, for your convenience) all the Universal APIs and DDIs. That file is:
C:\Program Files (x86)\Windows Kits\10\build\universalDDIs\<arch>\UniversalDDIs.xml
In the case of the Remove Lock DDIs, the functions appear in the above XML file even though they’re listed in MSDN as not being Universal. The XML file wins. These functions are indeed part of the Universal DDI set.
For UMDF 2 drivers, the difficulty of sticking to the Universal set of functions depends on how much Win32 you mix into your user-mode driver. If you stick to the WDF DDIs and a few supporting functions, you’ll have no problems at all. But the more “application-type code” you have in your driver, the more likely it is that you’re going to have to choose alternate APIs or re-write your code to work in a different way.
Luckily, you don’t have to guess, or rely on MSDN, or even look through a long XML file, to check to see that you’re only calling Universal DDIs. There’s a utility, ApiValidator, that’s part of the WDK that you can elect to run as part of your build process. This utility checks to ensure that everything in your Solution is appropriately Universal (See Figure 2).
To prove that creating a kernel-mode driver that sticks to the Universal set of DDIs is really as easy as I claim it is, I put my own code to the test. I rebuilt a reasonably complex KMDF device driver that I’ve been working on (two-way simultaneous DMA, several PIO type operations via more than a dozen IOCTLs, about 30 source files, and more than 10K NCSLs) that targets Win7 and later as a Universal Driver for Win10. There were no calls to any DDIs outside of the Universal DDI set.
But Wait… There’s More: Declarative Install
Building a driver that conforms to the Universal DDI set is good. It gets you most of the way towards true universal compatibility. But it’s not all you need to do.
Let’s say you’ve written a pretty nice SPB driver and you’ve successfully targeted the Universal DDI set. Now, let’s say, you want to install it on a little single-board computer. Maybe it’s a Snapdragon 617 system, maybe it’s a board with an Apollo Lake processor. But, in any case, the system is running Windows IoT Core.
You might have a great driver, but users have to be able to install it. Windows IoT core doesn’t have a standard Windows Explorer interface. In fact, it can run entirely headless. That means there can’t be any installer MSI to run and there can’t be a co-installer that needs to be executed. You need what’s referred to as a Universal INF file that’s restricted to “Declarative Driver Installation.”
Declarative Install refers to the ability to install drivers in an entirely offline environment, with no GUI interaction. It allows only a limited number of specific, definitive, and unambiguous operations to install the driver. Microsoft claims that not only does this make it possible to install drivers offline, but it also increases the predictability and supportability of driver installations. If that’s true, that’d be great, because, in my experience, driver installs either just work or they’re a complete pain in the ass. They never seem to be anything in between. Refer to the MSDN docs on Universal INF files for more information.
All is not puppies and rainbows, however. While I’m all for making driver installation more predictable, and allowing drivers to be installed offline, there are one or two things about Universal INF files that I find difficult to accept. The most significant of these is that the ClassInstall32 section is not allowed in Universal INFs. This means that an IHV/OEM cannot install their driver into a unique class that fits their environment. Not being able to use a custom install class also restricts you from defining a default protection on the Device Objects in your devnode. To me, this is not a good thing. I don’t want my image scanner or cochlear implant or missile launcher to be lumped into some other random device class. I want my unique devices to show up in their own unique classes. I’m hoping Microsoft re-thinks this restriction, because to me it’s just not acceptable.
Oh, you might be thinking, “If I can’t use a co-installer when I install my driver, how will I be able to tailor the installation to a particular device/platform/environment/system?” Well, there’s an answer for that too. It’s called Componentized Installation.
Componentized Installation allows you to divide your driver INF file into multiple pieces. You create a Base INF, that supports the basic functioning of your driver. Then you create one or more Extension INFs that can tailor or configure your driver for a particular system make, model, or version.
The key to this is that there’s a single INF for all systems that will always get the basic functionality of the device working. The Extension INF(s) can then do any specialized configuration that your driver might require for a particular system or platform. Say, you have special branding that needs to be applied for a given OEM. Or, you want to enable certain value-added functions on particular models of devices that you support. You can accomplish all this from your Extension INF. You write an Extension INF for each unique system type or flavor that you want to support. During the install process, Windows invokes the right INF to do your tailoring. What’s nice about this is that the Base INF and the Extension INF can be updated (and distributed via WU) separately. So you, dear driver writer, can get your Base INF working, and your OEM customer(s) can at some later point work through the process of getting their Extension INF set up the way they want.
Componentized Installation and Extension INFs are explained in more detail in the MSDN docs for Extension INFs.
And Then There Are… Hardware Support Apps
Have you heard of Windows 10 S? That’s the version of Windows that’s “locked down” and only allows you to run apps from the Store. That means that only UWP – not Win32 – apps are supported. Regardless of what you think about this, Microsoft says they’re going to sell these systems. And, if I had to guess, there’ll be other systems in the future that will have the same restrictions.
Suppose you currently have a Device Manager property sheet, or Control Panel Applet, that’s used to manage your device. That’s not going to work very well on a system that doesn’t support the Win32 API, right? So, the final part of the Universal Driver picture is Hardware Support Apps that are written as UWP apps. Of course, that’s not nearly as simple as it might seem at first blush. UWP has some pretty significant limitations: No IOCTLs, no WMI, no RPC. About the only cool technology that you can use in a UWP app is ETW.
In place of device-specific IOCTLs, UWP offers something called Custom Capabilities. These are specific functions your driver authorizes to specific applications. And, in this way, everything is supposed to be kept safe.
I’m not convinced. I’m not a fan of the whole UWP programming model in general (I’m a device guy… I don’t like being told what I can and cannot do). But I admit it: I really haven’t spent much time investigating the whole issue of Custom Capabilities for UWP apps. Maybe it’s not as bad as I think. I’ll look into it, and write an article that describes what I find in a future issue of The NT Insider. In the meantime, you can read (and weep) more about Custom Capabilities for Hardware Support Apps in MSDN.
Note that there’s an interesting side-effect of making a support app a UWP app. That app can now be pulled-down during the install (and also during future updates) from the Windows Store. You can also, optionally, pre-populate the app onto a specific install image using DISM. But in either case, the app will no longer be part of what we think of today as the traditional installer package. This does sound to me like it might make servicing easier.
Gimmie a D! Gimmie a C! Gimmie a H! Gimmie a U!
What’s That Spell?
It spells “DCHU.” No, you can’t pronounce it. It’s stands for:
- Declarative – You use a Declarative INF file
- Componentized – You separate your INF into a Base INF and zero or more Extension INFs
- Hardware Support Apps – Written at a UWP app, of course
- Universal – Your driver (and INF) is restricted to the Universal feature set
Together, Microsoft describes these as the Universal Driver Pillars. See the lovely artwork I stole from somebody’s WinHEC deck in Figure 3.
For your driver to be truly able to run on “any Windows platform” you need to embrace all four of the DCHU design principles.
Universal DDIs – Only Part of the Story
Now, hopefully, you see that writing a driver that’s restricted to the Universal DDIs is straight forward, but it’s also only part of the story. If you truly want your driver to be installable on any Windows system (now and in the future) you’re going to have to become DCHU compliant. Of course, how important any of this is to you will depend greatly on the type of drivers you write and the types of hardware you support.
Be aware that there are rumblings that DCHU compliance will become required for certain classes of drivers in the not too distant future. While that remains to be seen, at least the direction that Microsoft wants us to move is clear.