Windows Driver Foundation Setup and First Steps
Driver development on Windows is one of those things where the learning curve eats people alive if they don't have a clear path. The Windows Driver Foundation—WDF—was Microsoft's attempt to make this less painful. It split into two flavors: KMDF for kernel-mode drivers and UMDF for user-mode drivers. Most people end up in KMDF unless they have a specific reason to run in user mode. The framework handles a lot of boilerplate that used to be either written by hand or ignored until something broke in production. Before you write a single line of driver code, you need the right SDK installed. Grab the Windows Driver Kit from Microsoft. The current version ships with VS 2022 and earlier builds through Visual Studio's installer. Select the "Windows kernel-mode driver and device software development tools" component. If you're targeting Windows 10 and later, that's usually sufficient. Build targeting older versions like Windows 7 or 8.1 requires a separate older WDK. Mixing up SDK versions is a common way to waste half a day. Create a new project in Visual Studio using the Kernel Mode Driver, Function project template. Don't use the Null Driver template unless you want a skeleton with zero functionality—it's more confusion than help for beginners. The function driver template gives you a basic driver entry point, device add routine, and IoDeviceControl handler already wired up. That's where you start modifying.
Developing Drivers With The Windows Driver Foundation
The first thing to understand about WDF is that it changes how you think about the driver model. In old-style WDM, you managed object lifetimes manually. WDF wraps everything in framework objects that handle reference counting and cleanup for you. You still interact with hardware directly when needed, but the common pathways—device creation, file objects, I/O queues—are abstracted away. Here's the essential structure of a basic KMDF driver. You'll see EvtDeviceAdd as the main entry point where the framework calls your code after detecting the hardware. This is where you create the device object, configure queues, and set up your hardware interaction points. The framework handles PnP and power management callbacks separately, which keeps your code organized. Below is a minimal driver that responds to IOCTLs. It's not useful for anything, but it demonstrates the required structure. Every KMDF driver needs at least these pieces.
#include <ntddk.h>
#include <wdf.h>
DRIVER_INITIALIZE DriverEntry;
EVT_WDF_DRIVER_DEVICE_ADD EvtDeviceAdd;
EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL EvtIoDeviceControl;
NTSTATUS DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
NTSTATUS status;
WDF_DRIVER_CONFIG config;
WDF_DRIVER_CONFIG_INIT(&config, EvtDeviceAdd);
status = WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES,
&config, WDF_NO_HANDLE);
return status;
}
NTSTATUS EvtDeviceAdd(
_In_ WDFDRIVER Driver,
_In_ PWDFDEVICE_INIT DeviceInit
)
{
NTSTATUS status;
WDFDEVICE device;
WDF_OBJECT_ATTRIBUTES attributes;
WDF_IO_QUEUE_CONFIG queueConfig;
status = WdfDeviceCreate(&DeviceInit, WDF_NO_OBJECT_ATTRIBUTES, &device);
if (!NT_SUCCESS(status)) return status;
WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&queueConfig, WdfIoQueueDispatchParallel);
queueConfig.EvtIoDeviceControl = EvtIoDeviceControl;
status = WdfIoQueueCreate(device, &queueConfig, WDF_NO_OBJECT_ATTRIBUTES, NULL);
return status;
}
VOID EvtIoDeviceControl(
_In_ WDFQUEUE Queue,
_In_ WDFREQUEST Request,
_In_ size_t OutputBufferLength,
_In_ size_t InputBufferLength,
_In_ ULONG IoctlCode
)
{
NTSTATUS status = STATUS_SUCCESS;
switch (IoctlCode)
{
case IOCTL_CUSTOM_TEST:
// Handle your IOCTL here
break;
default:
status = STATUS_INVALID_DEVICE_REQUEST;
break;
}
WdfRequestComplete(Request, status);
}
Notice that the I/O queue is set to parallel dispatch. That means multiple threads can process requests simultaneously. For a basic driver, this is usually what you want. Sequential dispatch serializes everything through a single thread, which is safer for certain hardware but slows things down considerably. The default queue creation handles most common cases without extra configuration. One thing beginners consistently miss: WdfRequestComplete must be called on every code path in your EvtIoDeviceControl handler. Forgetting this in an error branch causes the request to hang forever. The calling application blocks waiting for a completion that never comes. I've seen this cause entire systems to appear frozen when a user-mode app was stuck on a pending IOCTL. Add a default case that calls WdfRequestComplete with an error status. It's trivial to add and prevents hours of debugging confusion.
Get the Full Details
Build Configuration and Testing
Building a WDF driver requires setting the right project properties. In Visual Studio, open the project properties and navigate to Configuration Properties > General. Set the Platform Toolset to "WindowsKernelModeDriver10.0" or later depending on your target. Under the Driver Settings node, configure the target OS version and the signing options. For development, you don't need a code-signing certificate immediately. Enable test signing mode in Windows using bcdedit /set testsigning on and reboot. This lets you load unsigned drivers. For production builds, you'll need a valid EV code-signing certificate that covers kernel-mode drivers. The process involves purchasing the certificate, signing the .sys file, and ensuring the signature chain is valid before submission to Microsoft for WHQL if you're distributing publicly. Testing a driver requires a test environment. Virtual machines work fine for basic development. Set up a VM with Windows 10 or 11, enable test signing, and install your driver using sc create and sc start. The Driver Verifier is another tool worth knowing about. It stresses your driver by checking for common mistakes like improper IRQL usage, pool allocation violations, and bad lock ordering. Enable it selectively—verifying all drivers on a system is a great way to crash your test machine repeatedly. Use ver /start 1 /driver yourdriver.sys /flags 0x1c to enable a focused set of checks.
I once spent three days tracking down a bug that turned out to be a pool allocation leak. The driver allocated memory using ExAllocatePoolWithTag in the EvtDeviceAdd callback but never freed it. WDF doesn't track raw kernel pool allocations. It only manages its own framework objects. Driver Verifier caught this on the second run with a pool tag violation, but only after I configured it with the POOL_TRACKING flag enabled. That flag wasn't on by default in the standard verification profile.
Common Pitfalls and What the Documentation Doesn't Emphasize
WDF simplifies many things but introduces its own complications. The framework uses a completion model where requests flow through queues and get processed by your callback. This is fundamentally different from the old WDM model where you handled interrupts and DPCs directly with more explicit control. The abstraction is useful until you need to understand what's happening under the hood. One issue that catches people off guard: WDF objects have parent-child relationships that affect lifecycle. If you create a queue as a child of a device, the queue is destroyed when the device is destroyed. But if you create a work item or a timer attached to a queue, and that queue gets deleted first, the framework will destroy those objects too. This can cause use-after-free bugs if your callbacks still reference those objects. Always check the parent relationship when creating framework objects. Another thing the official docs gloss over: IRQL constraints in WDF callbacks. Your EvtDeviceAdd runs at PASSIVE_LEVEL. Your EvtIoDeviceControl also runs at PASSIVE_LEVEL by default. But if you configure a queue for direct dispatch, the callback might run at higher IRQL depending on the requester. If you call a routine that requires PASSIVE_LEVEL from a high-IRQL context, the system raises a bug check. Use KeGetCurrentIrql() in your callbacks during development to verify you're at the expected level. It seems obvious but people skip it.

