Introduction
The Windows Operating System provides quite a few inter-process communication (IPC) APIs. Most widely known and used are:
- Windows Sockets
WM_COPYDATA
message- DDE (mostly deprecated)
- Named pipes
- Mailslots
- Remote Procedure Call (RPC)
- Distributed COM (DCOM)
In addition, a lot of "custom" IPC protocols have been developed, which work either on top of one of the above, or use some other method (like shared memory or files).
The boost C++ library also has its own IPC library, which is very flexible, but a bit hard to use and requires you to have binary dependency on several boost libraries (that is,
it is not header only).
A problem with most standard Windows IPC APIs is that they are very low-level. They are generally focused on delivering a simple byte stream from one endpoint to another.
When you try to use them in your own code, you often end up developing some kind of high-level wrapper. Those APIs that are designed to be high-level often have
their own drawbacks, like the need for system-wide registration (DCOM and RPC).
My goal for this library was to develop a simple high-level component for inter-process communications that would:
- require no additional tools (like MIDL compiler),
- provide native C++ interfaces with automatic native C++ types marshalling to its clients,
- utilize one of the built-in robust OS IPC mechanisms "under the hood",
- require no binary dependency on any library (that is, be a header-only library).
The library has been developed and tested on Visual C++ 2010 SP1. It uses a subset of the new C++11 language standard. This library focuses on Windows development
and thus is Windows platform dependent. It also uses several header-only libraries from boost (tested with 1.47). It has a dependency on ATL, but otherwise does not
require you neither to use ATL in your code nor to link to ATL statically or dynamically. The library supports Windows 2000 and later
Operating Systems (both 32-bit and 64-bit versions).
When to use this library
You should consider using this library when you need to quickly implement inter-process communication in your program.
SHLIPC library vs. Sockets (named pipes, mailslots, etc.)
Compared to simple stream or message IPC mechanisms, SHLIPC provides you a high-level C++ interface with input parameters and result values and automatic message dispatching.
When compared to sockets, it also allows you to specify ACL-based security, because it uses named pipes "under the hood" to actually transfer data.
SHLIPC library vs. RPC or DCOM
You should choose DCOM or RPC when you need complex marshalling or need to perform IPC between several computers. On the other hand, SHLIPC does not need a separate
tool during development or build (like the MIDL compiler) and does not require you to perform system-wide or per-user registration.
A good example of library application (and actually the original goal of development for this library) is to establish high-level communication between a program running
under limited user account and the same program (or a part of it) running under elevated user account.
When a program performs several actions one by one and only a small (maybe optional) subset of these actions require elevated administrator rights, the developer may choose
one of the following ways:
- Mark the whole program as requiring administrative rights. This approach has a security drawback because it greatly increases
the possible attack area for viruses and malware. Moreover, it may be inappropriate if actions requiring administrative rights are optional.
- Install a system-wide service and use IPC to communicate with it. The obvious drawback is the need to install a system-wide service.
The SHLIPC library may be used to perform IPC.
- Register a special COM component and use it via an elevation moniker. Again, the obvious drawback is the need to perform COM component registration on the target system.
- Run a copy of itself (or separate executable) with elevated rights and do IPC with it. The SHLIPC library was developed to simplify adopting this approach.
Sample application
The source code for this article includes the sample application which illustrates how you can use the library. We will be listing parts of the source code
here to illustrate the library usage.
This simple application dumps USN journal IDs for a given list of volumes. The problem is that in order
to get the USN journal ID, we have to open a volume to get its handle. This operation requires elevation. Our sample code will automatically launch a copy of itself elevated
in order to open handles. The elevated helper will then return opened handles to a caller application (which runs as standard user) so it can continue working with volumes.
You run the application like this:
pipetest c: d:
Using the library
First of all, you need to #include
the library's single header:
#include "pipeex.h"
All classes and functions in the library are defined in the pipe_transport
namespace.
Marshalling and adaptation
The SHLIPC library automatically marshals C++ fundamental types (boolean, integer, and floating-point types), STL strings (std::basic_string
),
and collections (random-access containers) (for example, it can automatically marshal std::vector<std::wstring>
). In addition,
simple struct
s containing supported types are also supported.
A struct
must be adapted before it can be used. This is performed using one of boost.Fusion's adaptation macros.
See boost.Fusion documentation for more information.
For example, the following code will define and adapt a structure:
BOOST_FUSION_DEFINE_STRUCT(
(test),CallerInformation,
(DWORD,ProcessId)
(wstring,ProcessName))
The following plain C++ struct
is created:
namespace test
{
struct CallerInformation
{
DWORD ProcessId;
wstring ProcessName;
};
}
Note that types that are already adapted, boost.Fusion sequences are also supported (this includes std::pair
, std::tuple
, boost::tuple
,
and all boost.Fusion native sequences).
Interface declaration
The next thing you need is to define an interface. The interface you define will eventually be converted to a plain abstract C++ class.
To define an interface, you need to define a MACRO
(at a global scope) with the following syntax:
#define IMyInterface_decl (IMyInterface) \
(method_decl) \
(method_decl) \
...
(method_decl)) \
where method_decl is:
(result_type)(method_name)(arg1_type)(arg2_type)...(argN_type)
Note: all parenthesis are mandatory!
result_type
must be one of the supported types described above. It is allowed to return both collections and adapted structures.
void
cannot be used as a return type. Argument types, if specified, must also all conform to the scheme described. Only "one-way",
or "input" parameters are supported. If you need to return some information from a method, use the return type (define a structure, for example).
Our sample application defines the following interface:
#define ITest_decl (ITest) \
((test::CallerInformation)(GetCallerInfo)) \
((std::vector<wstring>)(GetVolumes)) \
((bool)(SetResult)(int)(const std::vector<DWORD_PTR> &)) \
The first method, GetCallerInfo
, takes no parameters and returns our adapted structure, CallerInformation
. The sample application actually
uses only the ProcessId
member, but for illustration purposes, the method returns a structure.
The second method, GetVolumes
, also takes no parameters and returns a list of volumes for which an elevated application needs to open handles.
The last method takes an integer and a vector of integers (actually, values of type HANDLE
). Note that we had to use DWORD_PTR
instead
of HANDLE
because the latter is defined as a pointer. The library intentionally does not allow pointer types because it works only with values.
Creating the server
The next step for us is to create a server and client. The important thing to note here is that the protocol the library establishes is more point-to-point than client-server,
so these terms may be a little confusing. The easiest way for you to choose between whether your program is server or client is the following: the server
is the code that actually implements the defined interface and the client is the code that only calls the interface method's.
You should also consider whether the same executable plays server and client roles or you have separate executables for them. The library allows you to choose either scenario.
Our sample application plays both the roles of a server (when running non-elevated) and a client (when running elevated).
In order to create a server, you need to do the following. First, generate the server (put this line into the global scope):
PIPE_GEN_SERVER(ITest_decl);
Next, define a class that will actually implement your interface:
class MyServer : public ITest
{
public:
virtual test::CallerInformation GetCallerInfo()
{
}
virtual std::vector<std::wstring> GetVolumes()
{
}
virtual bool SetResult(int errors,
const std::vector<DWORD_PTR> &handles)
{
}
};
Create an instance of the pipe_transport::PipeServer
template class:
pipe_transport::PipeServer<ITestServer> server(ITest *pImpl, const std::wstring &pipe_name,
DWORD additional_flags = 0, LPSECURITY_ATTRIBUTES security_attributes = nullptr);
Where,
ITestServer
The name of your interface with "Server" appended at the end.
pImpl
Pointer to an object of type MyServer
that actually implements your interface.
pipe_name
A valid named pipe's name. See rules for naming named pipes in MSDN. A "\\.\" prefix is appended automatically. Both server and client must specify the same pipe name.
It can either be hardcoded in your source code (make sure to use a unique name, for example, a GUID) or generated at run-time (and passed to the client, for example, in a command line).
additional_flags
Additional flags to be passed to the CreateNamedPipe
API function in its dwPipeMode
parameter. For example, you may pass PIPE_REJECT_REMOTE_CLIENTS
if your code runs on Windows Vista or later to forbid connecting from remote computers. This parameter is optional.
security_attributes
An optional pointer to security attributes used to create a named pipe. If nullptr
is specified, default security will be used.
You can also derive your server class from both interface and pipe_transport::PipeServer
, as illustrated in the sample code.
Running the server
After the server instance is created, you can launch it. After it is launched, the server waits for a client. Once the client is connected, the server waits
for incoming messages. When a message is received, its parameters are unpacked and the corresponding server's method is called.
This method does not return until the client disconnects from a server.
server.Operate(DWORD timeout = INFINITE);
timeout
specifies the time, in milliseconds, the server waits for the client to connect. If the client does not connect within the specified time, false
is returned.
Otherwise, true
is returned after the client disconnects.
Creating the client
Creating the client is much simpler than creating the server. First, you need to generate a client (put this line at a global scope):
PIPE_GEN_CLIENT(ITest_decl);
Then define the following variable:
pipe_transport::PipeClient<ITestClient> client;
Again, use the name of your interface with "Client" appended at the end.
Once created, the client must be connected to the running server.
if (client.Connect(const std::wstring &pipe_name, DWORD Timeout = 10000,
const std::wstring &server_name = L"."))
{
}
where
pipe_name
The pipe's name. Must be the same as the one used in creating the server.
Timeout
Time to wait for the server to appear. If server is not found within the specified time interval, false
is returned.
server_name
Optional server name. Remember that the main purpose of this library is to provide IPC for processes running on the same computer. It should nevertheless work for a server
and client running on separate computers, but the library will not be able to recover from network errors.
A client object directly "derives" from your interface. You may use the client
variable to call methods of your interface directly,
as any other C++ method. Parameters will automatically be packaged and sent to the server. The returned value will be unpackaged and returned from the method.
The client's execution blocks until the server's response is received.
test::CallerInformation info = client.GetCallerInfo();
std::vector<std::wstring> volumes = client.GetVolumes();
client.SetResult(nErrors, handles);
Custom marshalling
It is possible to add support for marshalling custom types. Custom marshalling code must "disassemble" custom objects into values of types natively supported
by the library and "assemble" them back. There are two methods.
"Intrusive" method for custom classes
If you want to add marshalling support for your own class and is willing to modify the class definition, you may do the following:
First, add the marshal
and unmarshal
methods to your class:
class MyClass
{
public:
template<typename Writer>
void marshal(Writer &writer) const
{
}
template<typename Reader>
void unmarshal(Reader &reader)
{
}
};
Then add the following code into the pipe_transport::marshal
namespace:
namespace pipe_transport { namespace marshal {
template<>
struct tag<MyClass>
{
typedef stream_tag type;
};
}
}
After that, you may start using objects of the class MyClass
as interface method parameters or return values. When the library needs to marshal or unmarshal
the object's value, it will call its marshal
and unmarshal
member functions.
Non-intrusive method
If you cannot modify the class you want to marshal or is going to marshal a non-class type, you may use this method. Add partial template specialization into the namespace
pipe_transport::marshal
for your type:
namespace pipe_transport { namespace marshal {
template<>
struct Marshaller<MyType>
{
template<typename Writer>
static void write(Writer &writer, const MyType &v)
{
}
template<typename Reader>
static void read(Reader &reader, MyType &v)
{
}
};
The Writer
object provides overloads of operator <<
that marshal any value of supported types as well as any value for which custom marshallers exist.
In addition, it has three overloads of operator ()
:
template<class T>
void operator ()(const T &v);
template<class ConstInterator>
void operator ()(ConstIterator begin, ConstIterator end, std::true_type);
template<class ConstInterator>
void operator ()(ConstIterator begin, ConstIterator end, std::false_type);
The Reader
object provides overloads of operator >>
that unmarshal values of supported types as well as values for which custom marshallers exist.
In addition, it has three overloads of operator ()
:
template<class T>
void operator ()(T &v);
template<class Interator>
void operator ()(Iterator begin, Iterator end, std::true_type);
template<class Interator>
void operator ()(Iterator begin, Iterator end, std::false_type);
Change history