cc_tutorial/tutorials/tutorial14 at master · commschamp/cc_tutorial

Tutorial 14

Custom checksum and other custom code injections.

The CommsDSL specification supports several predefined most ubiquitous checksum calculation algorithms. However, there are protocols that define their own checksum calculation logic. In order to support such scenario there is a need to to be able to inject custom C++ code into the generated code.

For the sake of this tutorial exercise let's implement a custom checksum which adds 1 to every byte and then calculates the total sum of the result. Let's also call it PlusOneSum.

The schema file of this tutorial contains the following frame definition:

    <frame name="Frame">
        ...
        <checksum name="Checksum" alg="custom" algName="PlusOneSum" from="Size" >
            <int name="ChecksumField" type="uint16" />
        </checksum>
    </frame>

Note the type of the algorithm is set to be custom (alg="custom"). Setting of the custom checksum algorithm requires knowledge about name of the algorithm, which is provided using algName property.

Let's take a look at the generated frame code inside include/tutorial14/frame/Frame.h

...
#include "tutorial14/frame/checksum/PlusOneSum.h"
...

namespace tutorial14
{

namespace frame
{

template <typename TOpt = tutorial14::options::DefaultOptions>
struct FrameLayers
{
    ...
    template <typename TMessage, typename TAllMessages>
    using Checksum =
        comms::frame::ChecksumLayer<
            ...,
            tutorial14::frame::checksum::PlusOneSum,
            ...
        >;
    ...

};

...

} // namespace frame

} // namespace tutorial14

The important parts are the include of "tutorial14/frame/checksum/PlusOneSum.h" as well as passing tutorial14::frame::checksum::PlusOneSum as a checksum calculator type template parameter. The code generated by the commsdsl2comms doesn't have relevant file and the class defined. Hence the compilation of the generated code will fail unless relevant piece of code is injected.

The injection of the custom code is performed by creating a separate directory, structure of which resembles the structure of the output project produced by the commsdsl2comms and passing the path to the directory to the commsdsl2comms (using -c parameter) at the time of code generation (see commsdsl2comms Manual for details).

For this tutorial such directory is dsl_src. The dsl_src/include/tutorial14/frame/checksum/PlusOneSum.h will be copied to the output directory and eventually will be available as include/tutorial14/frame/checksum/PlusOneSum.h. The checksum calculation class needs to be implemented in a certain way with proper public interface. Please refer to the comms::frame::ChecksumLayer for details. The PlusOneSum class is implemented inside the required tutorial14::frame::checksum namespace.

In addition to the custom checksum, there may be some extravagant protocols for which current out-of-the box functionality of the CommsChampion Ecosystem is insufficient and/or incorrect and some custom code needs to be written. As the example let's define a message that has flags <set> field at the end, which defines how the previously read 4 bytes of data needs to be interpreted. If least significant bit of the flags is cleared than the preceding 4 bytes are interpreted as a single uint32 value, and in case the bit is set, then the preceding 4 bytes need to be interpreted as two uint16 values.

The CommsDSL definition of such message may look like this:

<message name="Msg1" id="MsgId.M1" displayName="^Msg1Name" ...>
    <optional name="F1" defaultMode="exists" >
        <int name="F1" type="uint32" />
    </optional>

    <optional name="F2" defaultMode="missing">
        <int name="F2" type="uint16" />
    </optional>

    <optional name="F3" defaultMode="missing">
        <int name="F3" type="uint16" />
    </optional>

    <set name="Flags" length="1">
        <bit name="F2F3" idx="0" />
    </set>
</message>

Unfortunately the CommsDSL allows conditions (<cond>) for <optional> field using references to only preceding fields, not to ones that follow, like in this example. The default generated code for this example won't do the required functionality. There is a need to modify default read and refresh functionalities for this message. They are implemented inside dsl_src/include/tutorial14/message/Msg1.h.read and dsl_src/include/tutorial14/message/Msg1.h.refresh files. The injected code finds its way into the official protocol generated code in include/tutorial14/message/Msg1.h

REMINDER: The refresh functionality is expected to return true when the message state has been updated and false when nothing has changed.

When implementing custom refresh functionality it is a good practice to invoke the refresh implemented by the base class, just in case that the default refresh is NOT trivial. If it is, the extra unnecessary code will just be optimized away by the compiler.

/// @brief Custom refresh functionality
bool doRefresh()
{
    ...
    bool updated = Base::doRefresh(); // Don't forget default refresh functionality
    ...
    return updated;
}

Please note that injection of the code snippets is performed by putting them in the file with special suffix / extension (appended to the relevant file name with original extension) to let the code generator know what functionality is being injected. For example knowledge about injected custom refresh functionality will force usage of comms::option::def::HasCustomRefresh option when message class is defined:

template <typename TMsgBase, typename TOpt = tutorial14::options::DefaultOptions>
class Msg1 : public
    comms::MessageBase<
        ...,
        comms::option::def::HasCustomRefresh
    >
{
    ...
};

The suffixes / extensions recognized by the code generator are:

  • .inc - Adds extra include statements.
  • .construct - Overwrites default construction functionality (applicable to messages only).
  • .value - Overwrites default value get / set functionality (applicable to fields only).
  • .read - Overwrites default read functionality.
  • .write - Overwrites default write functionality.
  • .length - Overwrites default serialization length calculation.
  • .valid - Overwrites default validity check.
  • .refresh - Overwrites default refresh functionality.
  • .name - Overwrites default name retrieval.
  • .public - Extra code to be added to public class members.
  • .protected - Extra code to be added to protected class members.
  • .private - Extra code to be added to private class members.
  • .append - Extra code to be appended to the end of the generated file.
  • .replace - Completely replace the generated file with the provided one.
  • .extend - Forces generation of the original code but with appended Orig suffix to allow extension of the original class inside a replacing file.

