-
Notifications
You must be signed in to change notification settings - Fork 0
Home
This is an implementation of messaging design pattern that allows zero-overhead message passing between modules. Each messaging system is made up of an instance of messenger, a set of module instances, and a set of messages.
The messenger handles the message passing to and between modules. Each messaging system should have exactly one messenger. Messenger types is a generic type that should be specialized using the types of modules. The messenger handles the instantiation of each module and calls the default constructor for each module. Messenger is defined in messenger.inl file. The following example demonstrates an example of messenger type specialization.
#pragma once
// The definition of Messenger::Messenger
#include "messenger.inl"
// The definition of Test::ModuleA
#include "test-module-a.inl"
// The definition of Test::ModuleB
#include "test-module-b.inl"
namespace Test
{
// The specialization of Messenger for our use with proper modules.
typedef Messenger::Messenger<Test::ModuleA, Test::ModuleB> TestMessenger;
}
The following example demonstrates the instantiation of a messenger and the process of passing a message to it. It also demonstrates an access to module. The access to a module is done via the index of the module using the get_module method as demonstrated below.
// Includes the Test::TestMessenger type for the messenger.
#include "test-messenger.inl"
// The entery point of the program.
int main()
{
// Instantiation of messenger.
Test::TestMessenger test_messenger;
// Pass an int message to messenger.
test_messenger.pass_message(12);
// Example of accessing a module.
std::cout << "The last int message Module A recieved had value: " << test_messenger.get_module<0>().last_recieved_message << std::endl;
return 0;
}
The messages are passed to all the modules that have the message processor of the message type. The messenger checks for proper message handler on the modules at compile time, which mean the are no runtime overhead for choosing the message processors.
Modules are normal classes/structs that are equipped with proper message processors. Each message processor, processes a certain type of a message. There are three ways to declare a message processor, which is demonstrated in the following examples.
#pragma once
// Used STL libraries
#include <string>
#include <iostream>
namespace Test
{
// Defintion of Module A
class ModuleA
{
// The structure that is passed as a message. This type is processed by the module itself so it does not need to be visible to other modules.
struct Report
{
// The message holds a string that the processesor will print.
std::string report;
};
public:
int last_recieved_message;
// This is a message processor that will process every message that is of type int.
// A message processor is any function named "process_message" that can take either
// a single parameter of const lvalue reference of message type, or a const lvalue
// reference of a messanger as the first parameter and a const lvalue referemce of
// message type as the second parameter and returns a tuple of messages.
auto process_message(const int& message)
{
last_recieved_message = message;
Report report;
report.report = "Processed an int message.";
// The returned tuple holds a set of messages that will be passed to modules all over again.
// It is important to note that the order of messages in tuple defines the order in which the messages are passed.
// In this case first the report, a message of type ModuleA::Report is passed to all the modules, and then a message
// of type std::string is passed to all the module.
return std::make_tuple(report, std::to_string(message));
}
// This is a message processor that will process every message that is of type
//ModuleA::Report.
auto process_message(const Report& message) -> std::tuple<>
{
std::cout << "Module A: " << message.report << std::endl;
// There are no messages to be passed to modules after returning.
return std::tuple<>();
}
};
}
#pragma once
// Used STL libraries
#include <string>
#include <iostream>
namespace Test
{
// Defintion of Module B
class ModuleB
{
// The structure that is passed as a message. This type is processed by the module itself so it does not need to be visible to other modules.
struct Report
{
std::string report;
};
public:
// This is a message processor that takes a messenger and a message, since messenger
// type is not availbe here, a template parameter is used for messenger type.
template <typename T> auto process_message(const T& messenger, const std::string& message)
{
Report report;
report.report = "Recieved a std::string message.";
// Use messenger to pass a message before returning.
messenger.pass_message(report);
std::cout << message << std::endl;
// There are no messages to be passed after returning.
return std::tuple<>();
}
// A message processor for messages of type ModuleB::Report.
template <typename T> auto process_message(const T& messenger, const Report& message)
{
std::cout << "Module B: " << message.report << std::endl;
// There are no messages to be passed after returning.
return std::tuple<>();
}
};
}
Any object of any type can be passed as a message. It is to be noticed that the messages are passed as constant Lvalue references. The modules that are to process the message are inferred by the type of the message. This means every module that has a message processor that is capable of processing a message of certain type, will receive the message.