Implementing a Driver Namespace
September 15, 2003
Walter Oney
Copyright © 2003 by Walter Oney. All rights reserved
On occasion, your hardware has more than one functional unit that applications need to access. For example, you might have a USB device with two bulk output endpoints. You might want to allow an application to open a handle to just one of the endpoints. Or, you might have a multifunction device that doesn't conform to the bus standard (for whatever bus your device plugs into), and you might want to let applications access just one of the functions.
One possible solution is to require applications to specify which endpoint or function they want to access as an argument to an IOCTL of some kind. This solution is workable, but the resulting application code can look pretty baroque -- think about every call needing a parameter structure that contains a member to indicate the target for the operation. Furthermore, this design rules out using ReadFile or WriteFile to read or write data.
A more elegant solution is to allow applications to open function-specific handles using a namespace that your driver implements. That is, the application's call to CreateFile specifies which function it wants to access. To see how this might work from the application side, suppose you're writing an MFC application and that you've already determined the symbolic name of the device (linkname in the following code fragments). You might, for example, have used SetupDiXxx function calls to enumerate instances of a device interface that your device exports using IoRegisterDeviceInterface. An application could open a handle to one of your functions like this:
CString linkname;
HANDLE hFred = CreateFile(linkname + _T("\\Fred"), . . .);
A different application could open a handle to another function like this:
CString linkname;
HANDLE hBarney = CreateFile(linkname + _T("\\Barney"), . . .);
I'll show you how to support this use of CreateFile. In summary, I'll show you how to:
To understand how a driver namespace works, you need to know some details about the kernel Object Manager. A good place to get lots of information about the Object Manager is the current edition of Inside Windows Xxx by David Solomon and Mark Russinovich. But here are the basics.
The first argument to CreateFile will be of the form \\.\FlintstoneDevice0\Fred or \\.\FlintstoneDevice0\Barney. The "FlintstoneDevice0" part of this name comes from a call to IoCreateSymbolicLink that your AddDevice function makes:
UNICODE_STRING devname;
RtlInitUnicodeString(&devname, L"\\Device\\FLINTSTONE0");
PDEVICE_OBJECT fdo;
IoCreateDevice(pdo, sizeof(DEVICE_EXTENSION), &devname, FILE_DEVICE_UNKNOWN,
FILE_SECURE_OPEN, FALSE, &fdo);
UNICODE_STRING linkname;
RtlInitUnicodeString(&linkname,
L"\\DosDevices\\FlintstoneDevice0");
IoCreateSymbolicLink(&linkname, &devname);
Of course, the preferred way to name your device is to call IoRegisterDeviceInterface in your AddDevice routine:
IoRegisterDeviceInterface(pdo, &GUID_DEVINTERFACE_FLINTSTONE, NULL, &pdx->InterfaceName);
When you do this, the I/O Manager internally creates a symbolic link that points to the PDO. User-mode calls to CreateFile use that symbolic link name to open handles to the device. The name is even more unguessable than "Rumpelstiltskin", though, and I didn't want to clutter this discussion. That's why I chose to use the older NT-4 style of device naming.
To return to the example, the user-mode call to CreateFile turns into a kernel-mode call to the native API function NtCreateFile, which, in turn, calls the Object Manager to locate the object named \\.\FlintstoneDevice0\Fred. To simplify matters a bit, the object manager replaces "\\." with "\DosDevices" and starts parsing the name "\DosDevices\FlintstoneDevice0\Fred". DosDevices is the name of a directory at the top level of the kernel namespace.1 FlintstoneDevice0 is the name of a symbolic link object within DosDevices. When the Object Manager reaches that object, it substitutes the name of the link's target for that portion of the pathname it's already parsed, yielding \Device\FLINTSTONE0\Fred.
So, now the Object Manager looks in the top-level Device directory to find the FLINTSTONE0 object. It will call the "open method" routine associated with DEVICE_OBJECT objects. The open method will create an IRP_MJ_CREATE request and send it to the device driver that owns the device object. The as-yet unparsed portion of the file name will appear in the FileName member of the FILE_OBJECT, as illustrated in this picture:
To find the unparsed portion of the name, your code might read like this:
NTSTATUS DispatchCreate(PDEVICE_OBJECT fdo,
PIRP Irp)
{
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
PFILE_OBJECT fop = stack->FileObject;
PUNICODE_STRING filename = &fop->FileName;
. . .
}
At this point, you should be on familiar ground after years of programming in C. If your namespace will contain just two entries (Fred and Barney), you could continue along these lines:
. . .
UNICODE_STRING FredName;
RtlInitUnicodeString(&FredName, L"\\Fred");
UNICODE_STRING BarneyName;
RtlInitUnicodeString(&BarneyName, L"\\Barney");
BOOLEAN bIsFred = RtlEqualUnicodeString(filename, &FredName, TRUE);
BOOLEAN bIsBarney = RtlEqualUnicodeString(filename, &BarneyName,
TRUE);
. . .
(Note that the TRUE argument in the two calls to RtlEqualUnicodeString specifies a case-insensitive comparison.)
By specifying additional name components beyond the name of your device, an application is able to open a handle for some separately addressable entity supported by your driver. That handle maps to a specific file object, as shown in this figure:
Whenever App-1 makes a call to ReadFile, WriteFile, DeviceIoControl, or CloseHandle, the I/O Manager will send an IRP to your driver with the IO_STACK_LOCATION's FileObject member pointing to the FILE_OBJECT for Fred. Similarly, when App-2 calls one of those APIs, the IRP's stack location will point to the FILE_OBJECT for Barney. It's not easy in the driver dispatch routine to tell which FILE_OBJECT is which, of course. What is easy, however, is for you to remember some private information when you initially process the IRP_MJ_CREATE. Suppose, therefore, that you define a per-handle data structure like this one:
typedef struct _HANDLE_INFO {
BOOLEAN bIsFred;
BOOLEAN bIsBarney;
. . .
} HANDLE_INFO, *PHANDLE_INFO;
In your DispatchCreate function, you create an instance of this per-handle structure . . .
NTSTATUS DispatchCreate(. . .)
{
. . .
PHANDLE_INFO phi = (PHANDLE_INFO) ExAllocatePool(NonPagedPool,
sizeof(HANDLE_INFO));
RtlZeroMemory(phi, sizeof(*phi));
phi->bIsFred = bIsFred;
phi->bIsBarney = bIsBarney;
. . .
}
What do you do with this structure? You save a pointer to it in the FILE_OBJECT:
NTSTATUS DispatchCreate(. . .)
{
. . .
fop->FsContext = (PVOID) phi;
. . .
}
Then, in every other dispatch routine, you extract the HANDLE_INFO pointer from the IRP's file object:
NTSTATUS DispatchSomething(PDEVICE_OBJECT
fdo, PIRP Irp)
{
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
PFILE_OBJECT fop = stack->FileObject;
PHANDLE_INFO phi = (PHANDLE_INF0) fop->FsContext;
. . .
}
You release the memory when you handle IRP_MJ_CLOSE:
NTSTATUS DispatchClosePDEVICE_OBJECT fdo,
PIRP Irp)
{
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
PFILE_OBJECT fop = stack->FileObject;
PHANDLE_INFO phi = (PHANDLE_INF0) fop->FsContext;
ExFreePool(phi);
. . .
}
The FsContext field (and the similar FsContext2 field) are there for use by the function driver. You use this to keep a pointer to "your stuff" as it relates to a particular open handle.
Walter Oney is a freelance driver programmer, seminar leader, and author based in Boston, Massachusetts. You can reach him by e-mail at waltoney@oneysoft.com. Information about the Walter Oney Software seminar series, and other services, is available online at http://www.oneysoft.com.
1 -- Starting with Windows XP, the kernel namespace is more complicated than this discussion would imply. Read Solomon & Russinovich, or take a look at page 72 in the 2d edition of my WDM book if you want to know about the details. But don't get bogged down in the fact that different sessions have private "DosDevices" directories that the Object Manager will search before searching the global one.