Previous chapters described MessageBase class, which provided implementation for some portions of polymorphic behaviour defined in the interface class Message. Such implementation eliminated common boilerplate code used in every ActualMessage* class.
This chapter is going to generalise the implementation of MessageBase into the generic comms::MessageBase class, which is communication protocol independent and can be re-used in any other development.
The generic comms::MessageBase class must be able to:
provide the ID of the message, i.e. implement the idImpl()
virtual member function, when such ID is known at compile time.
provide common dispatch functionality, i.e. implement dispatchImpl()
Note, that the comms::MessageBase class receives its base class as a template parameter. It is expected to be any variant of comms::Message or any extended interface class, which inherits from comms::Message.
The supported options may include:
namespace comms{namespace option{// Provide static numeric ID, to facilitate implementation of idImpl()template <std::intmax_tTId>structStaticNumIdImpl {};// Facilitate implementation of dispatchImpl()template <typenameTActual>structDispatchImpl {};// Provide fields of the message, facilitate implementation of// readImpl(), writeImpl(), lengthImpl(), validImpl(), etc...template <typenameTFields>structFieldsImpl {};} // namespace option} // namespace comms
Parsing the Options
The options provided to the comm::MessageBase class need to be parsed in a very similar way as it was with comms::Message in the previous chapter.
Starting with initial version of the options struct:
Just like with building custom message interface, there is a need to create chunks of implementation parts and connect them using inheritance based on used options.
namespace comms{// ID information chunktemplate <typenameTBase, std::intmax_tTId>classMessageImplStaticNumIdBase:publicTBase{public: // Reuse the message ID type defined in the interfaceusingMsgIdType=typename Base::MsgIdType;protected:virtualMsgIdTypegetIdImpl() constoverride {returnstatic_cast<MsgIdType>(TId); }};// Dispatch implementation chunktemplate <typenameTBase,typenameTActual>classMessageImplDispatchBase:publicTBase{public: // Reuse the Handler type defined in the interface classusingHandler=typename Base::Handler;protected:virtualvoiddispatchImpl(Handler& handler) constoverride {handler.handle(static_cast<TActual&>(*this)); }};} // namespace comms
NOTE, that single option comms::option::FieldsImpl<> may facilitate implementation of multiple functions: readImpl(), writeImpl(), lengthImpl(), etc... Every such function was declared due to using a separate option when defining the interface. We'll have to cherry-pick appropriate implementation parts, based on the interface options. As the result, these implementation chunks must be split into separate classes.
namespace comms{template <typenameTBase,typenameTFields>classMessageImplFieldsBase:publicTBase{public:usingAllFields=TFields;AllFields&fields() { return m_fields; }constAllFields&fields() const { return m_fields; }private: TFields m_fields;};template <typenameTBase>classNessageImplFieldsReadBase:publicTBase{public: // Reuse ReadIterator definition from interface classusingReadIterator=typename TBase::ReadIterator;protected:virtualErrorStatusreadImpl(ReadIterator& iter, std::size_t len) override { // Access fields via interface provided in previous chunkauto& allFields = TBase::fields(); ... // read all the fields }};... // and so on} // namespace comms
All these implementation chunks are connected together using extra helper classes in a very similar way to how the interface chunks where connected:
And so on for all the required implementation chunks: writeImpl(), lengthImpl(), validImpl(), etc...
The final stage is to connect all the implementation chunks together via inheritance and derive comms::MessageBase class from the result.
NOTE, that existence of the implementation chunk depends not only on the implementation options provided to comms::MessageBase, but also on the interface options provided to comms::Message. For example, writeImpl() must be added only if comms::Message interface includes write() member function (comms::option::WriteIterator<> option was used) and implementation option which adds support for fields (comms::option::FieldsImpl<>) was passed to comms::MessageBase.
The implementation builder helper class looks as following:
namespace comms{// TBase is interface class// TOptions... are the implementation optionstemplate <typenameTBase,typename... TOptions>structMessageImplBuilder{ // ParsedOptions class is supposed to be defined in comms::Message classusingInterfaceOptions=typename TBase::ParsedOptions; // Parse implementation optionsusingImplOptions=MessageImplParsedOptions<TOptions...>; // Provide idImpl() if possiblestaticconstbool HasStaticNumIdImpl = InterfaceOptions::HasMsgIdType && ImplOptions::HasStaticNumIdImpl;usingBase1=typename MessageImplProcessStaticNumId< TBase, ImplOptions, HasStaticNumIdImpl>::Type; // Provide dispatchImpl() if possiblestaticconstbool HasDispatchImpl = InterfaceOptions::HasHandler && ImplOptions::HasDispatchImpl;usingBase2=typename MessageImplProcessDispatch< Base1, ImplOptions, HasDispatchImpl>::Type; // Provide access to fields if possibleusingBase3=typename MessageImplProcessFields< Base2, ImplOptions, ImplOptions::HasFieldsImpl>::Type; // Provide readImpl() if possiblestaticconstbool HasReadImpl = InterfaceOptions::HasReadIterator && ImplOptions::HasFieldsImpl;usingBase4=typename MessageImplProcessReadFields< Base3, HasReadImpl>::Type; // And so on... ...usingBaseN= ...; // The last BaseN must be taken as final type.usingType=BaseN;};} // namespace comms
Please note, that TBase template parameter is passed to MessageImplBuilder<>, which in turn passes it up the chain of possible implementation chunks, and at the end it turns up to be the base class of the whole hierarchy.
The total number of used classes may seem scary, but there are only two, which are of any particular interest to us when implementing communication protocol. It's comms::Message to specify the interface and comms::MessageBase to provide default implementation of particular functions. All the rest are just implementation details.
Summary
After all this work our library contains generic comms::Message class, that defines the interface, as well as generic comms::MessageBase class, that provides default implementation for required polymorphic functionality.
Let's define a custom communication protocol which uses little endian for data serialisation and has numeric message ID type defined with the enumeration below:
enumMyMsgId{ MyMsgId_Msg1, MyMsgId_Msg2, ...};
Assuming we have relevant field classes in place (see Fields chapter), let's define custom ActualMessage1 that contains two integer value fields: 2 bytes unsigned value and 1 byte signed value.
usingActualMessage1Fields= std::tuple< IntValueField<std::uint16_t>, IntValueField<std::int8_t>>;template <typenameTMessageInterface>classActualMessage1:public comms::MessageBase< comms::option::StaticNumIdImpl<MyMsgId_Msg1>, // provide idImpl() if needed comms::option::DispatchImpl<ActualMessage1>, // provide dispatchImpl() if needed comms::option::FieldsImpl<ActualMessage1Fields> // provide access to fields and // readImpl(), writeImpl(), // lengthImpl(), validImpl() // functions if needed >{};
That's it, no extra member functions are needed to be implemented, unless the message interface class is extended one. Note, that the implementation of the ActualMessage1 is completely generic and doesn't depend on the actual message interface. It can be reused in any application with any runtime environment that uses our custom protocol.
The interface class is defined according to the requirements of the application, that uses the implementation of the defined protocol.
classMyHandler; // forward declaration of the handler class.usingMyMessage= comms::Message< comms::option::MsgIdType<MyMsgId>, // add id() operation comms::option::ReadIterator<const std::uint8_t*>, // add read() operation comms::option::WriteIterator<std::uint8_t*> // add write() operation comms::option::Handler<MyHandler>, // add dispatch() operation comms::option::LengthInfoInterface, // add length() operation comms::option::ValidCheckInterface, // add valid() operation comms::option::LittleEndian // use little endian for serialisation>;
For convenience the protocol messages should be redefined with appropriate interface: