- Summary
- Documentation
- Roadmap
- Build
- Tutorial (Remote Interfaces)
- Tutorial (Local Interfaces)
- Examples
The DBus Glue library wants to make interfacing with a DBus Service / Interface almost as simple as defining a C++ interface.
The DBus protocol is almost entirely hidden from the user.
This library only enables interfacing with existant DBus APIs. Creating one yourself with this library is not possible!
Example interfaces / Predefined interfaces can be found in my repository dbus-glue-system.
https://5cript.github.io/dbus-glue/
Here is a checkbox list of all the tings to come and all that are done so far.
- A bus object for all necessary bus related tasks.
- Handle the bus lifetime
- Calling methods
- Reading properties
- Writing properties
- Listening for signals
- Starting an event loop
- A Macro for declaring DBus interfaces as C++ interfaces.
- Attaching or creating a (simple) event loop to the bus object. (Note: If you use the sd_event* object system, you still have to setup and teardown the event stuff yourself, this is not wrapped by this library.)
- A rudamentary generator for interfaces. Not really necessary, since writing a simple class declaration is easy and fast, but can be used to get a quick start for big interfaces.
On a created interface, linked to a given DBus interface, you can:
- Call methods.
- Read and Write Properties.
- Connect to slots and listen for signals.
- Call methods and get the results asnchronously.
- Read and Write Properties asynchronously.
With your own registered interface you can:
- Declare your interface
- Expose the interface
- Expose a method
- Expose a property (read, read/write)
- Expose a signal
- Make exposed signal emitable
This project uses cmake.
- cd dbus-glue
- mkdir -p build
- cmake ..
- make -j4 (or more/less cores)
- make install (if you want)
- libsystemd, because the systemd sd-bus library is used underneath.
- boost preprocessor
This is a short tutorial for declaring external interfaces, attaching to them and using them.
- clone the library using git clone
- create a building directory somewhere and build the library, see the build chapter.
- now you can add the "include" directory to your projects search path and link the lib. Alternatively use make install. I personally advocate for a project structure where all git dependecies are parallel in the filesystem to the dependent project.
A very simple program to start off with. First the includes required for basics:
#include <dbus-glue/dbus_interface.hpp>
Here we see a very basic interface. You can use d-feet to inspect and try interfaces provided by services, daemons and other programs.
namespace org::freedesktop
{
// Declare an interface. This one is just reduced to one function, which is enough for this example
class IDBus
{
public:
// We want to make this function callable
virtual auto ListNames() -> std::vector <std::string> = 0;
// Silences a warning, but IDBus is never really used polymorphicly.
virtual ~IDBus() = default;
};
}
Mirror the dbus interface in C++ code. When you dont provide any Methods, Properties or Signals, a special placeholder macro has to be used called "DBUS_DECLARE_NO_*"
DBUS_DECLARE_NAMESPACE
(
(org)(freedesktop),
IDBus,
DBUS_DECLARE_METHODS(ListNames),
DBUS_DECLARE_NO_PROPERTIES,
DBUS_DECLARE_NO_SIGNALS
)
This shows how to use our set up interface:
int main()
{
using namespace DBusGlue;
// open the system bus
auto bus = open_system_bus();
// bind the interface to the remote dbus interface:
auto dbusInterface = create_interface <org::freedesktop::IDBus>(
bus,
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus"
);
// you can now call list names:
auto services = dbusInterface->ListNames();
// print all to console, which aren't stupidly named / numbered, therefore unnamed
for (auto const& service : services)
if (service.front() != ':')
std::cout << service << "\n";
// -> org.freedesktop.DBus
// org.freedesktop.Accounts
// ...
return 0;
}
Now lets change a little bit of the program. We now dont want to do the call synchronously, but asynchronously. Note that as soon as an event handling loop is attached to the bus, even the synchronous calls get processed by the loop and block while its not their turn to be executed, which, in bad cases, can result in "long" wait times at these functions, which may be undersirable. I therefore recommend to switch to an entirely asynchronous architecture when you use any asynchronous methods/signals on the bus.
Asynchronous functions use "continuation" style. Which means that whenever an asynchronous function finishes, a callback is called from which execution can be resumed.
Properties can also be read and written asynchronously, by calling get/set on them with the async flag, just the same as with methods.
Only showing relevant differences to before:
// new required header for the event loop
#include <dbus-glue/bindings/busy_loop.hpp>
int main()
{
using namespace DBusGlue;
using namespace std::chrono_literals;
// open the system bus
auto bus = open_system_bus();
// create an event loop and attach it to the bus.
// This is the default implementation, you can provide your own. For instance by using sd_event.
make_busy_loop(&bus);
// bind the interface to the remote dbus interface:
auto dbusInterface = create_interface <org::freedesktop::IDBus>(
bus,
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus"
);
// call ListNames asynchronously with async_flag overload:
// (always the first parameter, if there are any)
dbusInterface->ListNames(async_flag)
// if you omit the then, you wont get a result / response.
// Which can be fine, but is usually not intended
.then([](auto const& services)
{
// print all to console, which aren't stupidly named / numbered, therefore unnamed
for (auto const& service : services)
if (service.front() != ':')
std::cout << service << "\n";
})
// This is called when something goes wrong
// Can be omited
.error([](auto&, auto const& errorMessage)
{
// first parameter is the result message,
// It probably does not contain anything useful.
std::cerr << errorMessage << "\n";
})
// A timeout for completion, optional.
// default is 10 seconds
.timeout(1s)
;
return 0;
}
Asynchronous calls return a proxy object that can be used to bind callback, error callback and timeout before the call is made. The execution is made when the proxy object gets destroyed, which is after the end of the statement.
So you should ignore the return, and not pass it to a variable, like so:
// DONT!
auto&& temp = dbusInterface.ListNames(async_flag);
unless you actually want that like so:
{ // artifical scope
auto&& temp = dbusInterface.ListNames(async_flag);
temp.then([](auto const&){
std::cout << "callback!\n";
});
} // asynchronous call is made here.
Variants are containers that can contain a number of types. In order to read out of a variant, you have to know its stored type first. Luckily sdbus can deliver this information. One small note: due to implementation restrictions, loading and storing a value from a variant can only be done by a free function and not by a member function. This would look like follows:
// Example for reading a variant.
#include <dbus-glue/dbus_interface.hpp>
#include <dbus-glue/bindings/variant_helpers.hpp>
#include <iostream>
int main()
{
auto bus = open_system_bus();
// a variant dictionary is a map of variants.
variant_dictionary<std::map> dict;
bus.read_properties
(
"org.freedesktop.Accounts",
"/org/freedesktop/Accounts",
"org.freedesktop.Accounts",
dict
);
// In case that you dont know the type beforehand, but have to test first:
auto descriptor = dict["DaemonVersion"].type();
// print it in a somewhat readable way.
std::cout << descriptor.string() << "\n";
std::string ver, ver2;
variant_load<std::string>(dict["DaemonVersion"], ver);
std::cout << ver << "\n";
// You do NOT need to rewind a variant before rereading it, this is done for you.
// dict["DaemonVersion"].rewind();
variant_load<std::string>(dict["DaemonVersion"], ver2);
std::cout << ver2 << "\n";
}
// Example for writing to a variant.
#include <dbus-glue/dbus_interface.hpp>
#include <dbus-glue/bindings/variant_helpers.hpp>
#include <iostream>
using namespace std::string_literals;
int main()
{
auto bus = open_system_bus();
variant var;
// has to be non-member unfortunately
variant_store(bus, var, "hello"s);
std::string val;
variant_load(var, val);
std::cout << val << "\n"; // -> hello
std::cout << std::flush;
return 0;
}
This is the tutorial for declaring your own dbus interface.
On contrary to interfaces that are externally supplied, there is no adapt macro to use. The reason is that a macro to provide all the parameter and return value names on top of method names etc. would be very verbose and hard to debug on errors.
Here we have an interface that we want to expose to the world:
#include <dbus-glue/bindings/exposable_interface.hpp>
// Your interface to export has to derive from exposable_interface.
class MyInterface : public DBusGlue::exposable_interface
{
public:
// these members determine the path and service name
// for registration, do not need to be const literals.
std::string path() const override
{
return "/bluetooth";
}
std::string service() const override
{
return "com.bla.my";
}
public: // Methods
auto DisplayText(std::string const& text) -> void {}
auto Multiply(int lhs, int rhs) -> int {return lhs * rhs;}
public: // Properties
bool IsThisCool;
public: // Signals
// this is called emitable, not signal as in adapted interfaces.
emitable <void(*)(int)> FireMe{this, "FireMe"};
};
#include <dbus-glue/interface_builder.hpp>
int main()
{
auto bus = open_user_bus();
using namespace DBusGlue;
using namespace ExposeHelpers;
// creates an instance of MyInterface that can be used.
auto shared_ptr_to_interface = make_interface <MyInterface>(
// Multiply Method
DBusGlue::exposable_method_factory{} <<
name("Multiply") << // Method name
result("Product") << // Name can only be a single word!
parameter(0, "a") << // Name can only be a single word!
parameter(1, "b") << // Parameter number is optional, but if supplied, all should have it supplied.
as(&MyInterface::Multiply),
// Display Text Method
DBusGlue::exposable_method_factory{} <<
name("DisplayText") <<
result("Nothing") <<
parameter("text") <<
as(&MyInterface::DisplayText),
// IsThisCool Property
DBusGlue::exposable_property_factory{} <<
name("IsThisCool") <<
writeable(true) <<
as(&MyInterface::IsThisCool),
// FireMe Signal
DBusGlue::exposable_signal_factory{} <<
// name is in emitable constructor, not needed here.
// d-feet does not show signal parameter names
parameter("integral") <<
as(&MyInterface::FireMe)
);
// The bus takes a share to hold the interface and exposes it on the bus.
auto result = bus.expose_interface(shared_ptr_to_interface);
if (result < 0)
{
std::cerr << strerror(-result);
return 1;
}
result = sd_bus_request_name(static_cast <sd_bus*> (bus), "com.bla.my", 0);
if (result < 0)
{
std::cerr << strerror(-result);
return 1;
}
make_busy_loop(&bus, 200ms);
bus.loop <busy_loop>()->error_callback([](int, std::string const& msg){
std::cerr << msg << std::endl;
return true;
});
// emit signal
exposed->FireMe.emit(5);
// prevent immediate exit here however you like.
std::cin.get();
}
More examples are in the example directory
Here is the first example, to show a basis of what this library wants to do.
#include <dbus-glue/dbus_interface.hpp>
#include <iostream>
#include <vector>
#include <string>
using namespace DBusGlue;
/**
* @brief The IAccounts interface. Its the provided interface (org.freedesktop.Accounts) as a C++ class.
*/
class IAccounts
{
public:
virtual ~IAccounts() = default;
virtual auto CacheUser(std::string const& name) -> object_path = 0;
virtual auto CreateUser(std::string const& name, std::string const& fullname, int32_t accountType) -> object_path = 0;
virtual auto DeleteUser(int64_t id, bool removeFiles) -> void = 0;
virtual auto FindUserById(int64_t id) -> object_path = 0;
virtual auto ListCachedUsers() -> std::vector <object_path> = 0;
virtual auto UncacheUser(std::string const& user) -> void = 0;
public: // Properties
readable <std::vector <object_path>> AutomaticLoginUsers;
readable <bool> HasMultipleUsers;
readable <bool> HasNoUsers;
readable <std::string> DaemonVersion;
public: // signals
DBusGlue::signal <void(object_path)> UserAdded;
DBusGlue::signal <void(object_path)> UserDeleted;
};
//----------------------------------------------------------------------------------------
// This step is necessary to enable interface auto-implementation.
// There is a limit to how many properterties and methods are possible. (currently either 64 or 255 each, haven't tried, assume 64)
// This limit can be circumvented by DBUS_DECLARE_N. Which allows to make the same interface more than once.
// A successory call to DBUS_DECLARE_ZIP merges them all together.
DBUS_DECLARE
(
IAccounts,
DBUS_DECLARE_METHODS(CacheUser, CreateUser, DeleteUser, FindUserById, ListCachedUsers, UncacheUser),
DBUS_DECLARE_PROPERTIES(AutomaticLoginUsers, HasMultipleUsers, HasNoUsers, DaemonVersion),
DBUS_DECLARE_SIGNALS(UserAdded, UserDeleted)
)
//----------------------------------------------------------------------------------------
int main()
{
// open the system bus.
auto bus = open_system_bus();
try
{
// attach interface to remote interface.
auto user_control = create_interface <IAccounts> (bus, "org.freedesktop.Accounts", "/org/freedesktop/Accounts", "org.freedesktop.Accounts");
// calling a method with parameters
//user_control.CreateUser("hello", "hello", 0);
// calling a method and getting the result
auto cachedUsers = user_control->ListCachedUsers();
for (auto const& user : cachedUsers)
{
// the object_path type has a stream operator for output
std::cout << user << "\n";
}
// reading a property
std::cout << user_control->DaemonVersion << "\n";
}
catch (std::exception const& exc) // catch all possible exceptions.
{
std::cout << exc.what() << "\n";
}
return 0;
}
Here is an example on how to listen to emitted signals. Note that signal handling requires an event loop.
#include <dbus-glue/dbus_interface.hpp>
#include <dbus-glue/bindings/bus.hpp>
#include <dbus-glue/bindings/busy_loop.hpp>
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
using namespace DBusGlue;
using namespace std::chrono_literals;
/**
* @brief The IAccounts interface. Its the provided interface (org.freedesktop.Accounts) as a C++ class.
*/
class IAccounts
{
public:
virtual ~IAccounts() = default;
public: // Methods
virtual auto CreateUser(std::string const& name, std::string const& fullname, int32_t accountType) -> object_path = 0;
virtual auto DeleteUser(int64_t id, bool removeFiles) -> void = 0;
public: // Properties
public: // signals
DBusGlue::signal <void(object_path)> UserAdded;
DBusGlue::signal <void(object_path)> UserDeleted;
};
//----------------------------------------------------------------------------------------
// This step is necessary to enable interface auto-implementation.
// There is a limit to how many properterties and methods are possible. (currently either 64 or 255 each, haven't tried, assume 64)
// This limit can be circumvented by DBUS_DECLARE_N. Which allows to make the same interface more than once.
// A successory call to DBUS_DECLARE_ZIP merges them all together.
DBUS_DECLARE
(
IAccounts,
DBUS_DECLARE_METHODS(CreateUser, DeleteUser),
DBUS_DECLARE_NO_PROPERTIES,
DBUS_DECLARE_SIGNALS(UserAdded, UserDeleted)
)
int main()
{
auto bus = open_system_bus();
try
{
bus.install_event_loop(std::unique_ptr <event_loop> (new busy_loop(&bus, 50ms)));
// wrapped interface for creating / deleting accounts.
auto accountControl = create_interface <IAccounts>(
bus,
"org.freedesktop.Accounts",
"/org/freedesktop/Accounts",
"org.freedesktop.Accounts"
);
accountControl->UserAdded.listen(
[](object_path const& p) {
// success callback
std::cout << "callback - create: " << p << std::endl;
},
[](message&, std::string const& str) {
// failure callback
std::cerr << "oh no something gone wrong: " << str << "\n";
}
);
// WARNING! Passing "release_slot" forces you to manage the slots lifetime yourself!
// You can use this variation to manage lifetimes of your observed signals.
std::unique_ptr<void, void(*)(void*)> slot = accountControl->UserDeleted.listen(
[&](object_path const& p) {
// this is called from the dbus system.
std::cout << "callback - delete: " << p << std::endl;
// create a user from here.
auto path = accountControl->CreateUser("tempus", "tempus", 0);
},
[](message&, std::string const& str) {
// this is called when an error got signaled into our callback.
std::cerr << "oh no something gone wrong: " << str << "\n";
},
DBusGlue::release_slot
);
// try to delete a user with id 1001. WARNING, DONT JUST DELETE SOME USER ON YOUR SYSTEM. obviously...
try {
// commented out in case you just run this example
// you should get the id from the name first.
//accountControl->DeleteUser(1001, false);
} catch (std::exception const& exc) {
// Create the user if he doesn't exist
accountControl->CreateUser("tempus", "tempus", 0);
std::cout << exc.what() << std::endl;
}
// just wait here so we dont exit directly
std::cin.get();
}
catch (std::exception const& exc)
{
std::cout << exc.what() << "\n";
}
return 0;
}