D-Bus: the why
There are times when you have a number of projects written in different programming languages running in parallel, and you need them to communicate with each other. If it happens that your programs are all running on the same machine (i.e. not distributed) and that machine has a Linux OS, DBus turns out to provide a suitable solution: synchronous, asynchronous, and signal messages with bindings in various programming languages.
D-Bus: the what
We will talk about buses, interfaces, methods, and signals (not about properties though!) so make sure that you know all about the basics before you move on.
In sequel we build a simple class providing a service with a single method which emits a signal when invoked. Another class is also given to demonstrate how remote methods are called and emitted signals are received. For this tutorial the dbus-cpp
which is ‘‘a header-only dbus-binding leveraging C++-11’’ is used.
D-Bus: the how
The service, our interface
First and foremost we define how our interface (service.hpp
) looks like:
#include <string>
#include <chrono>
#include <core/dbus/bus.h>
#include <core/dbus/traits/service.h>
struct QuaintousService {
struct Method {
typedef QuaintousService Interface;
inline static const std::string& name() {
static const std::string s {"Command"};
return s;
}
inline static const std::chrono::milliseconds default_timeout() {
return std::chrono::milliseconds{10};
}
};
struct Signals {
struct Status {
static std::string name() {
return "Status";
};
typedef QuaintousService Interface;
typedef std::string ArgumentType;
};
};
};
The code above might look a bit strange at first glance, but I have tried to stay loyal to the examples provided by the dbus-cpp
library and by the end of this tutorial we will see that it is pretty smart approach actually! The snippet above defines a service with a single method called Command
and a signal called Status
. Note that in contrast to signals the arguments of a method are not specified.
The next step is to register our service as an interface with the D-Bus. This is done as follows:
namespace core {
namespace dbus {
namespace traits {
template<>
struct Service<Service> {
inline static const std::string& interface_name()
{
static const std::string s {"com.quaintous.service"};
return s;
}
};
}
}
}
The provider
Now we create a simple class (provider.cpp
) that handles the method calls on our Command
method of com.quaintous.com
interface and can emit the Status
signal (only the source is provided here, the header is pretty easy to figure out!):
#include <memory>
#include <thread>
#include <signal.h>
#include <core/dbus/dbus.h>
#include <core/dbus/asio/executor.h>
#include <core/dbus/service.h>
#include <core/dbus/types/stl/string.h>
#include <service.hpp>
using namespace core::dbus;
Bus::Ptr Provider::BUS = std::make_shared<Bus>(WellKnownBus::session);
Provider::Provider() : command_handlers(), allHandler() {
BUS->install_executor(asio::make_executor(BUS));
/// Create service on the bus
service::Ptr service = Service::add_service<QuaintousService>(BUS);
/// Add object
this->obj =
service->add_object_for_path(types::ObjectPath("/com/quaintous/service"));
/// Add method handler
this->obj->install_method_handler<Service::Method>(
std::bind(&Provider::handle_command, this, std::placeholders::_1));
/// Pointer to signal
this->sig = this->obj->get_signal<QuaintousService::Signals::Status>();
BUS->run();
};
void Provider::handle_command(const Message::Ptr& msg) {
std::string content;
msg->reader() >> content;
/// Just to demonstrate how signaling works!
this->sig->emit(content);
auto reply = Message::make_method_return(msg);
reply->writer() << "Pong: " << content;
BUS->send(reply);
};
Instantiating the Provider
class starts the D-Bus event loop, so it’s a good idea to create a new thread for it!
The consumer
Now we will create a class (consumer.cpp
) which calls the Command
method synchronously and retrieves the Status
signals:
#include <core/dbus/asio/executor.h>
#include <core/dbus/service.h>
#include <core/dbus/types/stl/string.h>
#include <core/dbus/result.h>
#include <service.hpp>
using namespace core::dbus;
Bus::Ptr Consumer::BUS = std::make_shared<Bus>(WellKnownBus::session);
Consumer::Consumer() {
BUS->install_executor(asio::make_executor(BUS));
// Fetch the stub from the Bus
Service::Ptr stub_service = Service::use_service<QuaintousService>(BUS);
stub = stub_service->object_for_path(types::ObjectPath("/com/quaintous/service"));
// Register for Status signal
Signal<QuaintousService::Signals::Status, std::string>::Ptr status_signal =
stub->get_signal<QuaintousService::Signals::Status>();
status_signal->connect([=](std::string status) {
/// Do whatever you wanna do!
});
/// Invoke `Command` method
Result<std::string> result =
stub->invoke_method_synchronously<QuaintousService::Method, std::string>("Ping");
BUS->run();
}
As it was the case with Provider
, instantiating Consumer
also starts D-Bus’ event loop.
Putting it altogether
Having a service registered with D-Bus and get it up and running is pretty straight forward:
- Define methods, signals, and properties (wasn’t discussed here) as in
service.hpp
. - Register method handlers for defined methods as in
provider.cpp
Using methods provided by an interface or listening to specific signals as in consumer.cpp
is also not complicated:
- Fetch stub of the desired interface residing on a specific path (e.g.
/com/quaintous/service
) - Invoke stub methods synchronously or asynchronously
- Register handlers for desired signals
Conclusion
From my own experience I can tell that It made my work a lot easier to use D-Bus as my main inter processing communication (IPC) mean. I glued an HTTP server written in Node.js with an embedded GUI written in C++ and had my commands serialized to an embedded board with its controller written in python!