If you write your driver in C++, I’d like to draw your attention to one of the less visible outputs of the Microsoft Build 2019 conference.
We released a library called “wil”, which you can get here: https://github.com/microsoft/wil
I can’t articulate any single unifying theme of the library; it’s just a collection of useful little wrappers. It won’t change your life, but it might mean you can avoid a few memory leaks.
wil was primarily developed by the Windows shell team for writing usermode code, but the NDIS and Bluetooth teams have contributed some small kernel-specific features. Here’s a few examples of how you can use wil in your kernel driver:
Memory leaks
The wil::unique_any
type works like std::unique_ptr
, except instead of managing a block of memory, it manages a handle to something. It’s designed to be specialized for each type of handle in the OS. So for example, wil::unique_wdf_common_buffer
works as a wrapper around a WDFCOMMONBUFFER
handle. It automatically calls WdfObjectDelete
when it goes out of scope.
NTSTATUS Example()
{
wil::unique_wdf_common_buffer my_buffer;
status = WdfCommonBufferCreate(..., &my_buffer);
if (STATUS_SUCCESS != status)
return status;
if (. . . something else fails . . . )
return STATUS_UNSUCCESSFUL; // no common buffer leak; WdfObjectDelete happens here
return STATUS_SUCCESS;
}
(Note that wil::out_param
can clean up that example even further; C++ is working on standardizing that: https://wg21.link/p1132 .)
You’re encouraged to add your own specializations. So for example, wil doesn’t currently have one for NDIS NBL pools, but you can create one by adding 1 line to your code:
using unique_nbl_pool = wil::unique_any<NDIS_HANDLE, decltype(&::NdisFreeNetBufferListPool), &::NdisFreeNetBufferListPool>;
This creates a type called unique_nbl_pool
that holds an NDIS_HANDLE, and will automatically call NdisFreeNetBufferListPool when it goes out of scope, if the handle value is not nullptr.
wil includes a scope guard. (C++ will get one soon: https://wg21.link/p0052 ) This is a common thing for people to want; you may already have your own scope guard. But if you don’t have one, well now you do:
NTSTATUS Example()
{
my_acquire(&lock);
lock->owner = KeGetCurrentThread();
// everything in this lambda runs when 'guard' goes out of scope
auto guard = wil::scope_exit([&]() {
lock->owner = nullptr;
release(&lock);
});
if ( . . . failure . . .)
return STATUS_UNSUCCESSFUL; // lock is released
return STATUS_SUCCESS;
}
A subset of the STL
Microsoft’s toolchain does not ship a copy of the STL that works in kernel mode. Partly this is because the kernel’s CRT doesn’t support C++ exceptions. (And partly this is because I/O is wildly different in kernel, so you’d have to rewrite the implementation of all the I/O libraries.)
But for kernel developers, wil ships a subset of an STL implementation. To avoid conflicting with the real STL, it’s available under the wistd
namespace. The rule of thumb is that wistd::foo is a drop-in replacement for std::foo.
This subset contains type_traits, so you can do wistd::move, wistd::is_pointer, or even wistd::is_trivially_destructible_v.
The library also provides a clone of std::unique_ptr
. So now you can do:
using unique_nbl = wistd::unique_ptr<NET_BUFFER_LIST, wil::function_deleter<decltype(&NdisFreeNetBufferList), NdisFreeNetBufferList>>;
(Note also the use of wil::function_deleter, to make it slightly more convenient to provide a custom delete functor to unique_ptr. I think there’s some effort to push this into the real STL.)
FAQ: what’s the difference between this use of unique_ptr and the prior example of unique_any? unique_ptr holds a block of memory that you want to reach into. So it makes sense to say nbl->Status = x;
. unique_any holds a handle value that you can’t dereference directly. You can’t say `nblPool->??? = ???;'.
Locking
This version of wil has an early cut of our thinking around a safe lock wrapper around KSPIN_LOCK
, named wil::acquire_kspin_lock
. It’s best explained with an example:
KSPIN_LOCK MyGlobalLock;
NTSTATUS Example()
{
auto lock = wil::acquire_kspin_lock(&MyGlobalLock);
if (. . . something . . .)
return STATUS_UNSUCCESSFUL; // lock is automatically released
return STATUS_SUCCESS; // lock is automatically released
}
Note that, in addition to preventing a leaked lock, you also don’t have to fuss with OldIrql. It’s still there, and you’re still raising to DISPATCH_LEVEL like before, but now it’s happening automatically. (Whether you think this is a bug or a feature is up to your tastes. If you don’t like RAII types that hide IRQL changes, then don’t use wil::acquire_kspin_lock
. For what it’s worth, I’ve been tidying up the implementation of NDIS.sys by hiding a lot of these sorts of mechanics, and I prefer to see only high-level code. But I am aware that there are many excellent developers who want to explicitly see every meaningful state change, including IRQL changes.)
Future
First off, let me point out that this library is used to implement large parts of the OS. There are hundreds of developers here who use it. So unlike, uh, some other things that get tossed onto github, this project is not likely to wither and die tomorrow.
There are, however, only a handful of kernel developers working on the library, so the kernel support has been coming along much slower. I’d like to expand the existing kernel features in depth (more builtin handle types, more types of locks), but also depth (wrappers for KEVENT and LIST_ENTRY, help for paging code, a story for operator new
, generic atomics). I’ve also had to admit failure on some experiments in wrapping UNICODE_STRING and WORK_QUEUE_ITEM, but would love to see someone smarter than me solve these.
One of the most valuable parts of the library – error checking & logging – doesn’t work at all in kernel mode. I am sure it’s possible; we just need someone to sort it out. (I’m not trying to hint that you should do it; we own this and will get to it eventually. But if you do figure it out, we do accept pull requests!) This sort of stuff ends up being really useful in C++ code, since you can get a nice log of where the failures are, without cluttering your code with a bunch of printfs.
NTSTATUS Example()
{
RETURN_IF_NTSTATUS_FAILED(ZwCreateFile(. . . ));
RETURN_IF_NTSTATUS_FAILED_MSG(ZwWriteFile(. . . .), "Failed to write %u bytes", size);
FAIL_FAST_IF_NULL(handle); // bugcheck
LOG_IF_NTSTATUS_FAILED(ZwClose(. . . ));
return STATUS_SUCCESS;
}
Anyway, this code’s another tool for you to write drivers with. Give it a try if you like. If something is broken, open an issue on github or try and catch my attention here. We want it to be useful for people outside of the Windows team too.