Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Simple High-Level Interprocess Communications Library (SHLIPC)

4.75/5 (7 votes)
2 Sep 2011CPOL10 min read 34.5K   1.1K  
A simple high-level IPC library with ability to use native C++ interfaces.

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

C++
#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 structs 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:

C++
BOOST_FUSION_DEFINE_STRUCT(
    (test),CallerInformation,
    (DWORD,ProcessId)
    (wstring,ProcessName))

The following plain C++ struct is created:

C++
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:

C++
#define IMyInterface_decl (IMyInterface) \
    (method_decl) \
    (method_decl) \
    ...
    (method_decl)) \
// end of macro

where method_decl is:

C++
(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:

C++
#define ITest_decl (ITest) \
    ((test::CallerInformation)(GetCallerInfo)) \
    ((std::vector<wstring>)(GetVolumes)) \
    ((bool)(SetResult)(int)(const std::vector<DWORD_PTR> &)) \
// end of macro

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):

C++
PIPE_GEN_SERVER(ITest_decl);

Next, define a class that will actually implement your interface:

C++
class MyServer : public ITest
{
public:
    // implement all methods of ITest here:
    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:

C++
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.

C++
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):

C++
PIPE_GEN_CLIENT(ITest_decl);

Then define the following variable:

C++
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.

C++
if (client.Connect(const std::wstring &pipe_name, DWORD Timeout = 10000, 
                   const std::wstring &server_name = L"."))
{
    // work with client
}   // client automatically disconnects when it goes out of scope

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.

C++
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:

C++
class MyClass
{
    // ...
public:    
    template<typename Writer>
    void marshal(Writer &writer) const
    {
        // use writer's operator << to marshal your class
    }

    template<typename Reader>
    void unmarshal(Reader &reader)
    {
        // use reader's operator >> to unmarshal your class
    }
};

Then add the following code into the pipe_transport::marshal namespace:

C++
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:

C++
namespace pipe_transport { namespace marshal {
template<>
struct Marshaller<MyType>
{
    template<typename Writer>
    static void write(Writer &writer, const MyType &v)
    {
        // marshal v into writer    
    }

    template<typename Reader>
    static void read(Reader &reader, MyType &v)
    {
        // unmarshal v from reader
    }
};

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 ():

C++
// Marshal fundamental type
template<class T>
void operator ()(const T &v);

// Marshal continuous random-access range of fundamental types
template<class ConstInterator>
void operator ()(ConstIterator begin, ConstIterator end, std::true_type);

// Marshal range of non-fundamental types
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 ():

C++
// Unmarshal fundamental type
template<class T>
void operator ()(T &v);

// Unmarshal continuous random-access range of fundamental types
template<class Interator>
void operator ()(Iterator begin, Iterator end, std::true_type);

// Unmarshal range of non-fundamental types
template<class Interator>
void operator ()(Iterator begin, Iterator end, std::false_type);

Change history

  • 09/02/2011 - Version 1
    • Original version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)