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:

  1. Define methods, signals, and properties (wasn’t discussed here) as in service.hpp.
  2. 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:

  1. Fetch stub of the desired interface residing on a specific path (e.g. /com/quaintous/service)
  2. Invoke stub methods synchronously or asynchronously
  3. 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!