The InOutput and Step instances inside the Pipeline can communicate with each other through messages stored in the InOutputMessageBus or the TaskMessageBus. Note that the Messages do not derive from any base-class such as Message (and are therefore written in italics and not verbatim)! Any class or struct can be used as a message on a MessageBus, as long as it provides two public fields and a serialization function (which may be empty):

1static constexpr uint32_t message_type_id;
2static std::string GetMessageName();
4template <class Archive>
5void serialize(Archive& archive)

The message_type_id should be a number (unique for each type) and is used to retrieve a Message of a specific type from the MessageBus quickly. The GetMessageName() function should return a std::string with the type name of the Message and is used in error messages. The DEFINE_MPMCA_PIPELINE_MESSAGE_ID macro is provided to help you define these two fields. For example, placing the following line inside a struct definition:


will be expanded into:

1static constexpr uint32_t message_type_id = 0x281F0CAC; // actual crc32 might be different
2static std::string GetMessageName() { return "StructName"; };

The macro accepts one or more input arguments, from which it will calculate a unique hash (CRC32) to be used as message_type_id and a concatenated string to be used as message name. The macro will convert numbers into characters when generating the name, which can be used to create a unique message_type_id and message name for templated structs that take one or more numbers as template arguments. For example:

 1template <size_t N>
 2struct SomeMessageType {
 4    DEFINE_MPMCA_PIPELINE_MESSAGE_ID("ASillyExample<", N, ">");
 6    std::array<int, N> some_data_member;
 7    std::array<float, N> some_other_data_member;
 9    template <class Archive>
10    void serialize(Archive& archive)
11    {
12        archive(some_data_member, some_other_data_member);
13    }

will expand into something similar to the following for the type SomeMessageType<6>:

1static constexpr uint32_t message_type_id = 0x281F0CAC; // actual crc32 might be different
2static std::string GetMessageName() { return "ASillyExample<6>";};

For more information on serialization for datalogging using the Cereal serialization library, see this page.


The Pipeline contains two separate MessageBus objects: the TaskMessageBus and the InOutputMessageBus. The TaskMessageBus is used for communication between InOutput and Task instances during the MainTick() and TaskCompleted() calls. The InOutputMessageBus is used for communication between InOutput instances during the Tick() calls.

Instances of messages on the MessageBus are typically constructed and placed on the MessageBus before the Pipeline starts performing updates. That is, for performance reasons, it is undesired to construct, push and pop the same types of messages onto the MessageBus on every update cycle, as this typically involves expensive memory allocation calls. Instead, the messages are constructed and pushed onto the MessageBus during the Pipeline::Prepare() call by InOutput and Step instances, and merely updated during the periodic update calls. During the Pipeline::Prepare() call, every InOutput and Step instance will be given the opportunity to

  1. create messages and push them onto the MessageBus, inside the PrepareTaskMessageBus() and PrepareInOutputMessageBus() callbacks, and then
  2. check whether the messages it requires for proper functionality were indeed added to the MessageBus, inside the CheckTaskMessageBus() and CheckInOutputMessageBus() callbacks.

Typically, the InOutput and Step instances that will update information on a Message should create and push Messages, and instances that merely read information should check whether the Messages were indeed added. As such, the MessageBus is a somewhat specific implementation of the classic publish-subscribe design pattern.