In this guide, we'll build a small client application that sends a message to the DBus bus.
Caution
Make sure you know how the DBus type system works. Official dbus documentation.
Setting up your testing environment
Since we don't want to depend on any services in this guide, we'll use utilities provided by DBus for testing how messages are sent. In a terminal window, run the following command:
user $ dbus-test-tool echo --name=com.example.Echo --session
This will create a DBus server on the session bus that can receive any arguments. It will not produce any output, but you can leave it running while testing your application.
In another terminal window, run the following:
user $ dbus-monitor "interface='com.example.Echo'"
This application will monitor any messages that are sent to the
com.example.Echo
application and will print out the data
with type information.
Connecting to the Bus
First, we need to connect to the bus. In your main function, create the following variables:
::Connection conn;
UDBus::Error error; UDBus
They will be used for connecting to the bus and checking if we have connected successfully.
The UDBus::Connection
class
The Connection
class provides an abstraction on top of
the DBusConnection
type by wrapping it in an RAII
container. It looks like this:
class Connection
{
public:
() = default;
Connectionexplicit Connection(DBusConnection* conn) noexcept;
operator DBusConnection*() noexcept;
void bus_get(DBusBusType type, Error& error) noexcept;
void bus_get_private(DBusBusType type, Error& error) noexcept;
[[nodiscard]] int request_name(const char* name, unsigned int flags, Error& error) noexcept;
udbus_bool_t read_write(int timeout_milliseconds) noexcept;
udbus_bool_t read_write_dispatch(int timeout_milliseconds) noexcept;
() noexcept;
Message pop_message
void open(const char* address, Error& error) noexcept;
void open_private(const char* address, Error& error) noexcept;
void ref(Connection& conn) noexcept;
void ref(DBusConnection* conn) noexcept;
void unref() noexcept;
void close() noexcept;
void flush() noexcept;
udbus_bool_t send(Message& message, dbus_uint32_t* client_serial) noexcept;
udbus_bool_t send_with_reply(Message& message, PendingCall& pending_return, int timeout_milliseconds) noexcept;
(Message& message, int timeout_milliseconds, Error& error) noexcept;
Message send_with_reply_and_block
~Connection() noexcept;
};
As you can see, the member functions are simply the same functions
that the DBusConnection
type is used in, except as members
and without the prefix.
Note
The DBusConnection*
implicit conversion operator. It
allows you to pass a class instance to a standard dbus-1
function that accepts a DBusConnection*
.
The UDBus::Error
class
The Error
class is also an RAII abstraction on top of
DBusError
. It looks like this:
class Error
{
public:
();
Errorexplicit Error(const DBusError& err) noexcept;
operator DBusError*() noexcept;
void set(const char* name, const char* message) noexcept;
static void move(DBusError* src, DBusError* dest) noexcept;
static void move(Error& src, Error& dest) noexcept;
bool has_name(const char* name) const noexcept;
bool is_set() noexcept;
[[nodiscard]] const char* name() const noexcept;
[[nodiscard]] const char* message() const noexcept;
void free() noexcept;
~Error();
};
Here, you can also convert the wrapper into its underlying type using the implicit conversion operator
Establishing a connection
To establish a connection, simply call
UDBus::Connection::bus_get()
like this:
::Connection connection;
UDBus::Error error;
UDBus
.bus_get(DBUS_BUS_SESSION, error);
connectionif (error.is_set())
{
std::cout << error.message() << std::endl;
return 1;
}
This will establish a connection on the session bus and will check if the connection was established successfully.
Initiating a method call
Next, we need to initiate a DBus method call on our test interface.
To do that, we need to create an instance of UDBus::Message
and call UDBus::Message::new_method_call
, like this:
::Message message;
UDBus.new_method_call("com.example.Echo", "/com/example/Echo", "com.example.Echo", "Test"); message
The UDBus::Message
class
The Message
class is an abstraction on top of the
DBusMessage
type that provides RAII and additional
utilities for appending data to a message. It looks like this:
class Message
{
public:
() = default;
Messageexplicit Message(DBusMessage* msg) noexcept;
operator DBusMessage*() noexcept;
void new_1(int messageType) noexcept;
void new_method_call(const char* bus_name, const char* path, const char* interface, const char* func) noexcept;
void new_method_return(Message& method_call) noexcept;
void new_method_return(DBusMessage* method_call) noexcept;
void new_signal(const char* path, const char* interface, const char* name) noexcept;
void new_error(Message& reply_to, const char* error_name, const char* error_message) noexcept;
void new_error_raw(DBusMessage* reply_to, const char* error_name, const char* error_message) noexcept;
template<typename T, typename... T2>
(const char* interface, const char* method, Type<T, T2...>& t) noexcept;
MessageGetResult handleMethodCall
template<typename T, typename... T2>
(const char* interface, const char* method, Type<T, T2...>& t) noexcept;
MessageGetResult handleSignal
void copy(Message& reply_to) noexcept;
void copy(DBusMessage* reply_to) noexcept;
void ref(Message& reply_to) noexcept;
void ref(DBusMessage* reply_to) noexcept;
void unref() noexcept;
void demarshal(const char* str, int len, DBusError* error) noexcept;
void pending_call_steal_reply(DBusPendingCall* pending) noexcept;
udbus_bool_t is_valid() noexcept;
udbus_bool_t is_method_call(const char* iface, const char* method) noexcept;
udbus_bool_t is_signal(const char* iface, const char* method) noexcept;
int get_type() noexcept;
const char* get_error_name() noexcept;
udbus_bool_t set_error_name(const char* name) noexcept;
// Use this to pass to function arguments
* get() noexcept;
DBusMessage
// Use this to assign to a function returning a raw dbus message pointer. It's preferred to use the
// "UDBUS_GET_MESSAGE" macro, as it will make your code more concise and less syntax heavy
** getMessagePointer() noexcept;
DBusMessage
~Message() noexcept;
template<typename T>
void append(const T& t) noexcept;
template<typename T>
void append(const std::vector<T>& t) noexcept;
void setUserPointer(void* ptr) noexcept;
};
Note
Functions with a postfix of _raw
or _1
are
named so due to C++ rules on function overloading. Raw functions, i.e.
functions postfixed with _raw
take a raw
dbus-1
type, instead of our custom type.
You can also use the implicit conversion operator to pass the
underlying DBusMessage*
to standard dbus-1
functions.
Message builders
To enable easy serialisation of data, the library implements the
MessageBuilder
class. An instance of this class has to be
initialised with a ready-to-send message. After that, serialisation is
done in a C++ stream-like fashion:
::MessageBuilder builder(message);
UDBus<< data; builder
Appending simple data to the method call
You can use the overload of operator<<
to append
data to the method call. Simple data structures, such as basic types or
arrays of basic types, can be pushed directly, for example:
dbus_uint32_t a = 0;
udbus_bool_t bt = true; // Note the custom type
std::vector<char*> actions = { (char*)"default" };
const char* test = "test";
<< a << bt << test << actions; message
Caution
Always use udbus_bool_t
to represent booleans when using
the library!
Appending structures
We also allow appending structures using stream modifiers. Example code:
<< UDBus::BeginStruct
message << a
<< bt
<< test
<< UDBus::BeginStruct
<< actions
<< test
<< UDBus::EndStruct
<< UDBus::EndStruct;
Note that you can easily nest structs.
Appending variants
You can also append variants using stream modifiers. Example code:
<< UDBus::BeginStruct
message << a
<< bt
<< test
<< UDBus::BeginVariant
<< UDBus::BeginStruct
<< actions
<< test
<< UDBus::EndStruct
<< UDBus::EndVariant
<< UDBus::EndStruct;
Complex arrays and dictionaries
To append a complex array use the UDBus::BeginArray
and
UDBus::EndArray
modifiers and the UDBus::Next
modifier to complete the current element and move on to the next.
Here is an example of a complex array of types:
<< UDBus::BeginArray;
builder for (int i = 0; i < 3; i++)
{
<< UDBus::Next
builder << UDBus::BeginStruct
<< a
<< expire
<< UDBus::BeginVariant
<< UDBus::BeginStruct
<< test
<< bt
<< UDBus::EndStruct
<< UDBus::EndVariant
<< a
<< UDBus::EndStruct;
}
<< UDBus::EndArray; builder
Meanwhile, dictionaries are the same as arrays, but every element is
wrapped in a block of UDBus::BeginDictEntry
and
UDBus::EndDictEntry
. For example:
const char* keys[3] = { "Key 1", "Key 2", "Key 3" };
<< UDBus::BeginArray;
builder for (size_t i = 0; i < 3; i++)
{
<< UDBus::Next
builder << UDBus::BeginDictEntry
<< keys[i]
<< UDBus::BeginVariant
<< UDBus::BeginStruct
<< a
<< expire
<< UDBus::BeginVariant
<< UDBus::BeginStruct
<< test
<< bt
<< UDBus::EndStruct
<< UDBus::EndVariant
<< a
<< UDBus::EndStruct
<< UDBus::EndVariant
<< UDBus::EndDictEntry;
}
<< UDBus::EndArray; builder
Caution
Due to the architecture of the array builder portion, you should not
leave trailing calls to UDBus::Next
as this will result in
an internal error. Your loops should instead look like this:
<< UDBus::BeginArray;
builder for (...)
{
<< UDBus::Next
builder << ...;
}
<< UDBus::EndArray; builder
Sending the message
To send a message, we need to do the following:
- Complete your message builder
- Create a
UDBus::PendingCall
object - Send the message
- Flush the connection
- Get the message reply
Completing your message
To complete your message and make it ready to send, add the
UDBus::EndMessage
modifier to your message builder:
<< UDBus::EndMessage; builder
Once this is called you can safely destroy your message builder object.
The UDBus::PendingCall
class
A pending call represents a method call that hasn't recieved a reply yet. It looks like this:
class PendingCall
{
public:
() = default;
PendingCall
operator DBusPendingCall*() noexcept;
operator DBusPendingCall**() noexcept;
void ref(DBusPendingCall* p) noexcept;
void ref(PendingCall& p) noexcept;
void unref() noexcept;
udbus_bool_t set_notify(DBusPendingCallNotifyFunction function, void* user_data, DBusFreeFunction free_user_data) noexcept;
void cancel() noexcept;
udbus_bool_t get_completed() noexcept;
void block() noexcept;
udbus_bool_t set_data(dbus_int32_t slot, void* data, DBusFreeFunction free_data_func) noexcept;
void* get_data(dbus_int32_t slot) noexcept;
~PendingCall() noexcept;
};
Sending a message
To send a message, we need to call
UDBus::Connection::send_with_reply
and
UDBus::Connection::flush
, like this:
::PendingCall pending;
UDBus.send_with_reply(message, pending, -1); // Last argument is a milliseconds timeout. -1 for no timeout
connection.flush(); connection
After that, we need to block on the pending call and getting the reply as a message:
.block();
pending
::Message reply;
UDBus.pending_call_steal_reply(pending);
replyif (reply.get_type() == DBUS_MESSAGE_TYPE_ERROR
{
std::cout << reply.get_error_name() << std::endl;
return 2;
}
Result
If successful, running the above program should output the following
code in dbus-monitor
:
signal time=1755390122.803349 sender=org.freedesktop.DBus -> destination=:1.295 serial=4294967295 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameAcquired
string ":1.295"
signal time=1755390122.803402 sender=org.freedesktop.DBus -> destination=:1.295 serial=4294967295 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameLost
string ":1.295"
method call time=1755390125.273487 sender=:1.296 -> destination=com.example.Echo serial=2 path=/com/example/Echo; interface=com.example.Echo; member=Test
uint32 0
boolean true
string "test"
array [
string "default"
string "test"
]
double 2.56
struct {
uint32 0
boolean true
string "test"
variant struct {
array [
string "default"
string "test"
]
string "test"
}
variant struct {
variant struct {
array [
string "default"
string "test"
]
string "test"
}
array [
string "default"
string "test"
]
string "test"
variant struct {
array [
string "default"
string "test"
]
string "test"
}
}
}
array [
struct {
uint32 0
double 2.56
variant struct {
string "test"
boolean true
}
uint32 0
}
struct {
uint32 0
double 2.56
variant struct {
string "test"
boolean true
}
uint32 0
}
struct {
uint32 0
double 2.56
variant struct {
string "test"
boolean true
}
uint32 0
}
]
array [
dict entry(
string "Key 1"
variant struct {
uint32 0
double 2.56
variant struct {
string "test"
boolean true
}
uint32 0
}
)
dict entry(
string "Key 2"
variant struct {
uint32 0
double 2.56
variant struct {
string "test"
boolean true
}
uint32 0
}
)
dict entry(
string "Key 3"
variant struct {
uint32 0
double 2.56
variant struct {
string "test"
boolean true
}
uint32 0
}
)
]