• Getting on the (D)Bus
  • Creating a Message
    • Append or Iter
    • Adding basic values
    • Adding arrays
    • Handling a Map
    • Finishing the message
  • Sending the message
    • Handling a reply
  • The End
    • End Notes

Sending a Notification on a Linux Desktop

Getting on the (D)Bus

DBus is the defacto form of interprocess communication (IPC) for the Linux desktop. It is used by Gnome and KDE, the two biggest players in the Linux desktop market.

There are a few libraries that you can use to interact with DBus. There are Python bindings and support for GLib and QT which covers the GTK and KDE respectively. You can also use the raw C API. For the purposes of this post, we will be using the underlying C api with Zig. Zig makes it easy to import C apis and use them directly:

const dbus = @cImport({
    @cInclude("dbus/dbus.h");
});

Note

This will require you to have the dbus headers in the include path.

By the end this post, we will develop the code to the point where we will send a desktop notification. I am assuming that you have access to a desktop that runs a Freedesktop Notification compliant service. You will also need the Zig 0.14.0 compiler.

Our first hurdle is to connect to one of the DBus busses. There are two kinds of connections: system and session. The system is more for the key systems that run the operating system. This is mainly for things that interact with hardware or kernel. The session bus handles software attached to your login session. These are usually programs that you launch your self or that your desktop needs to run. To send a notification, we need to connect with the Session bus.

Add the following to you’re main function:

    const conn = dbus.dbus_bus_get(dbus.DBUS_BUS_SESSION, null);
    if (conn == null) {
        std.debug.print("Could not open a connection\n", .{});
        return;
    }
    defer dbus.dbus_connection_unref(conn);

You’ll notice that I added an unref call and not a dbus_connection_close. The dbus library handles closing the connection once all references have been released. You will get an runtime error if you try and close the connection, but make sure to clean up your reference by calling unref on the connection.

Creating a Message

Now thatwe have a connection to the Session bus, we can create our message. It turns out that the message does not depend on the connection until you go to send it, so you could also create the message before connecting. This allows for some flexibility since building a message requires allocations that can fail, so depending on your use case, it might make more sense to build the message first.

There are several different types of DBus messages: Method Calls, Method Returns, Signals and Errors. For sending a notification, we only need to deal with the first one (for now). For a method call you will need 3 things:

  • The destination service name
  • The DBus path for the service
  • The interface method to call

Thankfully the Notification Spec is well layed out and we know each of these.

  • Service name: org.freedesktop.Notifications
  • DBus path: /org/freedesktop/Notifications
  • Interface Method: org.freedesktop.Notifications.Notify

With this information we can now construct a method call:

    const message = dbus.dbus_message_new_method_call(
        // The service name
        "org.freedesktop.Notifications",
        // The object path
        "/org/freedesktop/Notifications",
        // The interface
        "org.freedesktop.Notifications",
        // and the method
        "Notify",
    );

From there we can start populating the contents of the message.

Append or Iter

Now that we have a message, there are 2 ways to go about adding content. We can append items directly using dbus_message_append_args as long as the values are simple types like strings, ints, or arrays of simple types.

In the case of Notification, we need to be able to add Hints, and in order to do that, we need to use an Iterator. Getting an iterator for the root is simple. Calling dbus_message_iter_init_append will do the trick. You will need to allocate space for the iterator first, whether on the stack or heap. Since the iterator is short lived, it is fine to set space on the stack and pass the address to this method. For convinience and type checking purposes, I set up a single pointer to that stack space that will be referenced as we append data to the iterator.

    var root_raw: dbus.DBusMessageIter = undefined;
    const root = &root_raw;
    dbus.dbus_message_iter_init_append(message, root);

Adding basic values

The iterator api has 2 main ways to add basic values. Most are added with dbus_message_iter_append_basic method. This can be used for all basic values like strings and integers. One thing to be aware of here is that this method expects pointers to the values, not the values them selves. That’s why you have to specify the type as the second parameter.

This will make a bit more sense with an example. Below is how we would add the first 5 values for a notification:

    const app_name: [:0]const u8 = "App Name";
    var success = dbus.dbus_message_iter_append_basic(root, dbus.DBUS_TYPE_STRING, @ptrCast(&app_name));
    var no_replace: u32 = 0;
    success = dbus.dbus_message_iter_append_basic(root, dbus.DBUS_TYPE_UINT32, &no_replace);
    const icon: [:0]const u8 = "";
    success = dbus.dbus_message_iter_append_basic(root, dbus.DBUS_TYPE_STRING, @ptrCast(&icon.ptr));
    // You can also pass the string constants in directly
    success = dbus.dbus_message_iter_append_basic(root, dbus.DBUS_TYPE_STRING, @ptrCast(&"Summary"));
    success = dbus.dbus_message_iter_append_basic(root, dbus.DBUS_TYPE_STRING, @ptrCast(&"Description"));

Note

For this examble, I am ignoring the return value. For a more robust implementation, you will want to make sure that the method returns TRUE which means that data was allocated correctly. Otherwise you are out of memory and the message won’t send.

Adding arrays

Now we come to our first complex type: actions. Notification actions are an array of string pairs. The first of the pair is the action name, the second is the display name. You came have more than one pair, but there always need to be a multiple of 2 strings in the array.

