Implementing Binary Communication Protocols in C++ (for Embedded Systems)
Alex Robenko - 14 Aug 2020
Alex Robenko - 14 Aug 2020
[SHOWTOGROUPS=4,20,22]
This article is about easy and compile-time configurable implementation of binary communication protocols using C++11 programming language.
Introduction
This article is about easy and compile-time configurable implementation of binary communication protocols using C++11 programming language, with main focus on embedded systems (including bare-metal ones).
Interested? Then buckle up and read on.
Background
Almost every electronic device/component nowadays has to be able to communicate to other devices, components, or outside world over some I/O link. Such communication is implemented using various communication protocols, which are notorious for requiring a significant amount of boilerplate code to be written. The implementation of these protocols can be a tedious, time consuming and error-prone process. Therefore, there is a growing tendency among developers to use third party code generators for data (de)serialization. Usually such tools receive description of the protocol data structures in separate source file(s) with a custom grammar, and generate appropriate (de)serialization code and necessary abstractions to access the data.
The main problem with the existing tools is that their major purpose is data structures serialization and/or facilitation of remote procedure calls (RPC). The binary data layout and how the transferred data is going to be used is of much lesser importance. Such tools focus on speed of data serialization and I/O link transfer rather than on safe handling of malformed data, compile time customization needed by various embedded systems and significantly reducing amount of boilerplate code, which needs to be written to integrate generated code into the product's code base.
The binary communication protocols, which may serve as an API or control interface of the device, on the other hand, require a different approach. Their specification puts major emphasis on binary data layout, what values are being transferred (data units, scaling factor, special values, etc.) and how the other end is expected to behave on certain values (what values are considered to be valid and how to behave on reception of invalid values). It requires having some extra meta-information attached to described data structures, which needs to propagate and be accessible in the generated code. The existing tools either don't have means to specify such meta-information (other than in comments), or don't know what to do with it when provided. As a result, the developer still has to write a significant amount of boilerplate code in order to integrate the generated serialization focused code to be used in binary communication protocol handling.
Required Features
As an embedded C++ developer, I require the following features to be available to me, at least to some extent.
Polymorphic Interfaces Configuration
Usually every message definition is implemented (or code generated) as a separate class. In many cases, there is a common code that is applicable to all such message classes. The proper implementation would be to introduce polymorphic behavior with virtual functions (such as reading message data, writing it, calculating serialization length, etc.). However, creation of full interface to be polymorphic may be impractical. Every application that might use the protocol definition code is different. For example, in case many messages are uni-directional, one side (client) will require polymorphic write (but not read) for such messages, the one side (server) will require the opposite - polymorphic read (but not write). Most of the virtual functions will end up being included in the final binary / image, even if they are unused. It can result in unnecessary code bloat, which may be a problem for embedded systems (especially bare-metal ones).
There is a need for compile time, or at least code generation time configuration of the polymorphic interfaces that are going to be used. In the best case scenario, such configuration should be done per message class.
Extra Meta Information
There may be a significant amount of extra meta information that comes with the protocol definition. For example, one of the protocol fields in one of the messages needs to report a distance between two points. The protocol designer has decided to report the distance in centimeters. Such information will probably be written in plain text in protocol specification and if some third party code generator is used, then this information might end up being in the comment section describing the field or a message. However, in most cases, such information will not end up being in generated code. The developer that needs to integrate the generated code into its business logic needs to manually write some boilerplate code to do the math of conversion into the different distance units (such as meters) relevant to the application being developed.
It is also not very uncommon for the binary protocol being specified at the same time the application that uses it being developed. Imagine that some specific use case pops up during the development, where distance in centimeters doesn't provide sufficient precision. Thanks to the fact that protocol specification hasn't been finalized yet, the developer decides to change the reported distance units from centimeters to millimeters. It means that other developers that might have written their boilerplate code with assumption of distance units being centimeters must be aware of the change and not forget to modify their code.
Some protocol developers also dislike sending floating point numbers "as-is" over the communication link. In many such cases, floating point numbers are multiplied by some predefined value and sent over the I/O links as integers while the remaining fraction after the decimal point is dropped. The other side does the opposite operation on reception of the message, i.e., dividing the received integer by the same predefined value to get the floating point number. Which predefined value to use for such scaling is also meta information, which is not transferred "on the wire" and should be present in the generated or manually written code of the protocol definition.
Also in many cases, the protocol may define some values with special meaning. Let's assume there is a need to communicate a delay in seconds before some event needs to happen. There should be some special value that indicates infinite duration. Usually it is either 0 or maximum possible value of the unsigned type being used. The developer that integrates the generated code into the application needs to know what value it is. There is a need to write at least one extra piece of boilerplate code that wraps the special value and gives it a name. Wouldn't it be better if the generated code contained such helper function already?
Many protocols specify ranges of valid values and expect certain behavior on invalid values. There is an expectation that generated code would provide an ability to inquire that the received value is valid to avoid manually written boilerplate code that checks this information.
The list of such examples with meta-information that is part of the protocol definition, but not transfered as data of the messages can go on and on. There is a need for the generated code to provide the required functionality to be used by the integration developer without any concern of what to do when the meta-information is modified.
Customization of Data Types
Most of the currently available solutions for the protocol code generation use hard-coded types for some particular data structures, such as std::string for strings and/or std::vector for lists. Also in many cases, the generated functions that perform serialization / deserialization receive their input / output as std::istream or std:stream. These data structures may be unsuitable for some applications, especially embedded (including bare-metal) ones.
There is a need to be able to substitute the default data structures with some equivalent replacements, ideally at compile time for selected fields / messages, but globally (during code generation for example) may also be an acceptable solution.
Excluding Exceptions and Dynamic Memory Allocation
Many constrained embedded environments make it difficult to use exceptions as well as dynamic memory allocation (especially bare-metal ones). There is a need for an ability to generate protocol definition code that don't use either of them.
Efficient Built-In Mapping of Message ID to Type
Usually, when new encoded message arrives over I/O link, encoded as raw data, it has some transport framing containing numeric message ID. Unfortunately, most (if not all) of the available protocol code generation solution leave the task of mapping numeric ID into the actual message type (or appropriate handling function) to be written by the developer who integrates the generated code into the business logic. Usually, such code is boilerplate, efficiency of which depends on the competence of the developer.
Third Party Protocol Support
Many of the available protocol generation solutions have their own encoding and framing without any ability to modify it. Using of such tools may be acceptable for new protocols being defined from scratch. However, there are heaps of already defined third party protocols, code for which cannot be generated with such tools.
Injecting Custom Code
This requirement complements the Third Party Protocol Support one. Even if a chosen code generation solution supports definition of the third party protocol and its grammar for schema files is very rich, there will always be a protocol containing some small nuance, which cannot be represented correctly using the available grammar. As a result, the generated code may be incorrect and/or incomplete. Proper protocol code generation solution should allow injection of snippets of custom, manually written code.
[/SHOWTOGROUPS]
This article is about easy and compile-time configurable implementation of binary communication protocols using C++11 programming language.
Introduction
This article is about easy and compile-time configurable implementation of binary communication protocols using C++11 programming language, with main focus on embedded systems (including bare-metal ones).
Interested? Then buckle up and read on.
Background
Almost every electronic device/component nowadays has to be able to communicate to other devices, components, or outside world over some I/O link. Such communication is implemented using various communication protocols, which are notorious for requiring a significant amount of boilerplate code to be written. The implementation of these protocols can be a tedious, time consuming and error-prone process. Therefore, there is a growing tendency among developers to use third party code generators for data (de)serialization. Usually such tools receive description of the protocol data structures in separate source file(s) with a custom grammar, and generate appropriate (de)serialization code and necessary abstractions to access the data.
The main problem with the existing tools is that their major purpose is data structures serialization and/or facilitation of remote procedure calls (RPC). The binary data layout and how the transferred data is going to be used is of much lesser importance. Such tools focus on speed of data serialization and I/O link transfer rather than on safe handling of malformed data, compile time customization needed by various embedded systems and significantly reducing amount of boilerplate code, which needs to be written to integrate generated code into the product's code base.
The binary communication protocols, which may serve as an API or control interface of the device, on the other hand, require a different approach. Their specification puts major emphasis on binary data layout, what values are being transferred (data units, scaling factor, special values, etc.) and how the other end is expected to behave on certain values (what values are considered to be valid and how to behave on reception of invalid values). It requires having some extra meta-information attached to described data structures, which needs to propagate and be accessible in the generated code. The existing tools either don't have means to specify such meta-information (other than in comments), or don't know what to do with it when provided. As a result, the developer still has to write a significant amount of boilerplate code in order to integrate the generated serialization focused code to be used in binary communication protocol handling.
Required Features
As an embedded C++ developer, I require the following features to be available to me, at least to some extent.
Polymorphic Interfaces Configuration
Usually every message definition is implemented (or code generated) as a separate class. In many cases, there is a common code that is applicable to all such message classes. The proper implementation would be to introduce polymorphic behavior with virtual functions (such as reading message data, writing it, calculating serialization length, etc.). However, creation of full interface to be polymorphic may be impractical. Every application that might use the protocol definition code is different. For example, in case many messages are uni-directional, one side (client) will require polymorphic write (but not read) for such messages, the one side (server) will require the opposite - polymorphic read (but not write). Most of the virtual functions will end up being included in the final binary / image, even if they are unused. It can result in unnecessary code bloat, which may be a problem for embedded systems (especially bare-metal ones).
There is a need for compile time, or at least code generation time configuration of the polymorphic interfaces that are going to be used. In the best case scenario, such configuration should be done per message class.
Extra Meta Information
There may be a significant amount of extra meta information that comes with the protocol definition. For example, one of the protocol fields in one of the messages needs to report a distance between two points. The protocol designer has decided to report the distance in centimeters. Such information will probably be written in plain text in protocol specification and if some third party code generator is used, then this information might end up being in the comment section describing the field or a message. However, in most cases, such information will not end up being in generated code. The developer that needs to integrate the generated code into its business logic needs to manually write some boilerplate code to do the math of conversion into the different distance units (such as meters) relevant to the application being developed.
It is also not very uncommon for the binary protocol being specified at the same time the application that uses it being developed. Imagine that some specific use case pops up during the development, where distance in centimeters doesn't provide sufficient precision. Thanks to the fact that protocol specification hasn't been finalized yet, the developer decides to change the reported distance units from centimeters to millimeters. It means that other developers that might have written their boilerplate code with assumption of distance units being centimeters must be aware of the change and not forget to modify their code.
Some protocol developers also dislike sending floating point numbers "as-is" over the communication link. In many such cases, floating point numbers are multiplied by some predefined value and sent over the I/O links as integers while the remaining fraction after the decimal point is dropped. The other side does the opposite operation on reception of the message, i.e., dividing the received integer by the same predefined value to get the floating point number. Which predefined value to use for such scaling is also meta information, which is not transferred "on the wire" and should be present in the generated or manually written code of the protocol definition.
Also in many cases, the protocol may define some values with special meaning. Let's assume there is a need to communicate a delay in seconds before some event needs to happen. There should be some special value that indicates infinite duration. Usually it is either 0 or maximum possible value of the unsigned type being used. The developer that integrates the generated code into the application needs to know what value it is. There is a need to write at least one extra piece of boilerplate code that wraps the special value and gives it a name. Wouldn't it be better if the generated code contained such helper function already?
Many protocols specify ranges of valid values and expect certain behavior on invalid values. There is an expectation that generated code would provide an ability to inquire that the received value is valid to avoid manually written boilerplate code that checks this information.
The list of such examples with meta-information that is part of the protocol definition, but not transfered as data of the messages can go on and on. There is a need for the generated code to provide the required functionality to be used by the integration developer without any concern of what to do when the meta-information is modified.
Customization of Data Types
Most of the currently available solutions for the protocol code generation use hard-coded types for some particular data structures, such as std::string for strings and/or std::vector for lists. Also in many cases, the generated functions that perform serialization / deserialization receive their input / output as std::istream or std:stream. These data structures may be unsuitable for some applications, especially embedded (including bare-metal) ones.
There is a need to be able to substitute the default data structures with some equivalent replacements, ideally at compile time for selected fields / messages, but globally (during code generation for example) may also be an acceptable solution.
Excluding Exceptions and Dynamic Memory Allocation
Many constrained embedded environments make it difficult to use exceptions as well as dynamic memory allocation (especially bare-metal ones). There is a need for an ability to generate protocol definition code that don't use either of them.
Efficient Built-In Mapping of Message ID to Type
Usually, when new encoded message arrives over I/O link, encoded as raw data, it has some transport framing containing numeric message ID. Unfortunately, most (if not all) of the available protocol code generation solution leave the task of mapping numeric ID into the actual message type (or appropriate handling function) to be written by the developer who integrates the generated code into the business logic. Usually, such code is boilerplate, efficiency of which depends on the competence of the developer.
Third Party Protocol Support
Many of the available protocol generation solutions have their own encoding and framing without any ability to modify it. Using of such tools may be acceptable for new protocols being defined from scratch. However, there are heaps of already defined third party protocols, code for which cannot be generated with such tools.
Injecting Custom Code
This requirement complements the Third Party Protocol Support one. Even if a chosen code generation solution supports definition of the third party protocol and its grammar for schema files is very rich, there will always be a protocol containing some small nuance, which cannot be represented correctly using the available grammar. As a result, the generated code may be incorrect and/or incomplete. Proper protocol code generation solution should allow injection of snippets of custom, manually written code.
[/SHOWTOGROUPS]