For hardware interaction, WDF provides helper routines but doesn't replace the need to understand the hardware. If you're writing a driver for a USB device, you still need to implement the USB I/O request packets. WDF wraps the USB stack but your code needs to construct proper URBs. Similarly, for PCI devices, you use WdfFdoInitRegisterHardwareInterrupt to set up interrupt handling, but the actual interrupt service routine logic is yours to write. The framework manages registration and cleanup, not the hardware logic.
UMDF Considerations
UMDF drivers run in user mode, which means crashes don't blue screen the system. This is a significant advantage for certain types of drivers, especially those that interface with user-mode components or need to avoid kernel stability risks. UMDF v2 (available on Windows 8 and later) supports in-process agents that share the loader lock, which simplifies DLL loading but introduces threading constraints. The main tradeoff with UMDF is performance. User-mode to kernel-mode transitions add latency. For drivers that need to process I/O at line speed or handle real-time hardware interrupts, KMDF is the only viable option. UMDF works well for storage virtualization, printer drivers, and other cases where the performance hit is acceptable. I've seen UMDF drivers fail to activate because the DLL hosting the agent wasn't registered correctly. The error message is cryptic—something about the activation class not being found. The fix usually involves ensuring the DLL is in the same directory as the driver .sys file and that the registry entries for the activation class are correct. Check the event log under Application and Services Logs > Microsoft > Windows > DriverFrameworks-UserMode for detailed activation failure reasons.
Practical Workflow
A practical development workflow looks something like this. Write the driver code in Visual Studio. Build it in Debug or Release mode depending on whether you need kernel debugger attachment. Install the driver on the test machine using a setup script that copies the .sys file to System32\Drivers, creates the registry entries under HKLM\SYSTEM\CurrentControlSet\Services, and starts the service. Use a user-mode test application to send IOCTLs and verify behavior. Repeat. For automated testing, consider setting up a build pipeline that compiles the driver on every commit and runs Driver Verifier against it in a VM. This catches regressions early. The Windows Hardware Lab Kit includes tools for automated driver testing, though the setup is more involved than manual testing. The framework itself is mature and stable. KMDF has been around since Windows XP SP2 and has seen steady improvements through Windows 11. The API surface is well-defined and the documentation, while sometimes sparse on edge cases, is generally accurate for common scenarios. The real learning comes from understanding how the framework's abstractions map to actual hardware behavior and kernel mechanics. Once that clicks, developing WDF drivers becomes a matter of applying the right patterns rather than fighting the system.
