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:

UDBus::Connection conn;
UDBus::Error error;

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:
    Connection() = default;
    explicit 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;

    Message pop_message() noexcept;

    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 send_with_reply_and_block(Message& message, int timeout_milliseconds, Error& error) noexcept;

    ~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:
    Error();
    explicit 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:

UDBus::Connection connection;
UDBus::Error error;

connection.bus_get(DBUS_BUS_SESSION, error);
if (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:

UDBus::Message message;
message.new_method_call("com.example.Echo", "/com/example/Echo", "com.example.Echo", "Test");

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:
    Message() = default;
    explicit 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>
    MessageGetResult handleMethodCall(const char* interface, const char* method, Type<T, T2...>& t) noexcept;

    template<typename T, typename... T2>
    MessageGetResult handleSignal(const char* interface, const char* method, Type<T, T2...>& t) noexcept;

    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
    DBusMessage* get() noexcept;

    // 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
    DBusMessage** getMessagePointer() noexcept;

    ~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:

UDBus::MessageBuilder builder(message);
builder << data;

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";

message << a << bt << test << actions;

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:

message << UDBus::BeginStruct 
            << 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:

message << UDBus::BeginStruct 
            << 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:

builder << UDBus::BeginArray;
for (int i = 0; i < 3; i++)
{
    builder << UDBus::Next
            << UDBus::BeginStruct
                << a
                << expire
                << UDBus::BeginVariant
                    << UDBus::BeginStruct
                        << test
                        << bt
                    << UDBus::EndStruct
                << UDBus::EndVariant
                << a
            << UDBus::EndStruct;
}
builder << UDBus::EndArray;

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" };
builder << UDBus::BeginArray;
for (size_t i = 0; i < 3; i++)
{
    builder << UDBus::Next
            << 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;
}
builder << UDBus::EndArray;

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:

builder << UDBus::BeginArray;
for (...)
{
    builder << UDBus::Next
            << ...;
}
builder << UDBus::EndArray;

Sending the message

To send a message, we need to do the following:

  1. Complete your message builder
  2. Create a UDBus::PendingCall object
  3. Send the message
  4. Flush the connection
  5. 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:

builder << UDBus::EndMessage;

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:
    PendingCall() = default;

    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:

UDBus::PendingCall pending;
connection.send_with_reply(message, pending, -1); // Last argument is a milliseconds timeout. -1 for no timeout
connection.flush();

After that, we need to block on the pending call and getting the reply as a message:

pending.block();

UDBus::Message reply;
reply.pending_call_steal_reply(pending);
if (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
            }
      )
   ]