cc_tutorial/tutorials/tutorial10 at master · commschamp/cc_tutorial

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

Read Previous Tutorial <-----------------------> Read Next Tutorial