There are multiple Msg2.h.X files inside dsl_src/include/tutorial14/message folder that demonstrate usage of the most of the suffixes above and the injected code finds its way to include/tutorial14/message/Msg2.h.

Also note that when overriding the default functionalities of the message, only the non-virtual functions (have doX() form) must be redefined, i.e. dsl_src/include/tutorial14/message/Msg1.h.read defines doRead() function, and dsl_src/include/tutorial14/message/Msg1.h.refresh defines doRefresh() function. When overriding the default functionalities of the stand alone fields the function names need to be without any changes, i.e. read(), refresh(), etc...

Below are the expected signatures of the overriding functions for the message class:

// Read functionality inside *.read
template <typename TIter>
comms::ErrorStatus doRead(TIter& iter, std::size_t len) {...}

// Write functionality inside *.write
template <typename TIter>
comms::ErrorStatus doWrite(TIter& iter, std::size_t len) const {...}

// Length calculation inside *.length
std::size_t doLength() const {...}

// Validity check inside *.valid
bool doValid() const {...}

// Refresh functionality inside *.refresh
bool doRefresh() {...}

// Name retrieval inside *.name
static const char* doName() {...}

In case the default functionality of the field needs to be overridden, then the function signatures are similar but without doX prefix:

// Read functionality inside *.read
template <typename TIter>
comms::ErrorStatus read(TIter& iter, std::size_t len) {...}

// Write functionality inside *.write
template <typename TIter>
comms::ErrorStatus write(TIter& iter, std::size_t len) const {...}

// Length calculation inside *.length
std::size_t length() const {...}

// Validity check inside *.valid
bool valid() const {...}

// Refresh functionality inside *.refresh
bool refresh() {...}

// Name retrieval inside *.name
static const char* name() {...}

SIDE NOTE: Injection of custom read / write/ refresh/ etc... functionalities are allowed only for message classes and stand-alone global fields. Currently overriding of the functionality of the member field defined inside <message> node is not supported. If there is any need for such override, there is a need to move the field into the global <fields> area and use <ref> field inside the <message> to reference the modified global one.


ANOTHER SIDE NOTE: Quite often just by the look at the message / field definition inside the schema it's difficult or even impossible to determine whether the default implementation provided by the COMMS Library or the commsdsl2comms code generator is correct or custom code injection is required. Since v4.0 of the CommsDSL and commsdsl2comms it is possible to specify overriding code requirements using the following properties:

  • valueOverride - specifies overriding code requirement for the value retrieval operation.
  • readOverride - specifies overriding code requirement for the read operation.
  • writeOverride - specifies overriding code requirement for the write operation.
  • refreshOverride - specifies overriding code requirement for the refresh operation.
  • lengthOverride - specifies overriding code requirement for the length calculation operation.
  • validOverride - specifies overriding code requirement for the validity check operation.
  • nameOverride - specifies overriding code requirement for the name retrieval operation.

All these properties can have one of the following values:

  • any (default) - Inject the custom code if exits, use default implementation if it's missing.
  • replace - Requires presence of the custom code for injection, the code generation reports error if the code is not found.
  • extend - Also requires presence of the custom code for injection (similar to replace, but the default code produced by the commsdsl2comms needs to be present (renamed by adding "Orig" suffix to avoid names clash) and available for reuse in the new injected code.
  • none - The commsdsl2comms code generator will ignore the custom code for injection even if it's available.

The Msg1 definition of the schema uses these properties:

<message name="Msg1" id="MsgId.M1" displayName="^Msg1Name" readOverride="replace" refreshOverride="replace">
    ...
</message>

To avoid an explicit implementation of the function signature the v7.0 of the commsdsl2comms allows usage of the <class>.h.<op>_body files (instead of <class>.h.<op> ones) contents of which is expected to be only a body of the function without the signature.

  • .read_body - Overwrites default read function body.
  • .write_body - Overwrites default write function body.
  • .length_body - Overwrites default serialization length function body.
  • .valid_body - Overwrites default validity check function body.
  • .refresh_body - Overwrites default refresh function body.
  • .name_body - Overwrites default name retrieval function body.

There are multiple Msg3.h.X_body files inside dsl_src/include/tutorial14/message folder that demonstrate usage of the suffixes above and the injected code finds its way to include/tutorial14/message/Msg3.h.


Summary

  • The CommsChampion Ecosystem allows injection of custom code into the generated one.
  • The custom checksum calculation is chosen with alg="custom" property in conjunction with algName which specifies class name of the custom checksum calculation class.
  • The frame definition will attempt to include the missing file which is supposed to define checksum calculation class.
  • The checksum class definition file needs to be implemented separately and residing in the required relative path to the custom source directory passed to the code generator.
  • The checksum definition class must also reside inside the correct namespace used by the frame definition.
  • The code snippets for overriding message / field operation(s) need to reside in the correct relative location and have appropriate extension specifying type of the operation it overrides.
  • The presence of the custom code for injection can be regulated using relevant xOverride property.

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