Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
qt_dispatcher.hpp
Go to the documentation of this file.
1#pragma once
2
3// QtDispatcher — asynchronous dispatch through a QObject owned by Qt's event
4// loop. Construct on the context's thread. Posting and releasing the dispatcher
5// are safe from other threads; destroying either the dispatcher or its context
6// cancels pending work and releases callback captures immediately.
7
10
11#include <QCoreApplication>
12#include <QMetaObject>
13#include <QObject>
14#include <QThread>
15#include <QTimer>
16
17#include <chrono>
18#include <cstdint>
19#include <functional>
20#include <limits>
21#include <memory>
22#include <mutex>
23#include <stdexcept>
24#include <unordered_map>
25#include <utility>
26
27namespace aria::adapters::qt6 {
28
29class QtDispatcher final : public runtime::IDispatcher {
30 struct Job {
31 std::function<void()> callback;
32 std::chrono::steady_clock::time_point posted = std::chrono::steady_clock::now();
33 std::chrono::milliseconds delay;
34 };
35 struct State {
36 std::mutex mutex;
37 QObject* anchor = nullptr;
38 bool active = true;
39 std::uint64_t next_id = 0;
40 std::unordered_map<std::uint64_t, std::shared_ptr<Job>> pending;
41 };
42
43 static void close_(const std::shared_ptr<State>& state, bool delete_anchor) noexcept {
44 decltype(state->pending) retired;
45 {
46 std::lock_guard lock(state->mutex);
47 state->active = false;
48 retired.swap(state->pending);
49 if (delete_anchor && state->anchor) {
50 // The anchor's destructor takes the same gate, so its address
51 // stays live until this queued deletion request is registered.
52 QMetaObject::invokeMethod(state->anchor, &QObject::deleteLater,
53 Qt::QueuedConnection);
54 } else if (!delete_anchor) {
55 state->anchor = nullptr;
56 }
57 }
58 // Captures may re-enter the dispatcher or destroy other Qt objects.
59 // Release them outside the gate, independently of deleteLater delivery.
60 }
61
62 class Anchor final : public QObject {
63 public:
64 Anchor(QObject* parent, std::shared_ptr<State> state)
65 : QObject(parent), state_(std::move(state)) {}
66 ~Anchor() override { close_(state_, false); }
67 private:
68 std::shared_ptr<State> state_;
69 };
70
71public:
75 explicit QtDispatcher(QObject* context = QCoreApplication::instance())
76 : state_(std::make_shared<State>()) {
77 if (!context) throw std::invalid_argument("QtDispatcher: context must not be null");
78 if (QThread::currentThread() != context->thread())
79 throw std::logic_error("QtDispatcher: construct on the context's owner thread");
80 state_->anchor = new Anchor(context, state_);
81 }
82
83 ~QtDispatcher() override { close_(state_, true); }
84 QtDispatcher(const QtDispatcher&) = delete;
86
87 void post(std::function<void()> callback) override {
88 enqueue_(std::move(callback), std::chrono::milliseconds{0});
89 }
90
94 void post_delayed(std::chrono::milliseconds delay,
95 std::function<void()> callback) override {
96 if (delay.count() < 0) delay = std::chrono::milliseconds{0};
97 enqueue_(std::move(callback), delay);
98 }
99
100 [[nodiscard]] bool is_main_thread() const noexcept override {
101 std::lock_guard lock(state_->mutex);
102 return state_->active && state_->anchor &&
103 QThread::currentThread() == state_->anchor->thread();
104 }
105
106 [[nodiscard]] ::aria::SchedulerCaps caps() const noexcept override {
107 return ::aria::SchedulerCaps::Post
111 }
112
113private:
114 void enqueue_(std::function<void()> callback, std::chrono::milliseconds delay) {
115 if (!callback) return;
116 // Keep an independent owner outside the lock even if map allocation
117 // fails, so unwinding cannot destroy the callback under the gate.
118 auto job = std::make_shared<Job>(Job{std::move(callback),
119 std::chrono::steady_clock::now(), delay});
120 auto state = state_;
121 std::lock_guard lock(state->mutex);
122 if (!state->active || !state->anchor) return;
123 if (state->next_id == std::numeric_limits<std::uint64_t>::max())
124 throw std::overflow_error("QtDispatcher: task identifier exhausted");
125 const auto id = ++state->next_id;
126 state->pending.emplace(id, job);
127 try {
128 if (!QMetaObject::invokeMethod(state->anchor,
129 [weak = std::weak_ptr<State>(state), id] { deliver_(weak, id); },
130 Qt::QueuedConnection)) {
131 state->pending.erase(id); // job above still owns the capture
132 }
133 } catch (...) {
134 state->pending.erase(id);
135 throw;
136 }
137 }
138
139 static void deliver_(const std::weak_ptr<State>& weak, std::uint64_t id) noexcept {
140 auto state = weak.lock();
141 if (!state) return;
142 std::shared_ptr<Job> ready;
143 try {
144 QObject* anchor;
145 std::chrono::milliseconds remaining{0};
146 {
147 std::lock_guard lock(state->mutex);
148 if (!state->active || !state->anchor) return;
149 auto it = state->pending.find(id);
150 if (it == state->pending.end()) return;
151 anchor = state->anchor;
152 const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
153 std::chrono::steady_clock::now() - it->second->posted);
154 if (elapsed < it->second->delay) {
155 remaining = it->second->delay - elapsed;
156 } else {
157 ready = std::move(it->second);
158 state->pending.erase(it);
159 }
160 }
161 if (ready) {
162 // No dispatcher object or mutex is needed while invoking user
163 // code; the callback may destroy its dispatcher synchronously.
164 ready->callback();
165 return;
166 }
167 // This path runs only on the anchor's owner thread. Off-thread
168 // shutdown can invalidate work immediately but deletes the QObject
169 // through its event loop, after this invocation has returned.
170 auto timer = std::make_unique<QTimer>(anchor);
171 timer->setSingleShot(true);
172 const auto maximum = std::numeric_limits<int>::max();
173 const int interval = remaining.count() > maximum
174 ? maximum : static_cast<int>(remaining.count());
175 auto* raw = timer.get();
176 QObject::connect(raw, &QTimer::timeout, anchor, [weak, id, raw] {
177 raw->deleteLater();
178 deliver_(weak, id);
179 });
180 timer->start(interval);
181 (void)timer.release(); // QObject parent now owns the timer
182 } catch (...) {
183 std::shared_ptr<Job> retired;
184 {
185 std::lock_guard lock(state->mutex);
186 if (auto it = state->pending.find(id); it != state->pending.end()) {
187 retired = std::move(it->second);
188 state->pending.erase(it);
189 }
190 }
191 ::aria::report_callback_failure("qt.dispatcher", std::current_exception());
192 }
193 }
194
195 std::shared_ptr<State> state_;
196};
197
198} // namespace aria::adapters::qt6
void post_delayed(std::chrono::milliseconds delay, std::function< void()> callback) override
Negative delays behave like post().
Definition qt_dispatcher.hpp:94
::aria::SchedulerCaps caps() const noexcept override
Capability bitmask.
Definition qt_dispatcher.hpp:106
void post(std::function< void()> callback) override
Schedule the callable to run on the main thread (asynchronously).
Definition qt_dispatcher.hpp:87
QtDispatcher(const QtDispatcher &)=delete
QtDispatcher(QObject *context=QCoreApplication::instance())
The context controls the owner thread and bounds task lifetime.
Definition qt_dispatcher.hpp:75
bool is_main_thread() const noexcept override
Returns true if currently executing on the main thread.
Definition qt_dispatcher.hpp:100
QtDispatcher & operator=(const QtDispatcher &)=delete
~QtDispatcher() override
Definition qt_dispatcher.hpp:83
Abstract main-thread dispatcher.
Definition dispatcher.hpp:26
Definition qt_adapter.hpp:12
void report_callback_failure(std::string_view category, std::exception_ptr exception, std::string_view message={}) noexcept
Report a callback failure.
SchedulerCaps
Definition scheduler.hpp:83
@ Autonomous
Implementation does not require any external pump and runs work on background threads autonomously.
Definition scheduler.hpp:116
@ Delay
Can submit work after a wall-clock or virtual-time delay.
Definition scheduler.hpp:93
@ MainThread
Submitted work runs on a single, identifiable "main" thread that is consistent across calls.
Definition scheduler.hpp:97
Definition validation_key.hpp:110