Stackless Coroutines

Stackless Coroutines

Libdex supports two coroutine-like execution models:

  • DexFiber for stackful execution with its own stack.
  • DexCoroutine for stackless execution using an explicit state machine.

DexCoroutine is a DexFuture that runs on a DexScheduler. Use it when you want linear async-style flow without paying for a full fiber stack.

When To Use This Over a Fiber

Use a coroutine when all of these are true:

  • The task is mostly “then/await-like” and spends most of its time waiting for futures.
  • You want low-overhead scheduling and smaller per-task memory footprint.
  • You are comfortable with explicit resume-state style code.

Prefer a DexFiber when:

  • You need deep call stacks, existing recursive control flow, or third-party functions that expect normal stackful execution.

How to Run A Coroutine

Use dex_scheduler_spawn_coroutine() to schedule a coroutine.

DexFuture *
my_coroutine (DexCoroutineContext *context,
              gpointer             user_data)
{
  DEX_COROUTINE_BEGIN (context);

  /* return a future when done */
  return dex_future_new_true ();

  DEX_COROUTINE_END;
}

DexFuture *future =
  dex_scheduler_spawn_coroutine (NULL,
                                 my_coroutine,
                                 user_data,
                                 user_data_destroy);

If scheduler is NULL, the default scheduler is used.

The Function Contract

Coroutine entrypoints use DexCoroutineFunc:

  • Return a DexFuture to complete.
  • Return NULL only to indicate that the coroutine is suspended.
  • Store any state you need in the provided user_data.

The recommended pattern is:

  • define any state as a small struct (or with DEX_DEFINE_CLOSURE_TYPE);
  • call DEX_COROUTINE_BEGIN at the start;
  • call DEX_COROUTINE_SUSPEND when you only need to wait for completion;
  • call DEX_COROUTINE_SUSPEND_* for each awaited step;
  • return a future at completion;
  • end with DEX_COROUTINE_END.

Waiting on Futures Correctly

Use DEX_COROUTINE_SUSPEND_BOOLEAN and related macros instead of calling dex_await_*() directly.

dex_await_*() is a fiber-first API and in practice cannot be used like normal code unless the future is already done. Coroutine suspend helpers:

  • save the awaited future in the coroutine state;
  • return NULL to yield;
  • resume execution only after that future settles;
  • extract the awaited value on resume.

Use DEX_COROUTINE_SUSPEND when you only need to wait for a future to finish and do not need its result. This is the coroutine-friendly replacement for dex_await (future, NULL).

These macros are available for common await types:

  • DEX_COROUTINE_SUSPEND
  • DEX_COROUTINE_SUSPEND_BOOLEAN
  • DEX_COROUTINE_SUSPEND_INT, DEX_COROUTINE_SUSPEND_UINT
  • DEX_COROUTINE_SUSPEND_INT64, DEX_COROUTINE_SUSPEND_UINT64
  • DEX_COROUTINE_SUSPEND_OBJECT, DEX_COROUTINE_SUSPEND_BOXED
  • DEX_COROUTINE_SUSPEND_POINTER
  • DEX_COROUTINE_SUSPEND_ENUM, DEX_COROUTINE_SUSPEND_FLAGS
  • DEX_COROUTINE_SUSPEND_DOUBLE, DEX_COROUTINE_SUSPEND_FLOAT

Common Correctness Pattern

Keep coroutine state explicit.

DEX_COROUTINE_SUSPEND_* assumes the state machine stores both the continuation label and the currently pending future. A typical helper is:

DEX_DEFINE_CLOSURE_TYPE (LoadState, load_state,
                         DEX_DEFINE_CLOSURE_OBJECT (GFile, file),
                         DEX_DEFINE_CLOSURE_OBJECT (GFileInputStream, input),
                         DEX_DEFINE_CLOSURE_OBJECT (GFileInfo, info),
                         DEX_DEFINE_CLOSURE_VALUE (int, io_priority))

static DexFuture *
load_with_cache (DexCoroutineContext *context,
                 gpointer             user_data)
{
  LoadState *state = user_data;
  g_autoptr(GError) error = NULL;

  DEX_COROUTINE_BEGIN (context);

  DEX_COROUTINE_SUSPEND_OBJECT (&state->input, &error,
                                dex_file_read (state->file, state->io_priority));
  if (error != NULL)
    return dex_future_new_for_error (g_steal_pointer (&error));

  DEX_COROUTINE_SUSPEND_OBJECT (&state->info, &error,
                                dex_file_input_stream_query_info (state->input,
                                                                  G_FILE_ATTRIBUTE_STANDARD_SIZE,
                                                                  state->io_priority));
  if (error != NULL)
    return dex_future_new_for_error (g_steal_pointer (&error));

  return dex_future_new_int64 (g_file_info_get_size (state->info));

  DEX_COROUTINE_END;
}

Use DEX_DEFINE_CLOSURE_VALUE_WITH_CLEAR() for inline value types that need address-based cleanup, such as GWeakRef:

DEX_DEFINE_CLOSURE_TYPE (WatchState, watch_state,
                         DEX_DEFINE_CLOSURE_VALUE_WITH_CLEAR (GWeakRef,
                                                              widget_ref,
                                                              g_weak_ref_clear))

Cleanup And Cancellation

Like all DexFuture values, coroutines follow normal ownership and cancellation rules.

  • If a coroutine is no longer depended on, discarding may cancel its work.
  • Cancellation will propagate into an error for suspended coroutines which they can handle when they are next resumed.
  • If work must complete even after callers drop references, use dex_future_disown() on the returned future.
  • State registered with the coroutine is released with the configured callback function when the DexCoroutine completes.

When Not To Use

Do not use coroutines for arbitrary synchronous or blocking loops. Blocking work should be offloaded with dex_thread_spawn() or DexThreadPool.