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;
// ostream style << operator. Simply calls append
template<typename T>
& operator<<(const T& t) noexcept;
Message
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;
};
As you can see, the UDBus::Message
class also provides
an std::ostream
-style overload for
operator<<
that allows you to easily append data to a
message.
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.
Appending simple data to the method call
As said in the above section, 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;
Appending dictionaries and complex arrays
Basic arrays are trivial to push to a message, however complex arrays
need to provide more data to the library. To make this process easier,
we have implemented a class called ArrayBuilder
. It also
has a std::ostream
-like API and uses the same stream
modifiers as the Message
class. Before use, initialise it
with a message through its constructor.
Example:
::ArrayBuilder complex(message);
UDBus<< UDBus::BeginStruct
complex << a
<< expire
<< UDBus::BeginVariant
<< UDBus::BeginStruct
<< test
<< bt
<< UDBus::EndStruct
<< UDBus::EndVariant
<< a
<< UDBus::EndStruct << UDBus::Next
<< UDBus::BeginStruct
<< a
<< expire
<< UDBus::BeginVariant
<< UDBus::BeginStruct
<< test
<< bt
<< UDBus::EndStruct
<< UDBus::EndVariant
<< a
<< UDBus::EndStruct << UDBus::Next
<< UDBus::BeginStruct
<< a
<< expire
<< UDBus::BeginVariant
<< UDBus::BeginStruct
<< test
<< bt
<< UDBus::EndStruct
<< UDBus::EndVariant
<< a
<< UDBus::EndStruct << UDBus::Next;
::ArrayBuilder complex2(message);
UDBus<< UDBus::BeginDictEntry << a << UDBus::BeginVariant << complex << UDBus::EndVariant << UDBus::EndDictEntry << UDBus::Next
complex2 << UDBus::BeginDictEntry << a << UDBus::BeginVariant << complex << UDBus::EndVariant << UDBus::EndDictEntry << UDBus::Next
<< UDBus::BeginDictEntry << a << UDBus::BeginVariant << complex << UDBus::EndVariant << UDBus::EndDictEntry << UDBus::Next
<< UDBus::BeginDictEntry << a << UDBus::BeginVariant << complex << UDBus::EndVariant << UDBus::EndDictEntry << UDBus::Next
<< UDBus::BeginDictEntry << a << UDBus::BeginVariant << complex << UDBus::EndVariant << UDBus::EndDictEntry << UDBus::Next;
<< complex2; message
As you can see, dictionaries are appended using the
UDBus::BeginDictEntry
and UDBus::EndDictEntry
stream modifiers.
The type of an array is determined by the type of the first element.
Once UDBus::Next
is called, all data after that should be
of the same type.
Sending the message
To send a message, we need to do the following:
- Create a
UDBus::PendingCall
object - Send the message
- Flush the connection
- Get the message reply
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
:
method call time=1716739479.008182 sender=:1.61554 -> destination=com.example.Echo serial=2 path=/com/example/Echo; interface=com.example.Echo; member=Test
string "test"
uint32 0
string "arst"
string "a test notification"
array [
string "default"
]
array [
uint16 0
uint16 1
uint16 2
uint16 3
]
array [
dict entry(
uint32 0
variant array [
struct {
uint32 0
int32 -1
variant struct {
string "test"
boolean true
}
uint32 0
}
struct {
uint32 0
int32 -1
variant struct {
string "test"
boolean true
}
uint32 0
}
struct {
uint32 0
int32 -1
variant struct {
string "test"
boolean true
}
uint32 0
}
]
)
dict entry(
uint32 0
variant array [
struct {
uint32 0
int32 -1
variant struct {
string "test"
boolean true
}
uint32 0
}
struct {
uint32 0
int32 -1
variant struct {
string "test"
boolean true
}
uint32 0
}
struct {
uint32 0
int32 -1
variant struct {
string "test"
boolean true
}
uint32 0
}
]
)
dict entry(
uint32 0
variant array [
struct {
uint32 0
int32 -1
variant struct {
string "test"
boolean true
}
uint32 0
}
struct {
uint32 0
int32 -1
variant struct {
string "test"
boolean true
}
uint32 0
}
struct {
uint32 0
int32 -1
variant struct {
string "test"
boolean true
}
uint32 0
}
]
)
dict entry(
uint32 0
variant array [
struct {
uint32 0
int32 -1
variant struct {
string "test"
boolean true
}
uint32 0
}
struct {
uint32 0
int32 -1
variant struct {
string "test"
boolean true
}
uint32 0
}
struct {
uint32 0
int32 -1
variant struct {
string "test"
boolean true
}
uint32 0
}
]
)
dict entry(
uint32 0
variant array [
struct {
uint32 0
int32 -1
variant struct {
string "test"
boolean true
}
uint32 0
}
struct {
uint32 0
int32 -1
variant struct {
string "test"
boolean true
}
uint32 0
}
struct {
uint32 0
int32 -1
variant struct {
string "test"
boolean true
}
uint32 0
}
]
)
]