morph
0.1.0
Asynchronous bridge between GUI thread and business models
Toggle main menu visibility
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
13
namespace
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.
53
class
QtExecutor
:
public
::morph::exec::IExecutor
{
54
public
:
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
103
private
:
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
morph::qt::QtExecutor
IExecutor implementation that posts tasks to a Qt event loop.
Definition
qt_executor.hpp:53
morph::qt::QtExecutor::~QtExecutor
~QtExecutor() override=default
Destroys the lifetime token, so any still-queued task becomes a no-op.
morph::qt::QtExecutor::post
void post(std::function< void()> fn) override
Posts fn to the Qt event loop for execution on the context's thread.
Definition
qt_executor.hpp:86
morph::qt::QtExecutor::QtExecutor
QtExecutor(QObject *context=QCoreApplication::instance())
Constructs an executor that posts tasks to context's thread.
Definition
qt_executor.hpp:66
morph::exec::IExecutor
Abstract executor interface.
Definition
executor.hpp:70
morph::exec::IExecutor::coreExecutor
::core::async::IExecutor & coreExecutor() noexcept
This executor as a core-cpp executor: the identity an core::async::ExecutorScope names.
Definition
executor.hpp:100
morph
qt
qt_executor.hpp
Generated by
1.17.0