morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
qt_executor.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <QCoreApplication>
5#include <QMetaObject>
6#include <core/async/ExecutorContext.hpp>
7#include <functional>
8#include <memory>
9#include <morph/attributes.hpp>
10#include <morph/core/executor.hpp>
11#include <utility>
12
13namespace morph::qt {
14
15/// @brief `IExecutor` implementation that posts tasks to a Qt event loop.
16///
17/// Uses `QMetaObject::invokeMethod` with `Qt::QueuedConnection` so the callable
18/// always runs on the thread that owns the configured context object — by
19/// default `QCoreApplication::instance()`, i.e. the GUI thread.
20///
21/// Thread-safe: `post()` may be called from any thread. No `QObject` subclass is
22/// required by the caller.
23///
24/// Each delivered task runs inside a `core::async::ExecutorScope` naming this
25/// executor, so `morph::exec::runningOn(*this)` is true inside it.
26///
27/// **A task still queued when this executor is destroyed is dropped, not run.**
28/// `post()` enqueues and returns; it does not run the callable, and the queued
29/// Qt event outlives the `post()` call that created it. Joining the worker pool
30/// that made the call proves the `post()` *happened* — never that the event was
31/// *delivered*. Without a guard, an event still on the Qt queue at teardown is
32/// delivered against a freed executor and reads `_context` off freed memory.
33///
34/// `Bridge::executeVia` reaches that state on an ordinary run: it chains three
35/// `Completion` objects per dispatched action, each settled from inside the
36/// previous one's delivered callback, so a caller waiting only on its own
37/// top-level completion can observe "done" while an intermediate post is still
38/// queued. When that stale event is pumped, its body calls `post()` for the
39/// next link in the chain — against a freed executor. It segfaults on an
40/// uninstrumented build, not only under a sanitizer.
41///
42/// Each queued task therefore carries a weak observer of this executor's
43/// lifetime and does nothing if the executor is already gone. Dropping is the
44/// correct outcome rather than a lesser evil: a chain being torn down has
45/// nobody left to observe its result, and `Completion`'s orphan logging already
46/// covers a completion that never resolves.
47///
48/// **Boundary of the guarantee.** The check assumes this executor is destroyed
49/// on the same thread that runs its context's event loop, which holds for every
50/// owner in this repository. Destroying one from another thread while its loop
51/// is mid-delivery still needs external synchronisation: the token covers
52/// teardown with events still queued, not a genuine cross-thread race.
54public:
55 /// @brief Constructs an executor that posts tasks to @p context's thread.
56 ///
57 /// @param context `QObject` whose owning thread's event loop runs posted
58 /// tasks; `QMetaObject::invokeMethod` dispatches to whichever thread
59 /// `context->thread()` reports at the time each task is posted, so tasks
60 /// posted after `context` is moved to a different thread run there.
61 /// Defaults to `QCoreApplication::instance()`, i.e. the GUI thread.
62 /// Passing `nullptr` (e.g. when constructed
63 /// before `QCoreApplication` exists) makes `post()` a no-op, matching
64 /// `QMetaObject::invokeMethod`'s own handling of a null target. Borrowed,
65 /// not owned: a non-null @p context must outlive this executor.
66 explicit QtExecutor(QObject* context MORPH_LIFETIMEBOUND = QCoreApplication::instance())
67 : _context{context}, _alive{std::make_shared<char>()} {}
68
69 QtExecutor(const QtExecutor&) = delete;
70 QtExecutor& operator=(const QtExecutor&) = delete;
71 QtExecutor(QtExecutor&&) = delete;
72 QtExecutor& operator=(QtExecutor&&) = delete;
73
74 /// @brief Destroys the lifetime token, so any still-queued task becomes a no-op.
75 ~QtExecutor() override = default;
76
77 /// @brief Posts @p fn to the Qt event loop for execution on the context's thread.
78 ///
79 /// Returns immediately. @p fn is invoked asynchronously once the event loop
80 /// processes the queued event.
81 ///
82 /// Runs only if this executor is still alive when the event is delivered;
83 /// see the class docstring.
84 ///
85 /// @param fn Callable to execute on the context object's thread.
86 void post(std::function<void()> fn) override {
87 QMetaObject::invokeMethod(
88 _context,
89 [this, alive = std::weak_ptr<const char>{_alive}, fn = std::move(fn)]() mutable {
90 if (alive.expired()) {
91 return;
92 }
93 // `this` is safe to use here for the reason the alive check is
94 // sufficient: this executor is destroyed on the thread that
95 // delivers this event, so it cannot be destroyed between the
96 // check and the use.
97 ::core::async::ExecutorScope const scope{coreExecutor()};
98 fn();
99 },
100 Qt::QueuedConnection);
101 }
102
103private:
104 QObject* _context;
105
106 // The pointee is never read -- only the control block matters, so that a
107 // queued task can ask whether this executor still exists. `make_shared`
108 // keeps it to a single allocation.
109 std::shared_ptr<const char> _alive;
110};
111
112} // namespace morph::qt
IExecutor implementation that posts tasks to a Qt event loop.
~QtExecutor() override=default
Destroys the lifetime token, so any still-queued task becomes a no-op.
void post(std::function< void()> fn) override
Posts fn to the Qt event loop for execution on the context's thread.
QtExecutor(QObject *context=QCoreApplication::instance())
Constructs an executor that posts tasks to context's thread.
Abstract executor interface.
Definition executor.hpp:70
::core::async::IExecutor & coreExecutor() noexcept
This executor as a core-cpp executor: the identity an core::async::ExecutorScope names.
Definition executor.hpp:100