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:
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:
namespace comms{namespace option{// Define type used to store message IDtemplate <typenameT>structMsgIdType{};// Specify type of iterator used for readingtemplate <typenameT>structReadIterator {};// Specify type of iterator used for writingtemplate <typenameT>structWriteIterator {};// Use little endian for serialisation (instead of default big)structLittleEndian {};// Include serialisation length retrieval in public interfacestructLengthInfoInterface {};// Include validity check in public interfacestructValidCheckInterface {};// Define handler classtemplate <typenameT>structHandler{};} // namespace option} // namespace comms
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
classMyHandler;usingMyMessage= comms::Message< comms::option::MsgIdType<std::uint16_t>, // use std::uint16_t as message ID type comms::option::ReadIterator<const std::uint8_t*>, // use const std::uint8_t* as iterator for reading comms::option::WriteIterator<std::uint8_t*>, // use std::uint8_t* as iterator for writing comms::option::LengthInfoInterface, // add length() member function to interface comms::option::Handler<MyHandler> // add dispatch() member function with MyHandler as the handler class>;
should be equivalent to defining:
classMyMessage{public:usingMsgIdType= std::uint16_t;usingReadIterator=const std::uint8_t*;usingWriteIterator= std::uint8_t*;usingHandler=MyHandler;MsgIdTypeid() {...}ErrorStatusread(ReadIterator& iter, std::size_t len) {...}ErrorStatuswrite(WriteIterator& iter, std::size_t len) const {...} std::size_tlength() const {...}voiddispatch(Handler& handler) {...}protected:template <typenameT>staticTreadData(ReadIterator& iter) {...} // use big endian by defaulttemplate <typenameT>staticvoidwriteData(T value,WriteIterator& iter) {...} // use big endian by default ...};
And the following definition of MyMessage interface class
usingMyMessage= comms::Message< comms::option::MsgIdType<std::uint8_t>, // use std::uint8_t as message ID type comms::option::LittleEndian, // use little endian in serialisation comms::option::ReadIterator<const std::uint8_t*> // use const std::uint8_t* as iterator for reading>;
will be equivalent to:
classMyMessage{public:usingMsgIdType= std::uint8_t;usingReadIterator=const std::uint8_t*;MsgIdTypeid() {...}ErrorStatusread(ReadIterator& iter, std::size_t len) {...}protected:template <typenameT>staticTreadData(ReadIterator& iter) {...} // use little endiantemplate <typenameT>staticvoidwriteData(T value,WriteIterator& iter) {...} // use little endian ...};
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
classMyHandler;usingMyMessage= comms::Message< comms::option::MsgIdType<std::uint16_t>, // use std::uint16_t comms::option::ReadIterator<const std::uint8_t*>, // use const std::uint8_t* as iterator for reading comms::option::WriteIterator<std::uint8_t*>, // use std::uint8_t* as iterator for writing comms::option::LengthInfoInterface, // add length() member function to interface comms::option::Handler<MyHandler> // add dispatch() member function with MyHandler as the handler class>;
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.
namespace comms{// ID retrieval chunktemplate <typenameTBase,typenameTId>classMessageInterfaceIdTypeBase:publicTBase{public:usingMsgIdType=TId;MsgIdTypegetId() const {returngetIdImpl(); }protected:virtualMsgIdTypegetIdImpl() const=0;};// Big endian serialisation chunktemplate <typenameTBase>classMessageInterfaceBigEndian:publicTBase{protected:template <typenameT>staticTreadData(ReadIterator& iter) {...} // use big endiantemplate <typenameT>staticvoidwriteData(T value,WriteIterator& iter) {...} // use big endian};// Little endian serialisation chunktemplate <typenameTBase>classMessageInterfaceLittleEndian:publicTBase{protected:template <typenameT>staticTreadData(ReadIterator& iter) {...} // use little endiantemplate <typenameT>staticvoidwriteData(T value,WriteIterator& iter) {...} // use little endian};// Read functionality chunktemplate <typenameTBase,typenameTReadIter>classMessageInterfaceReadBase:publicTBase{public:usingReadIterator=TReadIter;ErrorStatusread(ReadIterator& iter, std::size_t size) {returnreadImpl(iter, size); }protected:virtualErrorStatusreadImpl(ReadIterator& iter, std::size_t size) =0;};... // and so on} // namespace comms
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.
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.
The interface building code just uses the helper classes in a sequence of type definitions:
namespace comms{classEmptyBase {};template <typename... TOptions>structMessageInterfaceBuilder{ // Parse the optionsusingParsedOptions=MessageInterfaceParsedOptions<TOptions...>; // Add ID retrieval functionality if ID type was providedusingBase1=typename MessageInterfaceProcessMsgId< EmptyBase, ParsedOptions, ParsedOptions::HasMsgIdType>::Type; // Add readData() and writeData(), that use the right endianusingBase2=typename MessageInterfaceProcessEndian< Base1, ParsedOptions::HasLittleEndian>::Type; // Add read functionality if ReadIterator type was providedusingBase3=typename MessageInterfaceProcessReadIterator< Base2, ParsedOptions, ParsedOptions::HasReadIterator>::Type; // Add write functionality if WriteIterator type was providedusingBase4=typename MessageInterfaceProcessWriteIterator< Base3, ParsedOptions, ParsedOptions::HasWriteIterator>::Type; // And so on... ...usingBaseN= ...; // The last BaseN must be taken as final type.usingType=BaseN;};} // namespace comms
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...