We describe a lightweight C++ RPC implementation in this article. The RPC is intended to fulfill the pretty specific purpose to call driver functions from user mode code. It’s really lightweight and it doesn’t have any advanced features, which many general-purpose RPC implementations have. However, the RPC library (which we intricately named RpcLib) turned out to be very helpful.
- Why RPC?
- Why C++ in driver?
- RPC library: client part
- RPC library: server part
- RPC objects
- RPC server
- RPC library: serialization
- RPC library: exception processing
- RPC library: executing calls
- Code Sample
This is quite a common task to control a kernel-mode driver from a user-mode application or service. But anyone who wrote drivers knows that it’s impossible to simply execute some piece of driver code directly from user mode. To make sending control commands to a driver possible, the operating system provides a mechanism of IO control codes (IOCTL’s). The library described in this article uses IOCTL as underlying transport.
“Aren’t you, guys, complicating things ?” - the reader may ask, - “Why
DeviceIoControl isn’t good enough for you?”. At first look, RPC really may seem an overcomplicated approach. But you can have certain difficulties using naked
DeviceIoControl and IOCTL’s.
The conventional way to send control commands to a driver is to declare an IOCTL for each command. You can look over the Driver to Hide Processes and Files article by Ivan Romanenko and Sergey Popenko or the How to develop a virtual disk for Windows article by Gena Mairanichenko. These articles are the examples of this approach.
The problem is that a real-world driver can have dozens of control codes. It means that the dispatch routine that processes the IOCTLs can be so long that you can get lost there.
Another thing is data transfer. If you need to send some complex data between a driver and an application, packing it into a buffer and unpacking it back can be a kind of difficult and annoying task. RPC has a serialization mechanism that does the dirty work for you.
So, the RPC actually makes complicated things a bit less complicated.
RpcLib was designed to be used in the C++ code and it heavily relies on the C++ features such as templates and exceptions. It currently doesn’t support the drivers written in plain C (because we’ve never actually needed it). Using C++ in the kernel-mode drivers has its pros and cons, but they are not the subject of this article.
Using RpcLib is really simple. Let’s see how it looks from the user mode. When you need to call an RPC function from the kernel, you create the
rpc::RpcCall object on the user-mode side. Then you initialize it with the name of the object registered on the server and with the name of the method that should be called.
Then you pass the input arguments, execute the call and get output arguments. It is pretty similar to the way of how a normal function call happens.
rpc::RpcCall class incapsulates the RPC call. Here is how it looks:
It has a buffer of the specified size (16Kb by default) for the input and output parameters. The
rpc::RpcCall object serializes the RPC object name and the function name and the input parameters into this buffer (serialization is described in the Serialization section). The
Call method sends the data from the buffer via the transport given by caller. After the call execution, the buffer contains output data to be deserialized.
The purpose of transport is to send the RPC call input data to the server and return output data. Normally, to send data from a user-mode application to a driver,
rpc::IoctlTransport is used. It’s simply a wrapper over
Let’s look now, how to write the code to process calls from the user mode. Here is an example of the typical RPC object with one RPC method:
Every RPC function is an object method that takes two arguments: the input buffer and the output buffer. Each buffer is represented by an object of the
rpc::Archive class. Usually, you need to deserialize arguments from the input archive with the
rpc::Unpack function. After the work is completed, you can serialize the result into the output archive with the
rpc::Pack function (serialization is described below). If there is an error, the method should throw an exception. It will be cought by the library, serialzied to the user mode and rethrown there. So, the caller will know that something has gone wrong.
To make an RPC function accessible, you need to register it with the
RPC_FUNCTION macro in the constructor.
Also, you need to register the object itself on the RPC server. It’s an object that dispatches calls between the RPC object. In the code above, the object is being registered in the constructor, but actually, you can do it wherever you want.
To dispatch the RPC calls, you need to write something like this in the function where you process your IOCTL requests:
That’s how to use the library from the kernel mode. As you can see, everything is quite simple. Now, let’s take a look inside the server part of the library.
All RPC objects must implement the
This interface has the method call, which gets the function name (as a string) and the archives for the input and output parameters. It must use the function name to dispatch the call to the right function. It’s a routine task, so we wrote the
rpc::RpcSkelBase class to do this. It is implemented as follows:
All the user has to do is to inherit their object from
rpc::RpcSkelBase and register the RPC function using the
RPC_FUNCTION macro you’ve seen above.
As we mentioned, the RPC server is the object that keeps the list of the RPC objects and dispatches calls between them. It implements the
rpc::IRpcServer interface. It works as follows:
There is a default implementation of this interface in RpcLib.
std::map of the RPC objects. You can register an object simply adding the object to the map. Method process is a little bit more interesting. Client sends to the server some data and expects it to return some result. As you’ve seen in the
rpc::RpcCall description, the two first pieces of data are always object name and function name; then there are the input arguments. The default implementation of the RPC server uses a single buffer for the input and output data.
To transfer the data between client and server we have to store them in a buffer at one side and extract them from the buffer at other side. This is what serialization does. We’ve been avoiding its description, but now it’s time to fill the gap.
There are several solutions for the data serialization in C++ (like Boost.Serialization or Google Protocol Buffers), but none of them can be compiled as a kernel-mode library out of the box. So, it turned out to be easier to write our own serialization for RpcLib, then to port any existing solution into the kernel mode. The serialization is quite simple, but it does the job.
It’s easy to use it. The serialization of
std::string looks as follows:
And here is deserialization:
The first parameter of both
rpc::Unpack is an object of the
rpc::Archive class, which is simply a wrapper over a memory buffer.
The serialization can handle standard C++ types (like bools, integers, chars), some STL containers (vector, string, list, and map), and standard exceptions.
Now let’s take a look under the hood. The implementation of
rpc::Unpack is as follows:
They are template functions that use the
serialize_traits template class, parameterized with type, which should be serialized.
The only thing you need to implement serialization for some type is to declare the
serialize_traits specialization for this type and implement the static methods
Serialization for some commonly used types is already implemented in the library.
One of the goals of the RpcLib creating was the exception support. If a server-side object throws an exception it must be serialized and returned back to the client. Client part of RpcLib must deserialize and rethrow it. Also, as far as possible, the exception should not be sliced, i.e. exception of the exactly same type should be rethrown to the client side.
The simplest and the most obvious way to do this is to select a set of exception types, which the library is going to support, and directly implement the catch for each type on the server and the throw for the each type on the client. But this approach isn’t flexible enough: to support one more exception you have to modify the library code.
To achieve the goal with the descent level of flexibility, we decided to use type lists, which are described in the Modern C++ Design book by Andrey Alexandrescu. RpcLib declares a type list of several default exceptions:
Exception type list is a template parameter for
rpc::RpcServer. To add the support for your own exception processing, you have to declare a type list that contains the type of your exception, for instance, appending this exception type to
typedef rpc::TL::Append<rpc::DefaultExceptions, CustomException>::Result MyExceptions;
CustomException is serializable (i.e.
serialzie_traits<CustomException> exists), you can use this type list as the template parameter for
rpc::ExceptionPacker class is used to process exceptions.It works as follows:
DispatchImpl is a template class that catches exceptions of each type specified in the type list:
The purpose of
UnpackAndThrow, which is also a template class, obviously is deserialize the exception of the correct type and throw it to the client side:
Let’s summarize how the RPC call is executed. Here is the complete diagram of what happens :
1. RpcCall serializes the RPC object name and the RPC function name
2. RpcCall serializes the function input parameters
3. RpcServer deserializes the object and function names
4. RpcServer finds the object in the map of the registered RPC objects and executes the Call method
5. The RPC object finds the function in the map of the registered RPC functions and calls it
6. The function gets the input arguments from the buffer
7. The function puts the output arguments to the buffer
8. RpcCall deserializes the output arguments from the buffer
We’ve created a small example to demonstrate how the RPC works, .To make it a little more interesting than a routine “hello world”, we implemented the obtaining a physical address corresponding to a virtual memory. So, there are two parts: application and driver.
The application can have one of such command line parameters:
--install - registers the driver in the OS
--uninstall - unregisters the driver
If called without parameters, the application interacts with the driver and displays physical addresses for a local variable, a global variable, ntdll.dll and kernel32.dll.
The driver processes calls from the application and uses
MmGetPhysicalAddress to return the result to the client.
.\bin - folder with binary files
.\obj - folder with object files
.\src - folder with source files
|-> .\Application - User-mode application
|-> .\Driver - Kernel-mode driver
|-> .\DrvCppLib - Kernel Library to develop driver in C++.
|-> .\DrvStlPort - Directory with STLPort modified for utilizing in windows drivers.
|-> .\RpcLib - RPC library for calling a driver from user mode (both user and kernel mode parts)
|-> .\RpcLibTest - Unit tests for RpcLib.
1. Install Windows Driver Kit. You can download it from the following link: http://msdn.microsoft.com/en-us/windows/hardware/gg487428.aspx
2. Set the global environment variable "BASEDIR" to the path of installed DDK:
Computer -> Properties -> Advanced -> Environment variables ->System Variables -> New
Like this: BASEDIR -> c:\winddk\7600.16385.1
3. Download and install boost (tested with 1.44 version). You can download it from the link:
4. Set the global environment variable "BOOST_ROOT" to the path of installed boost. (You will have to restart your computer after this)
5. Use Visual Studio 2008 to build the solution.
The library described in this article is an RPC implementation. It helps you to control a kernel-mode driver from a user-mode service or application by simplifying some routine tasks. The library is quite simple and adjusted for a very specific task, but it turned out to be helpful for this task. However, there are some limitations. The library doesn’t support any kind of interface description language (IDL) and the serialization supports quite small number of types. Also, you cannot use the library in the pure C drivers, which also may be considered as a disadvantage.
1. Toby Opferman. Driver Development Part 2: Introduction to Implementing IOCTLs
2. Andrei Alexandrescu. Modern C++ Design: Generic Programming and Design Patterns Applied
3. David Vandevoorde, Nicolai M. Josuttis. C++ Templates: The Complete Guide