API — ICoreEssentials/UI/Signals
The public contract of 2 header(s) under ICoreEssentials/UI/Signals — 6 class/struct definition(s), 28 declaration(s). Each section shows the header's banner and its public (and protected-virtual) surface exactly as the file writes it.
| Header | Defines | Declarations | Bases |
|---|---|---|---|
ICoreMainThread.h | ICoreMainThread | 6 | — |
ICoreSignal.h | SlotBase, Slot, ICoreSignalConnection, ICoreSignalScope, ICoreSignal | 22 | SlotBase |
ICoreMainThread.h#
ICoreEssentials/UI/Signals/ICoreMainThread.h
ICoreMainThread#
ICoreMainThread.h:49 · class · 6 declaration(s)
The one door to the GUI thread.
class ICoreMainThread {
public:
ICoreMainThread() = delete;
// Run `fn` on the GUI thread, queued behind whatever the event loop is
// already doing.
static void post(std::function<void()> fn);
// Run `fn` NOW when already on the GUI thread, queue it otherwise --
// Qt::AutoConnection's dispatch, where post() above is always-queued.
// Pick deliberately: a caller escaping a destructor or paint context
// needs post()'s returns-first guarantee; a caller that just wants the
// work done soonest on the right thread wants this.
static void runNowOrPost(std::function<void()> fn);
static bool isMainThread();
// LEND the platform's event loop for up to `ms` milliseconds, so queued
// work, timers and toolkit callbacks get their turn, then return. It is
// ICoreApplication::tick(waitMs) reachable without the instance -- the same
// "the loop is lent, not entered" contract, for a caller that has no way to
// hold the application object.
//
// ⚠ WHAT IT IS FOR, AND WHY A SLEEP IS NOT IT. The thing a caller is
// usually waiting for -- an ICoreUiTimer's debounce, a queued callback, a
// child process's completion -- needs a LOOP TURN, not wall-clock. A
// std::this_thread::sleep_for of the same duration delivers none of them,
// on EITHER backend, and turns a real wait into a race the caller wins on a
// fast machine and loses on a slow one.
//
// `ms <= 0` drains what is already pending and returns without waiting,
// matching tick(0).
//
// ⚠ MAIN THREAD ONLY -- it is the loop's own thread that can pump it. Off
// that thread, and before the application object exists, there is no loop
// to lend: it then SLEEPS for the remaining time rather than returning
// instantly, because the duration is the part such a caller can observe and
// a busy return would spin them. That is the same family as post()'s
// no-loop-yet fallback above, and it is the only case where this call does
// not do what its name says.
//
// ⚠ Added 2026-08-22 for a caller that had no portable spelling at all: a
// test harness whose waits were raw toolkit event loops, which HANG on a
// backend whose toolkit application object does not exist -- stopping the
// regression suite dead at two separate cases, on the same mechanism.
static void pumpFor(int ms);
// QApplication::quit(), behind the boundary. ONLY the fallback for
// processes that never construct an icore::Application -- under the SDK
// entry point quit() acts on a loop that is not running and does nothing,
// so route through icore::Application::requestQuit() first and call this
// when there is no live instance.
static void quitApplicationLoopFallback();
};
};
ICoreSignal.h#
ICoreEssentials/UI/Signals/ICoreSignal.h
The project's replacement for Qt signals/slots at every call site OUTSIDE the wrapper layer. Client classes hold public ICoreSignal<...> members where they used to declare
signals:sections, and receivers subscribe with connect() where they used to call QObject::connect. No moc, no Q_OBJECT, no QObject base required on either side.Naming: the sync emit is operator(), the queued emit is post(). There is deliberately no method named
emit-- that identifier is a Qt macro in any TU that sees a Qt header, which in this project is most of them.Threading: connect/disconnect/emit are safe from any thread. Sync emission runs the slots on the calling thread against a snapshot taken under the lock, so a slot may connect or disconnect (even itself) during delivery. post() delivers the snapshot on the GUI thread via ICoreMainThread -- this
SlotBase#
ICoreSignal.h:40 · struct · 1 declaration(s)
struct SlotBase {
public:
std::atomic<bool> alive { true };
virtual ~SlotBase() = default;
};
};
Slot#
ICoreSignal.h:46 · struct · bases SlotBase · 1 declaration(s)
struct Slot : SlotBase {
public:
std::function<void(Args...)> fn;
};
};
ICoreSignalConnection#
ICoreSignal.h:54 · class · pImpl · 6 declaration(s)
Handle to one registration.
class ICoreSignalConnection {
public:
ICoreSignalConnection();
~ICoreSignalConnection();
// Copyable, and every copy severs the SAME slot -- that is the contract,
// not an accident, and it survives the Impl because the Impl holds the same
// weak_ptr. Written out because a unique_ptr<Impl> deletes the implicit
// copy.
ICoreSignalConnection(const ICoreSignalConnection& other);
ICoreSignalConnection& operator=(const ICoreSignalConnection& other);
void disconnect();
[[nodiscard]] bool connected() const;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreSignalScope#
ICoreSignal.h:86 · class · pImpl · 6 declaration(s)
Collects connections and severs them all on destruction.
class ICoreSignalScope {
public:
ICoreSignalScope();
ICoreSignalScope(const ICoreSignalScope&) = delete;
ICoreSignalScope& operator=(const ICoreSignalScope&) = delete;
// Severs every connection it collected.
~ICoreSignalScope();
// const, with the state behind it, so a receiver can subscribe from a
// const member function -- much of this codebase builds its children from
// const methods, and what a scope holds is bookkeeping about the owner's
// subscriptions rather than part of the owner's logical value.
//
// ⚠ The `mutable` these two used to need is GONE, and not by oversight:
// operator-> on a const unique_ptr yields a NON-const Impl&, so state
// behind an Impl is already writable from a const member. The constness of
// the interface is unchanged; only the keyword that used to buy it is.
void add(ICoreSignalConnection connection) const;
void disconnectAll() const;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreSignal#
ICoreSignal.h:114 · class · 8 declaration(s)
class ICoreSignal {
public:
ICoreSignal() = default;
ICoreSignal(const ICoreSignal&) = delete;
ICoreSignal& operator=(const ICoreSignal&) = delete;
// Bare connect exists for slots whose lifetime provably exceeds the
// signal's (a static, or the signal's own owner). Everything else should
// pass a scope -- an unscoped lambda capturing `this` is exactly the
// dangling QObject::connect this class exists to retire.
ICoreSignalConnection connect(std::function<void(Args...)> fn) {
auto slot = std::make_shared<Slot>();
slot->fn = std::move(fn);
{
std::lock_guard<std::mutex> lock(m_mutex);
m_slots.push_back(slot);
}
return ICoreSignalConnection(slot);
}
ICoreSignalConnection connect(const ICoreSignalScope& owner, std::function<void(Args...)> fn) {
ICoreSignalConnection connection = connect(std::move(fn));
owner.add(connection);
return connection;
}
// Synchronous emit on the calling thread.
void operator()(Args... args) {
for (const auto& slot : snapshot()) {
if (slot->alive.load()) {
slot->fn(args...);
}
}
}
// Queued emit on the GUI thread. Arguments are copied into the hop, so
// they must be value types (everything crossing this boundary is).
//
// NOTE the local is `delivery`, not `slots`: most TUs see Qt's keyword
// macros, which #define `signals`, `slots` and `emit` away -- none of the
// three may be used as an identifier anywhere in the wrapper layer.
void post(Args... args) {
auto delivery = snapshot();
auto packed = std::make_tuple(std::move(args)...);
ICoreMainThread::post([delivery = std::move(delivery), packed = std::move(packed)]() mutable {
std::apply(
[&delivery](auto&... unpacked) {
for (const auto& slot : delivery) {
if (slot->alive.load()) {
slot->fn(unpacked...);
}
}
},
packed);
});
}
void disconnectAll() {
std::lock_guard<std::mutex> lock(m_mutex);
for (const auto& slot : m_slots) {
slot->alive.store(false);
}
m_slots.clear();
}
};