ID Layer
The job of this layer is handle the message ID information.
When new message is received, appropriate message object needs to be created, prior to invoking read operation of the next (wrapped) layer.
When any message is about to get sent, just get the ID information from the message object and serialise it prior to invoking the write operation of the next layer.
The code of the layer is pretty straightforward:
To properly finalise the implementation above we need to resolve two main challenges:
Implement
createMsg()
function which receives ID of the message andcreates the message object.
Define the
MsgPtr
smart pointer type, which is responsible to hold theallocated message object. In most cases defining it to be
std::unique_ptr<Message>
will do the job. However, the main problem here is usage of dynamic memoryallocation. Bare metal platform may not have such luxury. There must be a
way to support
"in place" allocation as well.
Creating Message Object
Let's start with creation of proper message object, given the numeric message ID. It must be as efficient as possible.
In many cases the IDs of the messages are sequential ones and defined using some enumeration type.
Let's assume that we have FactoryMethod
class with polymorphic createMsg()
function, that returns allocated message object wrapped in a MsgPtr
smart pointer.
In this case, the most efficient way is to have an array of pointers to polymorphic class FactoryMethod
. The index of the array cell corresponds to a message ID.
The code of MsgIdLayer::createMsg()
function is quite simple:
The runtime complexity of such code is O(1)
.
However, there are many protocols that their ID map is quite sparse and it is impractical to use an array for direct mapping:
In this case the array of FactoryMethod
s described earlier must be packed and binary search algorithm used to find required method. To support such search, the FactoryMethod
must be able to report ID of the messages it creates.
Then the code of MsgIdLayer::createMsg()
needs to apply binary search to find the required method:
Note, that std::lower_bound algorithm requires FactoryMethod
s in the "registry" to be sorted by the message ID. The runtime complexity of such code is O(log(n))
, where n
is size of the registry.
Some communication protocols define multiple variants of the same message, which are differentiated by some other means, such as serialisation length of the message. It may be convenient to implement such variants as separate message classes, which will require separate FactoryMethod
s to instantiate them. In this case, the MsgIdLayer::createMsg()
function may use std::equal_range algorithm instead of std::lower_bound, and use additional parameter to specify which of the methods to pick from the equal range found:
Please note, that MsgIdLayer::read()
function also needs to be modified to support multiple attempts to create message object with the same id. It must increment the idx
parameter, passed to createMsg()
member function, on every failing attempt to read the message contents, and try again until the found equal range is exhausted. I leave the implementation of this extra logic as an exercise to the reader.
To complete the message allocation subject we need to come up with an automatic way to create the registry of FactoryMethod
s used earlier. Please remember, that FactoryMethod
was just a polymorphic interface. We need to implement actual method that implements the virtual functionality.
Note, that the code above assumes that comms::option::StaticNumIdImpl
option (described in Generalising Message Implementation chapter) was used to specify numeric message ID when defining the ActualMessage*
class.
Also note, that the example above uses dynamic memory allocation to allocate actual message object. This is just for idea demonstration purposes. The Allocating Message Object section below will describe how to support "in-place" allocation.
The types of the messages, that can be received over I/O link, are usually known at compile time. If we bundle them together in std::tuple
, it is easy to apply already familiar meta-programming technique of iterating over the provided types and instantiate proper ActualFactoryMethod<>
object.
The size of the registry can easily be identified using std::tuple_size.
Now it's time to iterate (at compile time) over all the types defined in the AllMessages
tuple and create separate ActualFactoryMethod<>
for each and every one of them. Remember tupleForEach? We need something similar here, but missing the tuple object itself. We are just iterating over types, not the elements of the tuple object. We'll call it tupleForEachType()
. See Appendix D for implementation details.
We also require a functor class that will be invoked for every message type and will be responsible to fill the provided registry:
The initialisation function may be as simple as:
NOTE, that ActualFactoryMethod<>
factories do not have any internal state and are defined as static objects. It is safe just to store pointers to them in the registry array.
To summarise this section, let's redefine comms::MsgIdLayer
and add the message creation functionality.
Allocating Message Object
At this stage, the only missing piece of information is definition of the smart pointer type responsible to hold the allocated message object (MsgPtr
) and allowing "in place" allocation instead of using dymaic memory.
When dynamic memory allocation is allowed, everything is simple, just use std::unique_ptr
with standard deleter. However, it is a bit more difficult when such allocations are not allowed.
Let's start with the calculation of the buffer size which is big enough to hold any message in the provided AllMessages
bundle. It is similar to the size of the union
below.
However, all the required message types are provided as std::tuple
, not as union
. What we need is something like std::aligned_union, but for the types already bundled in std::tuple
. It turns out it is very easy to implement using template specialisation:
NOTE, that some compilers (gcc v5.0 and below) may not implement std::aligned_union
type, but they do implement std::aligned_storage. The Appendix E shows how to implement aligned union functionality using std::aligned_storage
.
The "in place" allocation area, that can fit in any message type listed in AllMessages
tuple, can be defined as:
The "in place" allocation is simple:
The "in place" allocation requires "in place" deletion, i.e. destruction of the allocated element.
The smart pointer to Message
interface class may be defined as std::unique_ptr<Message, InPlaceDeleter<Message> >
.
Now, let's define two independent allocation policies with the similar interface. One for dynamic memory allocation, and the other for "in place" allocation.
Please pay attention, that the implementation of InPlaceAllocationPolicy
is the simplest possible one. In production quality code, it is recommended to insert protection against double allocation in the used storage area, by introducing boolean flag indicating, that the storage area is or isn't free. The pointer/reference to such flag must also be passed to the deleter object, which is responsible to update it when deletion takes place.
The choice of the allocation policy used in comms::MsgIdLayer
may be implemented using the already familiar technique of using options.
If no option is specified, the DynMemAllocationPolicy
must be chosen. To force "in place" message allocation a separate option may be defined and passed as template parameter to comms::MsgIdLayer
.
Using the familiar technique of options parsing, we can create a structure, where a boolean value HasInPlaceAllocation
defaults to false
and can be set to true
, if the option mentioned above is used. As the result, the policy choice may be implemented as:
What remains to be done is to provide the ActualFactoryMethod<>
class with an ability to use allocation policy for allocating the message. Please remember, that ActualFactoryMethod<>
objects are stateless static ones. It means that the allocation policy object needs to passed as the parameter to its allocation function.
Summary
The final implementation of the ID Layer (comms::MsgIdLayer
) is a generic piece of code. It receives a list of message classes, it must recognise, as a template parameter. The whole logic of creating the right message object given the numeric ID of the message is automatically generated by the compiler using only static memory. When new message is added to the protocol, what needs to be updated is the bundle of available message classes (AllMessages
). Nothing else is required. Recompilation of the sources will generate a code that supports new message as well. The implementation of comms::MsgIdLayer
above has O(log(n))
runtime complexity of finding the right factory method and creating appropriate message object. It also supports multiple variants of the same message which are implemented as different message classes, but report the same message ID. By default comms::MsgIdLayer
uses dynamic memory to allocate new message object. It can easily be changed by providing comms::option::InPlaceAllocation
option to it, which will force usage of "in place" allocation. The "in place" allocation may create one message at a time. In order to be able to create a new message object, the previous one must be destructed and de-allocated before.
Last updated