COMMS
Template library intended to help with implementation of communication protocols.
|
The Message Handling section of the How to Use Defined Custom Protocol page describes a basic way to dispatch message object (held by a pointer to the main interface class) to its handling function. The described approach requires support for Polymorphic Dispatch Message for Handling. However, since version v1.1 the COMMS library supports other multiple ways to dispatch a message even if its interface doesn't define polymorphic dispatch() member function.
The handler for the message object is expected to look exactly the same as described in Message Handling, i.e. to define handle() member function for every actual message type it intends to handle, handle() member function for the interface type for the ones it doesn't, and define RetType type to specify return type of the handling functions in case it's not void.
There are several different implemented ways to dispatch a message object, held by a pointer to its interface class, to its appropriate handling function.
Every way has its advantages and disadvantages, please read on and choose one that suites your needs. There are some definition commonly used for all the examples below.
All the mentioned below dispatch functions are defined in comms/dispatch.h header.
The used name for the common interface class (see Defining Message Interface Class) is going to be MyMessage
The message types that need to be supported are bundled in std::tuple and named AllMessages
Also let's assume that numeric ID of Message1 is 1, of Message2 is 2, of Message90 is 90, and so on...
The polymorphic dispatch of the message object can look like this
At first, the comms::dispatchMsgPolymorphic() function will check (at compile time) whether the message object can be dispatched to the handler directly, i.e. the Polymorphic Dispatch Message for Handling is supported in the interface and the used handler is of a suitable type. If this is the case, the dispatch will be performed using the following call
In case the direct dispatch is not possible, the comms::dispatchMsgPolymorphic() function will analyze the provided tuple of message types (AllMessages) at compile time and generate appropriate global static dispatch tables (initialized before call to main()).
In case the numeric ID are sequential and unique with no more than 10% of the gaps (the ID of the last message is not greater than number of message types in the provided tuple multiplied by 1.1), the generated dispatch tables and logic provide O(1) runtime complexity to dispatch message object into appropriate handler.
The generated table is just an array of pointers to a dispatch method class equivalent to the code below
Every pointer to the array is to a global instantiation of the class below for every type in the provided tuple.
The code inside the comms::dispatchMsgPolymorphic() function will use the message ID as an index to access the registry array and invoke the virtual dispatch() method. In case the accessed cell is empty, the downcasting to the right message type won't occur and handle() message for the interface of the handler object will be invoked.
In case the IDs of the message types in the provided tuple are too sparse, The registry array will be packed (no holes inside) and binary search using std::lower_bound algorithm is going to be performed. In this case the DispatchMethod class will also report an ID of the message it is responsible to handle via virtual function.
NOTE, that the performed binary search will invoke O(log(n)) times the virtual getId() member function to find the appropriate dispatch method and then invoke virtual dispatch() one to downcast the message type and invoke appropriate handle() member function of the handler.
There can also be a case when some message has multiple forms, that implemented as different message classes, but which share the same ID. For example
To support such case, the comms::dispatchMsgPolymorphic() function is overloaded with new index parameter (which is index (or offset) starting from the first type in the tuple with the requested ID) to allow selection to what type to downcast
There is also an overload to comms::dispatchMsgPolymorphic(), which doesn't receive any numeric message ID
Such call checks (at compile time) whether the message interface provides polymorphic dispatch (see Polymorphic Dispatch Message for Handling). If this is the case, then it is used to dispatch the message to the handler. If not, then the message interface definition (MyMessage) must provide Polymorphic Retrieval of Message ID to be able to retrieve ID of the message object.
SUMMARY: The runtime complexity of polymorphic dispatch can be O(1) in case the numeric IDs of the supported message types in the provided tuple are NOT too sparse (no more than 10% holes). If this is not the case the runtime complexity is O(log(n)) with multiple virtual function calls to retrieve the ID of the dispatching method. Also the downside of the polymorphic dispatch is an amount of various v-tables the compiler will have to generate, which can significantly increase the code size. It can be a problem for various embedded systems with limited ROM.
The static binary search dispatch of the message object can look like this
The comms::dispatchMsgStaticBinSearch() function generates the code equivalent to having the following folded if statements where N is number of message types
The runtime complexity of such code is always O(log(n)) and there are no extra v-tables and virtual functions involved.
In case there are distinct message types with the same numeric ID (multiple forms of the same message), the overloaded function with extra index parameter is provided (similar to described earlier comms::dispatchMsgPolymorphic()).
There is also an overload to comms::dispatchMsgStaticBinSearch(), which doesn't receive any numeric message ID
Such call requires the message interface definition (MyMessage) to provide Polymorphic Retrieval of Message ID to be able to retrieve ID of the message object.
SUMMARY: The runtime complexity of the static binary search dispatch is always O(log(n)) regardless of how sparse or compact are IDs of the message types in the provided tuple. There are also no v-tables generated by the compiler.
The linear switch dispatch of the message object can look like this
The comms::dispatchMsgLinearSwitch() function generates the code equivalent to having the following folded switch statements.
The runtime complexity of depends on the compiler being used. It has been noticed that clang starting from v3.9 generates efficient dispatch table with O(1) complexity when binary code is optimized for speed (-O2 ). Other main compilers, such as gcc and MSVC generate sequential comparison statements with O(n) runtime complexity.
In case there are distinct message types with the same numeric ID (multiple forms of the same message), the overloaded function with extra index parameter is provided (similar to described earlier comms::dispatchMsgPolymorphic(), and comms::dispatchMsgStaticBinSearch()).
There is also an overload to comms::dispatchMsgLinearSwitch(), which doesn't receive any numeric message ID
Such call requires the message interface definition (MyMessage) to provide Polymorphic Retrieval of Message ID to be able to retrieve ID of the message object.
SUMMARY: The usage of linear switch dispatch is there for real "stuntmen". If you are using clang compiler, able and willing to analyze generated binary code, and require optimal performance, then consider using linear switch dispatch. For all other cases its usage is not recommended.
The COMMS library also provides a default way to dispatch message object without specifying type of the dispatch and allowing the library to choose the best one (see comms::dispatchMsg()).
In such case the COMMS library will check whether the direct invocation over dispatch() member function exposed by the message interface class is possible (see Polymorphic Dispatch Message for Handling) or the condition of O(1) polymorphic dispatch tables holds true (no more than 10% holes in the used IDs) and use polymorphic dispatch in this case. Otherwise static binary search one will be used.
In case there are distinct message types with the same numeric ID (multiple forms of the same message), the overloaded function with extra index parameter is provided similar to other dispatch methods described above.
To verify what dispatch policy is being used the COMMS library provides compile time inquiry functions comms::dispatchMsgIsPolymorphic() and comms::dispatchMsgIsStaticBinSearch() for that purpose.
In case MyMessage interface class defines Polymorphic Dispatch Message for Handling functionality, the "polymorphic" dispatch method will always be chosen.
There is a comms::dispatchMsgIsDirect() compile time check that can be used to verify that the direct dispatch actually being used:
In some occasions there is a need to know the exact message type given the numeric ID without having any message object present for dispatching. The classic example would be the creation of message object itself given the ID (that's what comms::MsgFactory class does). To support such cases the COMMS library provides the same 3 types of dispatching the given ID to its appropriate type.
For type dispatching the handler object is expected to look a bit different.
NOTE, that the actual type is passed to the handle() member function as a template parameter. If some types require special handling function, please use template specialization, like in the example below.
Similar to Dispatch of the Message Object all the mentioned below dispatch functions are defined in comms/dispatch.h header.
The message types that need to be supported are bundled in std::tuple and named AllMessages
All the type dispatchMsgType*() methods described below return bool which in case of being true indicates that the type was successfully found and appropriate handle() member function of the handler object being called. The return of false indicates that the appropriate type hasn't been provided in AllMessages tuple.
Just like with Polymorphic dispatch of the message object, polymorphic dispatch of the message type generates similar dispatch tables with virtual functions for O(1) or O(log(n)) runtime complexity depending on how sparse the IDs in the provided tuple are.
Please see comms::dispatchMsgTypePolymorphic() for reference.
Please note, that in case there are distinct message types with the same numeric ID (multiple forms of the same message), the overloaded function with extra index parameter is also provided.
Similar to Static Binary Search dispatch of the message object, static binary search dispatch of the message type generates code equivalent to mentioned folded if statements with O(log(n)) runtime complexity.
Please see comms::dispatchMsgTypeStaticBinSearch() for reference.
Please note, that in case there are distinct message types with the same numeric ID (multiple forms of the same message), the overloaded function with extra index parameter is also provided.
Similar to Linear Switch dispatch of the message object, linear switch dispatch of the message type generates code equivalent to mentioned folded switch statements with O(1) runtime complexity when compiled with clang compiler v3.9 and above, and O(n) runtime complexity for other major compilers.
Please see comms::dispatchMsgTypeLinearSwitch() for reference.
Please note, that in case there are distinct message types with the same numeric ID (multiple forms of the same message), the overloaded function with extra index parameter is also provided.
The COMMS library also provides a default way to dispatch message type without specifying type of the dispatch and allowing the library to choose the best one (see comms::dispatchMsgType()).
In such case the COMMS library will check whether the condition of O(1) polymorphic dispatch tables holds true (no more than 10% holes in the used IDs) and use polymorphic dispatch in this case. Otherwise static binary search one will be used.
In case there are distinct message types with the same numeric ID (multiple forms of the same message), the overloaded function with extra index parameter is provided similar to other dispatch methods described above.
Just like with message object dispatching the same compile time comms::dispatchMsgTypeIsPolymorphic() and comms::dispatchMsgTypeIsStaticBinSearch() inquiry functions can be used to verify the dispatch policy for types being used.