a message/signal libary for modern C++.
Requires C++ 17.
lk::msg has three different parts: A Listener
, a Message
and a Channel
.
The concept is clear by the pure english meaning of these terms: A listener listens for a message on a channel.
A message in this case is any object, wrapped within std::any
, it could even be a lambda.
A listener has to register to a channel in order to start getting messages. This is handled in an RAII way, as a Listener has to be created with a Channel as an argument, and will automatically register / unregister when created / destroyed respectively. When a message is received, each listener passes the message on to its handler, which is a std::function
object, assigned on construction. Channels are enforced to be created as std::shared_ptr
, so that all listeners keep it alive, and a Listener will never listen on a channel that doesn't exist. A Channel can, however, be deactivated by being closed.
For specific Linux / Windows instructions, see below.
This project uses CMake.
- Configure CMake. Useful options are:
CMAKE_BUILD_TYPE
, which can either beDebug
orRelease
.LK_MESSAGES_STATIC
, which should beON
for a static library build, andOFF
(default) for a dynamic library build.
- Build. The output folder is referred to as
$bin
in this guide. - Add the
include
folder to the include directories of your compiler/build system of choice, and$bin
to your link directories. - Include
lk/messages.h
and link againstlkmsg
.
This guide assumes you have basic knowledge of the terminal and assumes a shell like sh
.
- Run
cmake
with-DCMAKE_BUILD_TYPE=Release
. You can specify-DLK_MESSAGES_STATIC=ON
to buildlk::msg
as a static library. by default a dynamic library is built. - Run
make
. - You now have
liblkmsg.so
orliblkmsg.a
, depending on whetherLK_MESSAGES_STATIC
was set in cmake. - Add
include
to your build systems include paths - Add folder in which
make
was run to link paths. - Link against
lkmsg
, for example with-llkmsg
.
Use Visual Studio, CLion or other IDE that supports CMake projects.
- Open / Import this project.
- Configure CMake. Useful options are:
CMAKE_BUILD_TYPE
, which can either beDebug
orRelease
.LK_MESSAGES_STATIC
, which should beON
for a static library build, andOFF
(default) for a dynamic library build.
- Build.
- The output folder now has the library
.dll
or.lib
file, which you can link to.include
should be added to include paths.
In general, it is recommended that you using namespace lk
, if msg
does not conflict with other namespaces in your project.
lk::msg
uses the following concepts:
- Channel: Connects listeners to messages. This is the base of all communication in
lk::msg
. A Channel is instantiated like so:
lk::msg::Channel::Ptr my_channel = lk::msg::Channel::create();
This creates a shared (refcounted) pointer to a channel. This is the only way to construct a Channel.
- Listener: Connects to a Channel to listen to new messages. Invokes a callback/handler on message receive.
lk::msg::Listener my_listener(my_channel, my_handler);
Note that the constructor takes a lk::msg::Channel::Ptr
by value. This ensures that the Channel is kept alive (through refcounting) as long as listeners exist.
my_handler
is a function of signature void(const lk::msg::Message&)
. You may pass nullptr
for this and set the handler later via my_listener.set_handler(my_handler);
.
- Message: A lightweight object encoding the type of message (as an
int
), and the message data withstd::any
.
lk::msg::Message my_message(1, std::string("Hello, World!"));
bool success = my_channel.send(std::move(my_message));
Note that the message is moved into Channel::send()
- this design clarifies that the channel now owns the message, and once its been sent to every listener, it will be destroyed.
The first argument, 1
in this case, is of type int
. This is to be used, with an enum instead of a raw int
for example, to identify what type of message it is, i.e. what its purpose is.
Note that, to identify which type is held by the std::any
member data
, std::any::type
can be compared to typeid(xyz)
to find out whether a cast from data
to type xyz
is valid. This purpose
member of Message is useful because two different messages (like a hello and a goodbye message) could both have a string as data (i.e. std::any::type
), yet be different kinds of messages which are to be handled differently. In this case, the std::any::type
is the same, but the Message::purpose
is not.
- MultiListener: A single object managing multiple listeners to multiple channels, receiving all messages in a single handler. Can be used like a normal Listener, but will listen to multiple channels at the same time. If this finds frequent use, then Channels likely have not been used properly, and multiple channels can probably be merged into one. A MultiListener is useful for logging all ongoing messages in the program, but if its used for other things, there is likely an inefficient use of Channels.
My favourite way to use this library is to use classes & structs as the data of the Message. Example struct you could pass in a message:
enum MyMessageType {
Hello,
Update,
Goodbye
};
struct SimpleMessage {
Object sender;
Object receiver;
std::string message;
};
// ...
using namespace lk;
SimpleMessage msg { ... };
msg::Message hello_message(MessageType::Hello, msg);
channel->send(std::move(hello_message));
This way, on the receiving end, you can std::any_cast
it back to a SimpleMessage
, and thus pass as complex a message as you want.