swick's blog

Integrating libdex with GDBus


Writing asynchronous code in C has always been a challenge. Traditional callback-based approaches, including GLib’s async/finish pattern, often lead to the so-called callback hell that’s difficult to read and maintain. The libdex library offers a solution to this problem, and I recently worked on expanding the integration with GLib’s GDBus subsystem.

The Problem with the Sync and Async Patterns

Writing C code involving tasks which can take non-trivial amount of time has traditionally required choosing between two approaches:

  1. Synchronous calls - Simple to write but block the current thread
  2. Asynchronous callbacks - Non-blocking but result in callback hell and complex error handling

Often the synchronous variant is chosen to keep the code simple, but in a lot of cases, blocking for potentially multiple seconds is not acceptable. Threads can be used to prevent the other threads from blocking, but it creates parallelism and with it the need for locking. It also can potentially create a huge amount of threads which mostly sit idle.

The asynchronous variant has none of those problems, but consider a typical async D-Bus operation in traditional GLib code:

static void
on_ping_ready (GObject      *source_object,
               GAsyncResult *res,
               gpointer      data)
{
  g_autofree char *pong = NULL;

  if (!dex_dbus_ping_pong_call_ping_finish (DEX_BUS_PING_PONG (source_object),
                                            &pong,
                                            res, NULL))
    return; // handle error

  g_print ("client: %s\n", pong);
}

static void
on_ping_pong_proxy_ready (GObject      *source_object,
                          GAsyncResult *res,
                          gpointer      data)
{
  DexDbusPingPong *pp dex_dbus_ping_pong_proxy_new_finish (res, NULL);
  if (!pp)
    return; // Handle error

  dex_dbus_ping_pong_call_ping (pp, "ping", NULL,
                                on_ping_ready, NULL);
}

This pattern becomes unwieldy quickly, especially with multiple operations, error handling, shared data and cleanup across multiple callbacks.

What is libdex?

Dex provides Future-based programming for GLib. It provides features for application and library authors who want to structure concurrent code in an easy to manage way. Dex also provides Fibers which allow writing synchronous looking code in C while maintaining the benefits of asynchronous execution.

At its core, libdex introduces two key concepts:

  • Futures: Represent values that will be available at some point in the future
  • Fibers: Lightweight cooperative threads that allow writing synchronous-looking code that yields control when waiting for asynchronous operations

Futures alone already simplify dealing with asynchronous code by specefying a call chain (dex_future_then(), dex_future_catch(), and dex_future_finally()), or even more elaborate flows (dex_future_all(), dex_future_all_race(), dex_future_any(), and dex_future_first()) at one place, without the typical callback hell. It still requires splitting things into a bunch of functions and potentially moving data through them.

static DexFuture *
lookup_user_data_cb (DexFuture *future,
                     gpointer   user_data)
{
  g_autoptr(MyUser) user = NULL;
  g_autoptr(GError) error = NULL;

  // the future in this cb is already resolved, so this just gets the value
  // no fibers involved 
  user = dex_await_object (future, &error);
  if (!user)
    return dex_future_new_for_error (g_steal_pointer (&error));

  return dex_future_first (dex_timeout_new_seconds (60),
                           dex_future_any (query_db_server (user),
                                           query_cache_server (user),
                                           NULL),
                           NULL);
}

static void
print_user_data (void)
{
  g_autoptr(DexFuture) future = NULL;

  future = dex_future_then (find_user (), lookup_user_data_cb, NULL, NULL);
  future = dex_future_then (future, print_user_data_cb, NULL, NULL);
  future = dex_future_finally (future, quit_cb, NULL, NULL);

  g_main_loop_run (main_loop);
}

The real magic of libdex however lies in fibers and the dex_await() function, which allows you to write code that looks synchronous but executes asynchronously. When you await a future, the current fiber yields control, allowing other work to proceed while waiting for the result.

g_autoptr(MyUser) user = NULL;
g_autoptr(MyUserData) data = NULL;
g_autoptr(GError) error = NULL;

user = dex_await_object (find_user (), &error);
if (!user)
  return dex_future_new_for_error (g_steal_pointer (&error));

data = dex_await_boxed (dex_future_first (dex_timeout_new_seconds (60),
                                          dex_future_any (query_db_server (user),
                                                          query_cache_server (user),
                                                          NULL),
                                          NULL), &error);
if (!data)
  return dex_future_new_for_error (g_steal_pointer (&error));

g_print ("%s", data->name);

Christian Hergert wrote pretty decent documentation, so check it out!

Bridging libdex and GDBus

With the new integration, you can write D-Bus client code that looks like this:

g_autoptr(DexDbusPingPong) *pp = NULL;
g_autoptr(DexDbusPingPongPingResult) result = NULL;

pp = dex_await_object (dex_dbus_ping_pong_proxy_new_future (connection,
                                                            G_DBUS_PROXY_FLAGS_NONE,
                                                            "org.example.PingPong",
                                                            "/org/example/pingpong"),
                       &error);
if (!pp)
  return dex_future_new_for_error (g_steal_pointer (&error));

res = dex_await_boxed (dex_dbus_ping_pong_call_ping_future (pp, "ping"), &error);
if (!res)
  return dex_future_new_for_error (g_steal_pointer (&error));

g_print ("client: %s\n", res->pong);

This code is executing asynchronously, but reads like synchronous code. Error handling is straightforward, and there are no callbacks involved.

On the service side, if enabled, method handlers will run in a fiber and can use dex_await() directly, enabling complex asynchronous operations within service implementations:

static gboolean
handle_ping (DexDbusPingPong       *object,
             GDBusMethodInvocation *invocation,
             const char            *ping)
{
  g_print ("service: %s\n", ping);

  dex_await (dex_timeout_new_seconds (1), NULL);
  dex_dbus_ping_pong_complete_ping (object, invocation, "pong");

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static void
dex_dbus_ping_pong_iface_init (DexDbusPingPongIface *iface)
{
  iface->handle_ping = handle_ping;
}
pp = g_object_new (DEX_TYPE_PING_PONG, NULL);
dex_dbus_interface_skeleton_set_flags (DEX_DBUS_INTERFACE_SKELETON (pp),
                                       DEX_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_FIBER);

This method handler includes a 1-second delay, but instead of blocking the entire service, it yields control to other fibers during the timeout.

The merge request contains a complete example of a client and service communicating with each other.

Implementation Details

The integration required extending GDBus’s code generation system. Rather than modifying it directly, the current solution introduces a very simple extension system to GDBus’ code generation.

The generated code includes:

  • Future-returning functions: For every _proxy_new() and _call_$method() function, corresponding _future() variants are generated
  • Result types: Method calls return boxed types containing all output parameters
  • Custom skeleton base class: Generated skeleton classes inherit from DexDBusInterfaceSkeleton instead of GDBusInterfaceSkeleton, which implements dispatching method handlers in fibers

Besides the GDBus code generation extension system, there are a few more changes required in GLib to make this work. This is not merged at the time of writing, but I’m confident that we can move this forward.

Future Directions

I hope that this work convinces more people to use libdex! We have a whole bunch of existing code bases which will have to stick with C in the foreseeable future, and libdex provides tools to make incremental improvements. Personally, I want to start using in in the xdg-desktop-portal project.


Do you have a comment?

Toot at me on mastodon or send me a mail!