Tutorial 10
Dealing with small number of uni-directional messages.
There are cases when number of uni-directional messages is relatively small compared to the number of all the messages. In this case splitting the common interface definition to input and output may be quite inconvenient. In may also result in unnecessary code duplication if such interfaces need to expose common polymorphic behavior, such as message name retrieval.
When there is a common message interface class defined for both input and output messages, the default configuration of uni-directional messages will result in generation of unneeded virtual functions, which in turn will consume an extra code space. It might be an unacceptable price to pay in embedded systems with limited ROM size.
The COMMS Library provides a way to suppress generation of specific virtual functions using extra configuration options. Let's see how the generated code helps with the task.
The schema of this tutorial defines only two uni-directional messages
(Msg1 and Msg2). All the rest are bi-directional messages.
<message name="Msg1" id="MsgId.M1" displayName="^Msg1Name" sender="client" /> <message name="Msg2" id="MsgId.M2" displayName="^Msg2Name" sender="server" /> <message name="Msg3" id="MsgId.M3" displayName="^Msg3Name" /> <message name="Msg4" id="MsgId.M4" displayName="^Msg4Name" /> <message name="Msg5" id="MsgId.M5" displayName="^Msg5Name" />
In this particular example the server is responsible to respond with Msg2 to
Msg1 received from the client and echo back all other messages.
Previous tutorial introduced various lists of input messages generated by the commsdsl2comms utility, which reside in include/<namespace>/input folder. The reader also might have noticed the usage of <namespace>::options::DefaultOptions (defined include/<namespace>/options/DefaultOptions.h) passed as a default value to some template parameter to various classes in the protocol definition. The purpose of such parameter is to provide global application specific configurations to the protocol definition. Let's take a look at its definition:
struct DefaultOptions { struct message { using Msg1 = comms::option::app::EmptyOption; using Msg2 = comms::option::app::EmptyOption; }; // struct message struct frame { struct FrameLayers { using Data = comms::option::app::EmptyOption; using Id = comms::option::app::EmptyOption; }; // struct FrameLayers }; // struct frame };
The inner structure of the class resembles the namespaces used to define the classes, i.e.
DefaultOptions::message::Msg1 is passed as extra configuration parameter to
tutorial10::message::Msg1
template <typename TMsgBase, typename TOpt = tutorial10::options::DefaultOptions> class Msg1 : public comms::MessageBase< TMsgBase, typename TOpt::message::Msg1, ... > { ... };
The used options parameter (TOpt) is the way to extra customize the message definition.
The <namespace>::options::DefaultOptions
contains an empty customization, i.e. comms::option::app::EmptyOption is passed to
all the customization points.
The generated code contains multiple out-of-the box customization options which reside in include/<namespace>/options folder. The ones applicable to this example are tutorial10::options::ServerDefaultOptions and tutorial10::options::ClientDefaultOptions.
Let's take a look how ServerDefaultOptions are defined:
template <typename TBase = tutorial10::options::DefaultOptions> struct ServerDefaultOptionsT : public TBase { struct message : public TBase::message { using Msg1 = std::tuple< comms::option::app::NoWriteImpl, comms::option::app::NoRefreshImpl, typename TBase::message::Msg1 >; using Msg2 = std::tuple< comms::option::app::NoReadImpl, comms::option::app::NoDispatchImpl, typename TBase::message::Msg2 >; }; }; using ServerDefaultOptions = ServerDefaultOptionsT<>;
The options passed to Msg1 suppress generation of polymorphic write()
as well as polymorphic refresh() because it is never expected to
be written by the server. The Msg2 on the other hand is never
expected to get received, hence the read() and dispatch() are
suppressed.
Note that passing multiple options are supported via bundling them in std::tuple.
SIDE NOTE: All the options inside include/<namespace>/options folder are implemented to allow their combinations. For example:
using BareMetalServerOptions =
tutorial10::options::BareMetalDefaultOptionsT<
tutorial10::options::ServerDefaultOptions
>;The definition above creates options that are suitable for the bare-metal server.
Now it's take to take a look again at the definitions inside the ServerSession.h:
class ServerSession : public Session { using Base = Session; public: // Protocol configuration options suitable for server using ServerOptions = tutorial10::options::ServerDefaultOptions; // Definition of messages using Msg1 = tutorial10::message::Msg1<Message, ServerOptions>; using Msg2 = tutorial10::message::Msg2<Message, ServerOptions>; using Msg3 = tutorial10::message::Msg3<Message, ServerOptions>; using Msg4 = tutorial10::message::Msg4<Message, ServerOptions>; using Msg5 = tutorial10::message::Msg5<Message, ServerOptions>; private: // Definition of the frame using Frame = tutorial10::frame::Frame< Message, tutorial10::input::ServerInputMessages<Message, ServerOptions>, ServerOptions >; };
The tutorial10::options::ServerDefaultOptions are passed to the definition of all the messages, as well as frame definition.
The server implements the following handling function for arriving Msg1:
void ServerSession::handle(Msg1& msg) { std::cout << "Received message \"" << msg.doName() << "\" with ID=" << (unsigned)msg.doGetId() << std::endl; // try to echo Msg1, expected to fail sendMessage(msg); Msg2 outMsg; sendMessage(outMsg); }
Note, that attempt to echo Msg1 back is expected to fail because options passed to Msg1 definition
suppress generation of polymorphic write() functionality. The return value of the write() operation will be
comms::ErrorStatus::NotSupported.
All other messages will be written by the server without any problem.
SIDE NOTE: When comms::option::app::WriteIterator option is passed to the
comms::Message class
when common interface class created, the following functionality is introduced:
class comms::Message { public: using WriteIterator = ...; // Provided iterator type comms::ErrorStatus write(WriteIterator& iter, std::size_t len) { return writeImpl(iter, len); } protected: virtual comms::ErrorStatus writeImpl(WriteIterator& iter, std::size_t len) { return comms::ErrorStatus::NotSupported; } };
In case comms::option::app::NoWriteImpl is passed to the
comms::MessageBase when
actual message class is defined, the proper writeImpl() overriding member function is NOT
generated, which results in usage of the default one defined by the interface.
The definition and the functionality of the client is very similar.
class ClientSession : public Session { public: // Common interface class for input messages using Message = tutorial10::Message< 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::LengthInfoInterface, // Polymorphic length calculation comms::option::app::IdInfoInterface, // Polymorphic message ID retrieval comms::option::app::NameInterface, // Polymorphic message name retrieval comms::option::app::Handler<ClientSession> // Polymorphic message dispatch >; // Protocol configuration options suitable for client using ClientOptions = tutorial10::options::ClientDefaultOptions; // Definition of messages using Msg1 = tutorial10::message::Msg1<Message, ClientOptions>; using Msg2 = tutorial10::message::Msg2<Message, ClientOptions>; using Msg3 = tutorial10::message::Msg3<Message, ClientOptions>; using Msg4 = tutorial10::message::Msg4<Message, ClientOptions>; using Msg5 = tutorial10::message::Msg5<Message, ClientOptions>; private: using Frame = tutorial10::frame::Frame< Message, tutorial10::input::ClientInputMessages<Message, ClientOptions>, ClientOptions >; ... };
The tutorial10::options::ClientDefaultOptions suppress
polymorphic read() and dispatch() implementation for Msg1 (because it's never expected to get
received) as well as polymorphic write() and refresh() implementation for Msg2 (because it's never
expected to get sent).
struct ClientDefaultOptionsT : public TBase { struct message : public TBase::message { using Msg1 = std::tuple< comms::option::app::NoReadImpl, comms::option::app::NoDispatchImpl, typename TBase::message::Msg1 >; using Msg2 = std::tuple< comms::option::app::NoWriteImpl, comms::option::app::NoRefreshImpl, typename TBase::message::Msg2 >; }; }; using ClientDefaultOptions = ClientDefaultOptionsT<>;
The attempt to send Msg2 out to the server is expected to fail because Msg2 definition
does not support polymorphic write:
void ClientSession::sendMsg1() { Msg1 msg; sendMessage(msg); // try to send Msg2, expected to fail Msg2 msg2; sendMessage(msg2); }
Summary
- Suppressing unnecessary virtual function is possible via usage of various protocol definition options.
- The generated code provides various such options which reside in include/<namespace>/options folder.
- The default options used throughout the protocol definition are <namespace>::options::DefaultOptions
- The options relevant to server are <namespace>::options::ServerDefaultOptions
- The options relevant to client are <namespace>::options::ClientDefaultOptions
- The provided various protocol options inside include/<namespace>/options are defined to allow various combinations.
Read Previous Tutorial <-----------------------> Read Next Tutorial