Generalising Message Interface

The basic generic message interface may include the following operations:

  • Retrieve the message ID.

  • Read (deserialise) the message contents from raw data in a buffer.

  • Write (serialise) the message contents into a buffer.

  • Calculate the serialisation length of the message.

  • Dispatch the message to an appropriate handling function.

  • Check the validity of the message contents.

There may be multiple cases when not all of the operations stated above are needed for some specific case. For example, some sensor only reports its internal data to the outside world over some I/O link, and doesn't listen to the incoming messages. In this case the read() operation is redundant and its implementation should not take space in the produced binary code. However, the component that resides on the other end of the I/O link requires the opposite functionality, it only consumes data, without producing anything, i.e. write() operation becomes unnecessary.

There must be a way to limit the basic interface to a particular set of functions, when needed.

Also there must be a way to specify:

  • type used to store and report the message ID.

  • type of the read/write iterators

  • endian used in data serialisation.

  • type of the message handling class, which is used in dispatch() functionality.

The best way to support such variety of requirements is to use the variadic templates feature of C++11, which allows having non-fixed number of template parameters.

These parameters have to be parsed and used to define all the required internal functions and types. The common message interface class is expected to be defined like this:

namespace comms
{
template <typename... TOptions>
class Message
{
    ...
};
} // namespace comms

where TOptions is a set of classes/structs, which can be used to define all the required types and functionalities.

Below is an example of such possible option classes:

Our PRIMARY OBJECTIVE for this chapter is to provide an ability to create a common message interface class with only requested functionality.

For example, the definition of MyMessage interface class below

should be equivalent to defining:

And the following definition of MyMessage interface class

will be equivalent to:

Looks nice, isn't it? So, how are we going to achieve this? Any ideas?

That's right! We use MAGIC!

Sorry, I mean template meta-programming. Let's get started!

Parsing the Options

First thing, that needs to be done, is to parse the provided options and record them in some kind of a summary structure, with predefined list of static const bool variables, which indicate what options have been used, such as one below:

If some variable is set to true, the summary structure may also contain some additional relevant types and/or more variables.

For example the definition of

should result in

Here goes the actual code.

First, there is a need to define an initial version of such summary structure:

Then, handle the provided options one by one, while replacing the initial values and defining additional types when needed.

Note, that inheritance relationship is used, and according to the C++ language specification the new variables with the same name hide (or replace) the variables defined in the base class.

Also note, that the order of the options being used to define the interface class does NOT really matter. However, it is recommended, to add some static_assert() statements in, to make sure the same options are not used twice, or no contradictory ones are used together (if such exist).

Assemble the Required Interface

The next stage in the defining message interface process is to define various chunks of interface functionality and connect them via inheritance.

Note, that the interface chunks receive their base class through template parameters. It will allow us to connect them together using inheritance. Together they can create the required custom interface.

There is a need for some extra helper classes to implement such connection logic which chooses only requested chunks and skips the others.

The code below chooses whether to add MessageInterfaceIdTypeBase into the inheritance chain of interface chunks.

Let's assume that the interface options were parsed and typedef-ed into some ParsedOptions type:

Then after the following definition statement

the NewBaseClass is the same as OldBaseClass, if the value of ParsedOptions::HasMsgIdType is false (type of message ID wasn't provided via options), otherwise NewBaseClass becomes comms::MessageInterfaceIdTypeBase, which inherits from OldBaseClass.

Using the same pattern the other helper wrapping classes must be implemented also.

Choose right chunk for endian:

Add read functionality if required:

And so on...

The interface building code just uses the helper classes in a sequence of type definitions:

Once all the required definitions are in place, the common dynamic message interface class comms::Message may be defined as:

As the result, any distinct set of options provided as the template parameters to comms::Message class will cause it to have the required types and member functions.

Now, when the interface is in place, it is time to think about providing common comms::MessageBase class which is responsible to provide default implementation for functions, such as readImpl(), writeImpl(), dispatchImpl(), etc...

Last updated

Was this helpful?