Tutorial 17
Custom transport framing layers.
So far, in all the previous tutorials every frame layer had a well defined single
responsibility. However, there are protocols that may split the used field to
multiple parts where every part has its own influence on how the message is
decoded and/or handled. In most such cases, the <id> or <size>
layers allocate a couple of extra bits of its field to make some extra flags.
This tutorial is quite similar to the previous in terms of
having the common Flags field in its interface definition which influence
whether some fields exist in both Msg1 and Msg2.
<interface name="Interface" description="Common Interface for all the messages."> <set name="Flags" length="1"> <bit name="B0" idx="0" /> <bit name="B1" idx="1" /> </set> </interface>
However instead of having separate <value> layer in its frame, both
<id> and <size> layers have their fields split into actual
id and size values, but also having some extra flags bits.
<frame name="Frame"> <custom name="IdWithFlags" semanticLayerType="id"> <bitfield name="Field"> <ref field="MsgId" bitLength="6" /> <set name="Flags" bitLength="2"> <bit name="F1" idx="0" description="Re-assigned to B1 of interface flags" /> </set> </bitfield> </custom> <custom name="SizeWithFlags" semanticLayerType="size"> <bitfield name="Field"> <int name="Size" type="uint16" bitLength="12"/> <set name="Flags" bitLength="4"> <bit name="F1" idx="0" description="Re-assigned to B0 of interface flags" /> </set> </bitfield> </custom> <payload name="Data" /> </frame>
Please pay attention to the following details:
- The default
<id>and<size>do NOT support the intended split of the value into multiple sub-fields. Hence the<custom>layer definition needs to be used instead. - The layer that replaces
<id>must set semanticLayerType property to id to let the code generator know which layer replaces original<id>layer. - Setting semanticLayerType for the layer that replaced
<size>is not necessary at this stage of developement (the code generator doesn't produce any special code for such layer), but still recommended. - The
<bitfield>field is used to split the field in multiple members. - In this particular tutorial
SizeWithFlagsfollows theIdWithFlags, i.e the frame isID (with flags) | SIZE (with flags) | PAYLOAD. The opposite case where the size handling precedes the id is also supported but it has its nuances. It will be covered in one of the howto-s.
SIDE NOTE: Before v5.0 of the CommsDSL Specification,
the IdWithFlags custom layer had to use idReplacement="true" property to indicate that
the layer replaces the original <id>. Since v5.0 it is deprecated and semanticLayerType
should be used instead.
Let's take a look inside generated include/tutorial17/frame/Frame.h. It contains the following include statements:
#include "tutorial17/frame/layer/IdWithFlags.h" #include "tutorial17/frame/layer/SizeWithFlags.h"
The <custom> layers require custom implementation of the frame layer, because the code generator
doesn't produce them. If the relevant code is not injected, then the compilation of the protocol code will
probably fail. The dsl_src/include/tutorial17/frame/layer/IdWithFlags.h
and dsl_src/include/tutorial17/frame/layer/SizeWithFlags.h
files implement required layers. The code is just copied to
include/tutorial17/frame/layer/IdWithFlags.h
and include/tutorial17/frame/layer/SizeWithFlags.h
respectively by the commsdsl2comms code generator.
Let's take a look how the include/tutorial17/frame/layer/IdWithFlags.h is actually implemented.
template<typename TField, typename TMessage, typename TAllMessages, typename TNextLayer, typename... TOptions> class IdWithFlags : public comms::frame::MsgIdLayer< TField, TMessage, TAllMessages, TNextLayer, TOptions..., comms::option::def::ExtendingClass<IdWithFlags<TField, TMessage, TAllMessages, TNextLayer, TOptions...> > > { ... };
It extends comms::frame::MsgIdLayer. The latter supports its extension and customization to support cases like in this tutorial. It is properly described in Defining Custom Message ID Frame Layer tutorial page of the COMMS Library documentation.
Please note usage of comms::option::def::ExtendingClass option which specifies the actual type of extending class. It is basically the CRTP C++ idiom. The extending class is expected to override the following member functions:
getMsgIdFromField()- retrieve the numeric message ID given the reference to the updated (successfully read) layer field.beforeRead()- provides a chance to update allocated message object with relevant data (if needed) before the read operation is forwarded to the next layer. In our example the flags stored inside the interface are updated accordingly.prepareFieldForWrite()- update the field's value (assign provided message ID and other field members) before it is written.
Please refer to the API documentation of the comms::frame::MsgIdLayer class for the full information on the functions' signatures.
template<typename TField, typename TMessage, typename TAllMessages, typename TNextLayer, typename... TOptions> class IdWithFlags : public comms::frame::MsgIdLayer<...> { ... public: // Repeat some types from the base class using Field = typename Base::Field; using MsgIdType = typename Base::MsgIdType; using MsgIdParamType = typename Base::MsgIdParamType; // Given the combined bitfield field, retrieve message ID value static MsgIdType getMsgIdFromField(const Field& field) { return field.field_msgId().value(); } // Before forwarding read to the next layer update flags extra transport field in the interface template<typename TMsg> static void beforeRead(const Field& field, TMsg& msg) { msg.transportField_flags().setBitValue_B1(field.field_flags().getBitValue_F1()); } // Prepare field value to be written template <typename TMsg> static void prepareFieldForWrite(MsgIdParamType id, const TMsg& msg, Field& field) { field.field_msgId().value() = id; field.field_flags().setBitValue_F1(msg.transportField_flags().getBitValue_B1()); } };
The include/tutorial17/frame/layer/SizeWithFlags.h is implemented in very similar way, but extending comms::frame::MsgSizeLayer.
/// @brief Customizing the size layer template<typename TField, typename TNextLayer, typename... TOptions> class SizeWithFlags : public comms::frame::MsgSizeLayer< TField, TNextLayer, TOptions..., comms::option::def::ExtendingClass<SizeWithFlags<TField, TNextLayer, TOptions...> > > { }
The Defining Custom Message Size Frame Layer tutorial page of the COMMS Library documentation provides a lot of details on how define such layer.
The SizeWithFlags class also uses comms::option::def::ExtendingClass option
which specifies the actual type of extending class.
The extending class is expected to override the following member functions:
getRemainingSizeFromField()- retrieve the remaining message size given the reference to the updated (successfully read) layer field.beforeRead()- provides a chance to update allocated message object (if such is ready) with relevant data (if needed) before the read operation is forwarded to the next layer. In our example the flags stored inside the interface are updated accordingly.prepareFieldForWrite()- update the field's value (assign remaining message size and other field members) before it is written.
Please refer to the API documentation of the comms::frame::MsgSizeLayer class for the full information on the functions' signatures.
template<typename TField, typename TNextLayer, typename... TOptions> class SizeWithFlags : public comms::frame::MsgSizeLayer< TField, TNextLayer, TOptions..., comms::option::def::ExtendingClass<SizeWithFlags<TField, TNextLayer, TOptions...> > > { public: // Repeat some types from the base class using Field = typename Base::Field; // Given the combined bitfield field, retrieve remaining size static std::size_t getRemainingSizeFromField(const Field& field) { return static_cast<std::size_t>(field.field_size().value()); } // Before forwarding read to the next layer update flags extra transport field in the interface template<typename TMsg> static void beforeRead(const Field& field, TMsg* msg) { COMMS_ASSERT(msg != nullptr); // The message object is expected to be created msg->transportField_flags().setBitValue_B0(field.field_flags().getBitValue_F1()); } // Prepare field value to be written template <typename TMsg> static void prepareFieldForWrite(std::size_t size, const TMsg* msg, Field& field) { assert(msg != nullptr); field.field_size().value() = static_cast<typename Field::Field_size::ValueType>(size); field.field_flags().setBitValue_F1(msg->transportField_flags().getBitValue_B0()); } };
NOTE that the msg parameter in beforeRead() and prepareFieldForWrite() member functions
is passed by pointer (not reference like with IdWithFlags). It is to allow cases when
SIZE precedes ID in the framing definition and the message object is not available. In this
particular tutorial the message object is created before the SizeWithFlags layer
performs its read() operation, as the result the pointer is expected to be not nullptr.
Both ServerSession and ClientSession work as usual. There is one small nuance though in how ClientSession is defined and works.
In this particular tutorial the common interface of the ClientSession
does NOT expose polymorphic length calculation and its output iterator is defined to be
std::back_insert_iterator<std::vector<std::uint8_t> >, i.e. not random-access one.
using Message = tutorial17::Interface< comms::option::app::ReadIterator<const std::uint8_t*>, // Polymorphic read comms::option::app::WriteIterator<std::back_insert_iterator<std::vector<std::uint8_t> > >, // Polymorphic write comms::option::app::IdInfoInterface, // Polymorphic message ID retrieval comms::option::app::NameInterface, // Polymorphic name retrieval comms::option::app::Handler<ClientSession> // Polymorphic dispatch >;
As the result when client writes the message, the size value is not known during write() operation and
comms::ErrorStatus::UpdateRequired is expected to be returned.
void ClientSession::sendMessage(const Message& msg) { ... auto es = m_frame.write(msg, writeIter, output.max_size()); if (es == comms::ErrorStatus::UpdateRequired) { auto updateIter = &output[0]; es = m_frame.update(msg, updateIter, output.size()); } ... }
To properly update the size value, the update() operation needs to be invoked, but because
the value also needs to update the flags part of the field, the message object needs to be
passed as a parameter as well. The SizeWithFlags::prepareFieldForWrite() function will be
called to properly set the size and the flags.
Summary
- Custom layers are defined using
<custom>XML node. - The layer that replaces
<id>needs to set semanticLayerType property to id. - To implement custom replacement of
<id>, the custom layer code may extend comms::frame::MsgIdLayer (see Defining Custom Message ID Frame Layer for details). - To implement custom replacement of
<size>, the custom layer code may extend comms::frame::MsgSizeLayer (see Defining Custom Message Size Frame Layer for details).
Read Previous Tutorial <-----------------------> Read Next Tutorial