Skip to content

Add single-threaded EventLoop that multiplexes D-Bus and fd sources - #21

Merged
jwinarske merged 1 commit into
mainfrom
jw/event-loop
Jul 12, 2026
Merged

Add single-threaded EventLoop that multiplexes D-Bus and fd sources#21
jwinarske merged 1 commit into
mainfrom
jw/event-loop

Conversation

@jwinarske

Copy link
Copy Markdown
Owner

First step of the event-loop source contract that was scoped earlier. This PR adds the machinery only — no client is converted yet.

What

  • EventSourcefd() + events() + dispatch(revents), invoked on the loop thread.
  • EventLoop — drives an sdbus::IConnection via its external-loop poll data (getEventLoopPollData() / processPendingEvent()) alongside a set of EventSources, all on the thread that calls run(). This is the alternative to enterEventLoopAsync(): because D-Bus callbacks and device/fd I/O run on one thread, state shared between them needs no locking.
    • stop(code) is async-signal-safe and thread-safe (writes an eventfd to break poll()), so it can be driven from a signal handler or another thread.
    • add() / remove() defer to the next iteration, so they are safe to call from within a dispatch() (controller plug/unplug does exactly this). Removed sources must outlive the iteration — documented on the method.

Why

It's the foundation for retiring the per-client worker-thread + std::mutex pattern in the BlueZ clients — the same fragmentation that PR #18's races came from. sdbus-c++ supports exactly this via getEventLoopPollData()/processPendingEvent(), so the D-Bus connection, UdevMonitor, and each InputReader can eventually share one loop.

Validation

Opt-in self-test (-DSDBUS_CPP_EXAMPLES_BUILD_TESTS=ON, wired into CTest) drives the live system bus with two fd sources on a single thread and asserts:

  1. a semaphore-eventfd source is dispatched once per token (3/3),
  2. an async ListNames reply is delivered by run() — proving the loop drives processPendingEvent (125 names, ok),
  3. stop() unblocks run() and returns 0,

with a timerfd as a hard deadline so it can never hang. Passes repeatably; ctest 100%. Verified clean under clang-format --Werror and clang-tidy-19.

Notes

  • The test is opt-in and not built by CI (the option is off by default, so the test TU isn't in CI's compile_commands.json); it needs a reachable system bus and is intended for local/CTest runs. It skips gracefully with success if no bus is present.
  • The one NOLINT(performance-unnecessary-value-param) in the test marks an API-mandated by-value parameter: sdbus deduces reply-handler argument types via function_traits and requires std::optional<sdbus::Error> by value (a const-ref signature fails to compile), matching every existing async handler in the tree.

Next

Step 2 — convert UdevMonitor and InputReader into EventSources and move the three BlueZ clients onto EventLoop, deleting their threads and mutexes. That step first needs the design decision flagged in the scoping: whether synchronous D-Bus calls inside handlers stay synchronous (simplest, but a slow call stalls input reading) or move to async / a dedicated HID read thread.

Foundation for retiring the per-client worker-thread + mutex pattern in the
BlueZ clients. EventLoop drives an sdbus::IConnection via its poll data
(getEventLoopPollData / processPendingEvent) together with a set of EventSource
objects, so D-Bus callbacks and device/fd I/O run on one thread and state
shared between them needs no locking.

- EventSource: fd() + events() + dispatch(revents), invoked on the loop thread.
- EventLoop::run(bus) polls the bus fd/eventFd plus every registered source;
  stop() is async-signal-safe and thread-safe via an eventfd; add()/remove()
  defer to the next iteration so they are safe to call from within a
  dispatch().
- Opt-in self-test (-DSDBUS_CPP_EXAMPLES_BUILD_TESTS=ON) drives the system bus
  with two fd sources on one thread and asserts token dispatch, async-reply
  delivery, and a clean stop, with a timerfd deadline so it cannot hang.

No client uses it yet; converting the BlueZ clients is a follow-up.

Signed-off-by: Joel Winarske <joel.winarske@linux.com>
@jwinarske
jwinarske merged commit 42698ea into main Jul 12, 2026
6 checks passed
@jwinarske
jwinarske deleted the jw/event-loop branch July 12, 2026 21:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant