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;

    // ostream style << operator. Simply calls append
    template<typename T>
    Message& operator<<(const T& t) 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;
};

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

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;

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:

UDBus::ArrayBuilder complex(message);
complex << 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
        << UDBus::BeginStruct
            << a
            << expire
            << UDBus::BeginVariant
                << UDBus::BeginStruct
                   << test
                   << bt
                << UDBus::EndStruct
            << UDBus::EndVariant
            << a
        << UDBus::EndStruct << UDBus::Next;

UDBus::ArrayBuilder complex2(message);
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
         << UDBus::BeginDictEntry << a << UDBus::BeginVariant << complex << UDBus::EndVariant << UDBus::EndDictEntry << UDBus::Next;

message << complex2;

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:

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

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
               }
            ]
      )
   ]