In order to write an array into the message, you have to open a “container”. Containers can be other types as well, but for now we only need an array. We also have to specify the array type. In this case we are sending strings which have the signature “s”. Once we have the container open, we have to remember to close it. Zig makes this convinient with defer. Then we add each string like normal.

    {
        var action_iter: dbus.DBusMessageIter = undefined;
        success = dbus.dbus_message_iter_open_container(root, dbus.DBUS_TYPE_ARRAY, "s", &action_iter);
        defer _ = dbus.dbus_message_iter_close_container(root, &action_iter);

        const actions = [_][*]const u8{
            "action",
            "Some Action",
        };

        for (actions) |action| {
            success = dbus.dbus_message_iter_append_basic(&action_iter, dbus.DBUS_TYPE_STRING, @ptrCast(&action));
        }
    }

Note

In this example, we only handle the happy path, namely that the appending won’t fail. In a more robust implementation, you would likely want to abort the container if you had any failures.

Handling a Map

Notification servers can implement extra features known as hints. There are a number of well defined hints in the specification and each notification server should document their specific hints. One that is widely supported is urgency. This allows you to determine the priority of the notification and allows the server to handle things like “Do Not Disturb” mode or things like that.

The challenging part is that this is a Map. Or more specifically an array of containers that are a key/value pair. It’s a little confusing at first, so lets put this into our example here.

The first thing we need to do is open an array. This is very similar to what we had to do for actions, but this time we need a different signature: “{sv}”. Simply put, this signature says that we will have a “Dict Entry” (the “{}” part of the signature), with a string key, and a variant value. The variant part means that it can be any DBus data type, but it has to be packaged in such a way as to signal what type it is. When adding the variant, you have to provide it’s type.

The urgency hint has a value that is a byte variant. This is indicated with the "y" string. It can be a 0 for low, 1 for normal, and 2 for critical.

        // Open the container for all hints.
        var hints_iter: dbus.DBusMessageIter = undefined;
        success = dbus.dbus_message_iter_open_container(root, dbus.DBUS_TYPE_ARRAY, "{sv}", &hints_iter);
        defer _ = dbus.dbus_message_iter_close_container(root, &hints_iter);

        {
            // This container holds one hint entry. If you were adding many hints this would probably
            // be inside a loop
            var hint_iter: dbus.DBusMessageIter = undefined;
            success = dbus.dbus_message_iter_open_container(&hints_iter, dbus.DBUS_TYPE_DICT_ENTRY, null, &hint_iter);
            defer _ = dbus.dbus_message_iter_close_container(&hints_iter, &hint_iter);

            success = dbus.dbus_message_iter_append_basic(&hint_iter, dbus.DBUS_TYPE_STRING, @ptrCast(&"urgency"));

            {
                // A variant is apparently a container 🤷
                // Notice that we specify the contents with the type string "y" indicating a byte.
                var variant_iter: dbus.DBusMessageIter = undefined;
                success = dbus.dbus_message_iter_open_container(&hint_iter, dbus.DBUS_TYPE_VARIANT, "y", &variant_iter);
                defer _ = dbus.dbus_message_iter_close_container(&hint_iter, &variant_iter);

                var val = dbus.DBusBasicValue{
                    .byt = 1, // Normal urgency
                };
                _ = dbus.dbus_message_iter_append_basic(&variant_iter, dbus.DBUS_TYPE_BYTE, @ptrCast(&val));
            }
        }

Finishing the message

To finish the Notification message, we have one more field. We provide an expiration time for the notification. This is another basic value, so as we did for the first handful of values, we add an integer to the message.

    var expire_timeout: i32 = -1; // Default timeout
    success = dbus.dbus_message_iter_append_basic(root, dbus.DBUS_TYPE_INT32, &expire_timeout);

Sending the message

Now that we have all of that out of the way, we need to get that message submitted to the bus. There are a few options here, depending on your event loop and tolerance for blocking calls. DBus by nature is asyncronous, and most of the send methods are non-blocking. You submit the message and at a later time check back for a reply.

In our case, we will do the simplest method of blocking until we get a reply. This is done with the dbus_connection_send_with_reply_and_block I know, rather verbose, but at least you know exactly what you are getting yourself into.

    const reply = dbus.dbus_connection_send_with_reply_and_block(conn, message, dbus.DBUS_TIMEOUT_USE_DEFAULT, @ptrCast(&err));
    // Null check and value capture

Handling a reply

In the case of a notification, we are expecting a reply. This is a rather simple message, but it will help to illustrate how to get a value out of a message. Similar to writing a message, we can read basic values with the dbus_message_get_args and recieve basic values through pointers. If we need a more complex value, we would employ DBusMessageIter again but this time using the read methods.

    // Null check and value capture
    if (reply) |msg| {
        var id: dbus.DBusBasicValue = undefined;
        success = dbus.dbus_message_get_args(msg, null, dbus.DBUS_TYPE_UINT32, &id, dbus.DBUS_TYPE_INVALID);
        std.debug.print("Notification ID: {}\n", .{id.u32});
    } else {
        std.debug.print("Error sending: {s}\n==\n{s}\n", .{ std.mem.span(err.name), std.mem.span(err.message) });
    }

The End

With all those pieces in place, you should be able to compile and run your program. You should get a simple notification through your notification server.


End Notes

Download Example

You can run the file with the following command. If your dbus headers are in a different location, you will need to update them to match your system install.

$ zig run -lc \
      -I/usr/include/dbus-1.0 \
      -I/usr/lib64/dbus-1.0/include \
      -L/usr/lib64 \
      -ldbus-1 \
      dbusMessage.zig