AUI Framework  develop
Cross-platform base for C++ UI apps
Loading...
Searching...
No Matches
ASignal.h
    1/*
    2 * AUI Framework - Declarative UI toolkit for modern C++20
    3 * Copyright (C) 2020-2025 Alex2772 and Contributors
    4 *
    5 * SPDX-License-Identifier: MPL-2.0
    6 *
    7 * This Source Code Form is subject to the terms of the Mozilla Public
    8 * License, v. 2.0. If a copy of the MPL was not distributed with this
    9 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
   10 */
   11
   12#pragma once
   13
   14#include <algorithm>
   15#include <functional>
   16#include "AUI/Common/ADeque.h"
   17#include "AUI/Common/AObject.h"
   18#include "AUI/Thread/AMutex.h"
   19#include "AAbstractSignal.h"
   20#include "AUI/Traits/values.h"
   21#include "AUI/Util/ARaiiHelper.h"
   22#include "AUI/Util/AEvaluationLoopException.h"
   23
   24namespace aui::detail::signal {
   25
   26template <typename... Args>
   27std::tuple<Args...> makeTupleOfCopies(std::tuple<const Args&...> args) {
   28    return args;
   29}
   30
   31static_assert(requires(const int& v) {
   32    { makeTupleOfCopies(std::tie(v)) } -> aui::same_as<std::tuple<int>>;
   33});
   34
   35template <size_t I, typename TupleInitial, typename TupleAll>
   36auto resizeTuple(TupleInitial initial, TupleAll all) {
   37    if constexpr (I == 0) {
   38        return initial;
   39    } else {
   40        return resizeTuple<I - 1>(
   41            std::tuple_cat(initial, std::tie(std::get<std::tuple_size_v<TupleInitial>>(all))), all);
   42    }
   43}
   44
   45template <aui::not_overloaded_lambda Lambda, typename... Args>
   46inline void callIgnoringExcessArgs(Lambda&& lambda, const Args&... args) {
   47    static constexpr size_t EXPECTED_ARG_COUNT = std::tuple_size_v<typename lambda_info<std::decay_t<Lambda>>::args>;
   48    auto smallerTuple = resizeTuple<EXPECTED_ARG_COUNT>(std::make_tuple(), std::tie(args...));
   49    std::apply(lambda, smallerTuple);
   50}
   51
   52template <typename Projection>
   53struct projection_info {
   54    static_assert(
   55        aui::reflect::pointer_to_member<Projection> || aui::not_overloaded_lambda<Projection> ||
   56            aui::function_pointer<Projection>,
   57        "====================> ASignal: projection is required to be an pointer-to-member or not overloaded lambda or function pointer");
   58};
   59
   60template <aui::reflect::pointer_to_member Projection>
   61struct projection_info<Projection> {
   62private:
   63    template <typename... T>
   64    struct cat;
   65
   66    template <typename First, typename... T>
   67    struct cat<First, std::tuple<T...>> {
   68        using type = std::tuple<First, T...>;
   69    };
   70
   71public:
   72    using info = typename aui::reflect::member<Projection>;
   73    using return_t = typename info::return_t;
   74    using args = typename cat<typename info::clazz&, typename info::args>::type;
   75};
   76
   77template <aui::not_overloaded_lambda Projection>
   78struct projection_info<Projection> {
   79    using info = typename aui::lambda_info<Projection>;
   80    using return_t = typename info::return_t;
   81    using args = typename info::args;
   82};
   83
   84template <aui::function_pointer Projection>
   85struct projection_info<Projection> {
   86    using info = typename aui::function_info<Projection>;
   87    using return_t = typename info::return_t;
   88    using args = typename info::args;
   89};
   90
   91template <
   92    typename AnySignal,   // can't use AAnySignal here, as concept would depend on itself
   93    typename Projection>
   94struct ProjectedSignal {
   95    friend class ::AObject;
   96
   97    AnySignal& base;
   98    std::decay_t<Projection> projection;
   99
  100    ProjectedSignal(AnySignal& base, Projection projection) : base(base), projection(std::move(projection)) {}
  101
  102    using projection_info_t = aui::detail::signal::projection_info<Projection>;
  103
  104    using projection_returns_t = typename projection_info_t::return_t;
  105
  106    static constexpr bool IS_PROJECTION_RETURNS_TUPLE = aui::is_tuple<projection_returns_t>;
  107
  108    using emits_args_t =
  109        std::conditional_t<IS_PROJECTION_RETURNS_TUPLE, projection_returns_t, std::tuple<projection_returns_t>>;
  110
  111    template <convertible_to<AObjectBase*> Object, not_overloaded_lambda Lambda>
  112    void connect(Object objectBase, Lambda&& lambda) {
  113        base.connect(
  114            objectBase,
  115            tuple_visitor<typename projection_info_t::args>::for_each_all([&]<typename... ProjectionArgs>() {
  116                return [invocable = std::forward<Lambda>(lambda),
  117                        projection = projection](const std::decay_t<ProjectionArgs>&... args) {
  118                    auto result = std::invoke(projection, args...);
  119                    if constexpr (IS_PROJECTION_RETURNS_TUPLE) {
  120                        std::apply(invocable, std::move(result));
  121                    } else {
  122                        std::invoke(invocable, std::move(result));
  123                    }
  124                };
  125            }));
  126    }
  127
  128    operator bool() const { return bool(base); }
  129
  130    bool isAtSignalEmissionState() const noexcept {
  131        return base.isAtSignalEmissionState();
  132    }
  133
  134private:
  135    template <not_overloaded_lambda Lambda>
  136    auto makeRawInvocable(Lambda&& lambda) const {
  137        return tuple_visitor<emits_args_t>::for_each_all([&]<typename... ProjectionResult>() {
  138            return [lambda = std::forward<Lambda>(lambda)](const std::decay_t<ProjectionResult>&... args) {
  139                aui::detail::signal::callIgnoringExcessArgs(lambda, args...);
  140            };
  141        });
  142    }
  143};
  144
  145}   // namespace aui::detail::signal
  146
  147template <typename... Args>
  148class ASignal final : public AAbstractSignal {
  149    static_assert(
  150        (std::is_object_v<Args> && ...),
  151        "There's no effect of specifying of non value arguments for the signal. Consider removing const and "
  152        "reference modifiers.");
  153
  154    /* ASignal <-> AObject implementation stuff */
  155    friend class AObject;
  156
  157    /* tests */
  158    friend class PropertyPrecomputedTest_APropertyPrecomputed_Complex_Test;
  159    friend class SignalSlotTest;
  160    friend class PropertyTest;
  161    friend class PropertyPrecomputedTest;
  162
  163    template <typename AnySignal, typename Projection>
  164    friend struct aui::detail::signal::ProjectedSignal;
  165
  166    template <typename T>
  167    friend class AWatchable;
  168
  169public:
  170    using func_t = std::function<void(const Args&...)>;
  171    using emits_args_t = std::tuple<Args...>;
  172
  173    template <typename Projection>
  174    auto projected(Projection&& projection) const {
  175        return aui::detail::signal::ProjectedSignal(const_cast<ASignal&>(*this), std::forward<Projection>(projection));
  176    }
  177
  178    struct call_wrapper {
  179        ASignal& signal;
  180        std::tuple<const Args&...> args;
  181
  182        void invokeSignal(AObject* sender) { signal.invokeSignal(sender, args); }
  183    };
  184
  185    call_wrapper operator()(const Args&... args) { return { *this, std::make_tuple(std::cref(args)...) }; }
  186
  187    ASignal() = default;
  188    ASignal(ASignal&&) noexcept = default;
  189    ASignal(const ASignal&) noexcept {
  190        // mOutgoingConnections are not borrowed on copy operation.
  191    }
  192
  193    ASignal& operator=(ASignal&&) noexcept = default;
  194    ASignal& operator=(const ASignal&) noexcept {
  195        // mOutgoingConnections are not borrowed on copy operation.
  196        return *this;
  197    }
  198
  199    virtual ~ASignal() noexcept = default;
  200
  208    operator bool() const { return hasOutgoingConnections(); }
  209
  210    void clearAllOutgoingConnections() const noexcept override { mOutgoingConnections.clear(); }
  211    void clearAllOutgoingConnectionsWith(aui::no_escape<AObjectBase> object) const noexcept override {
  212        clearOutgoingConnectionsIf([&](const _<ConnectionImpl>& p) { return p->receiverBase == object.ptr(); });
  213    }
  214
  215    [[nodiscard]] bool hasOutgoingConnections() const noexcept {
  216        return !mOutgoingConnections.empty();
  217    }
  218
  219    [[nodiscard]] bool hasOutgoingConnectionsWith(aui::no_escape<AObjectBase> object) const noexcept override {
  220        return std::any_of(
  221            mOutgoingConnections.begin(), mOutgoingConnections.end(),
  222            [&](const SenderConnectionOwner& s) { return s.value->receiverBase == object.ptr(); });
  223    }
  224
  225    [[nodiscard]]
  226    bool isAtSignalEmissionState() const noexcept {
  227        return mLoopGuard.is_locked();
  228    }
  229
  230private:
  231    struct ConnectionImpl final : Connection {
  232        friend class ASignal;
  233
  234        void disconnect() override {
  235            std::unique_lock lock(AObjectBase::SIGNAL_SLOT_GLOBAL_SYNC);
  236            unlinkInSenderSideOnly(lock);
  237            if (!lock.owns_lock()) lock.lock();
  238            unlinkInReceiverSideOnly(lock);
  239
  240            receiverBase = nullptr;
  241            receiver = nullptr;
  242            sender = nullptr;
  243        }
  244
  245    private:
  250        ASignal* sender = nullptr;
  251
  257        AObjectBase* receiverBase = nullptr;
  258
  269        AObject* receiver = nullptr;
  270
  274        func_t func;
  275
  283        bool toBeRemoved = false;
  284
  292        void unlinkInReceiverSideOnly(std::unique_lock<ASpinlockMutex>& lock) {
  293            toBeRemoved = true;
  294
  295            auto receiverLocal = std::exchange(receiverBase, nullptr);
  296            if (!receiverLocal) {
  297                return;
  298            }
  299            receiver = nullptr;
  300            removeIngoingConnectionIn(receiverLocal, *this, lock);
  301        }
  302
  303        void unlinkInSenderSideOnly(std::unique_lock<ASpinlockMutex>& lock) {
  304            toBeRemoved = true;
  305            auto localSender = std::exchange(sender, nullptr);
  306            if (!localSender) {
  307                return;
  308            }
  309
  310            // As we marked toBeRemoved, we are not required to do anything further. However, we can perform a cheap
  311            // operation to clean the connection right now. If we fail at some point we can leave it as is.
  312            // invokeSignal will clean the connection for us at some point.
  313            auto it = std::find_if(
  314                localSender->mOutgoingConnections.begin(), localSender->mOutgoingConnections.end(),
  315                [&](const SenderConnectionOwner& o) { return o.value.get() == this; });
  316            if (it == localSender->mOutgoingConnections.end()) {
  317                // It can happen probably when another thread is performing invocation on this signal and stole the
  318                // mOutgoingConnections array.
  319                return;
  320            }
  321            // it->value may be unique owner of this, let's steal the ownership before erasure to keep things safe.
  322            auto self = std::exchange(it->value, nullptr);
  323            localSender->mOutgoingConnections.erase(it);
  324            lock.unlock();
  325        }
  326
  327        void onBeforeReceiverSideDestroyed() override {
  328            std::unique_lock lock(AObjectBase::SIGNAL_SLOT_GLOBAL_SYNC);
  329            // this function can be called by receiver's AObject cleanup functions (presumably, destructor), so we
  330            // assume receiver (and thus receiverBase) are invalid.
  331            receiver = nullptr;
  332            receiverBase = nullptr;
  333            unlinkInSenderSideOnly(lock);
  334        }
  335    };
  336
  340    struct SenderConnectionOwner {
  341        _<ConnectionImpl> value = nullptr;
  342
  343        SenderConnectionOwner() = default;
  344        explicit SenderConnectionOwner(_<ConnectionImpl> connection) noexcept : value(std::move(connection)) {}
  345        SenderConnectionOwner(const SenderConnectionOwner&) = default;
  346        SenderConnectionOwner(SenderConnectionOwner&&) noexcept = default;
  347        SenderConnectionOwner& operator=(const SenderConnectionOwner& rhs) {
  348            if (this == &rhs) {
  349                return *this;
  350            }
  351            release();
  352            value = rhs;
  353            return *this;
  354        }
  355        SenderConnectionOwner& operator=(SenderConnectionOwner&& rhs) noexcept {
  356            if (this == &rhs) {
  357                return *this;
  358            }
  359            release();
  360            value = std::move(rhs.value);
  361            return *this;
  362        }
  363
  364        ~SenderConnectionOwner() {
  365            release();
  366        }
  367
  368    private:
  369        void release() noexcept {
  370            if (!value) {
  371                return;
  372            }
  373            std::unique_lock lock(AObjectBase::SIGNAL_SLOT_GLOBAL_SYNC);
  374            // this destructor can be called in ASignal destructor, so it's worth to reset the sender as well.
  375            value->sender = nullptr;
  376            value->unlinkInReceiverSideOnly(lock);
  377            value = nullptr;
  378        }
  379    };
  380
  381    mutable AVector<SenderConnectionOwner> mOutgoingConnections;
  382    ASpinlockMutex mLoopGuard;
  383
  384    void invokeSignal(AObject* sender, std::tuple<const Args&...> args = {});
  385
  386    template <aui::convertible_to<AObjectBase*> Object, aui::not_overloaded_lambda Lambda>
  387    const _<ConnectionImpl>& connect(Object objectBase, Lambda&& lambda) {
  388        AObject* object = nullptr;
  389        if constexpr (std::is_base_of_v<AObject, std::remove_pointer_t<Object>>) {
  390            object = objectBase;
  391        }
  392        const auto& connection = [&]() -> _<ConnectionImpl>& {
  393            auto conn = _new<ConnectionImpl>();
  394            conn->sender = this;
  395            conn->receiverBase = objectBase;
  396            conn->receiver = object;
  397            conn->func = makeRawInvocable(std::forward<Lambda>(lambda));
  398            std::unique_lock lock(AObjectBase::SIGNAL_SLOT_GLOBAL_SYNC);
  399
  400            std::erase_if(mOutgoingConnections, [](const SenderConnectionOwner& o) {
  401                return o.value == nullptr;
  402            });
  403
  404            return mOutgoingConnections.emplace_back(std::move(conn)).value;
  405        }();
  406        if (objectBase != AObject::GENERIC_OBSERVER) {
  407            addIngoingConnectionIn(objectBase, connection);
  408        }
  409        return connection;
  410    }
  411
  412    _<Connection> addGenericObserver(AObjectBase* receiver, std::function<void()> observer) override {
  413        return connect(receiver, [observer = std::move(observer)] { observer(); });
  414    }
  415
  416    template <aui::not_overloaded_lambda Lambda>
  417    auto makeRawInvocable(Lambda&& lambda) const {
  418        return [lambda = std::forward<Lambda>(lambda)](const Args&... args) mutable {
  419            aui::detail::signal::callIgnoringExcessArgs(lambda, args...);
  420        };
  421    }
  422
  423private:
  424    template <typename Predicate>
  425    void clearOutgoingConnectionsIf(Predicate&& predicate) const noexcept {
  426        /*
  427         * Removal of connections before end of execution of clearOutgoingConnectionsIf may cause this ASignal
  428         * destruction, causing undefined behaviour. Destructing these connections after mSlotsLock unlocking solves the
  429         * problem.
  430         */
  431        AVector<SenderConnectionOwner> slotsToRemove;
  432
  433        slotsToRemove.reserve(mOutgoingConnections.size());
  434        mOutgoingConnections.removeIf([&slotsToRemove, predicate = std::move(predicate)](SenderConnectionOwner& p) {
  435            if (predicate(p.value)) {
  436                slotsToRemove << std::move(p);
  437                return true;
  438            }
  439            return false;
  440        });
  441
  442        slotsToRemove.clear();
  443    }
  444};
  445#include <AUI/Thread/AThread.h>
  446
  447template <typename... Args>
  448void ASignal<Args...>::invokeSignal(AObject* sender, std::tuple<const Args&...> args) {
  449    if (mOutgoingConnections.empty())
  450        return;
  451
  452    _<AObject> senderPtr, receiverPtr;
  453
  454    if (sender != nullptr) {
  455        if (auto sharedPtr = weakPtrFromObject(sender).lock()) {   // avoid sender removal during signal processing
  456            senderPtr = std::move(static_cast<_<AObject>>(sharedPtr));
  457        }
  458    }
  459
  460    std::unique_lock lock(AObjectBase::SIGNAL_SLOT_GLOBAL_SYNC);
  461    std::unique_lock lock2(mLoopGuard, std::try_to_lock);
  462    if (!lock2.owns_lock()) {
  463        throw AEvaluationLoopException();
  464    }
  465    auto outgoingConnections = std::move(mOutgoingConnections);   // needed to safely iterate through the slots
  466    ARaiiHelper returnBack = [&] {
  467        if (!lock.owns_lock()) lock.lock();
  468        AUI_MARK_AS_USED(senderPtr);
  469        AUI_MARK_AS_USED(receiverPtr);
  470
  471        if (mOutgoingConnections.empty()) {
  472            mOutgoingConnections = std::move(outgoingConnections);
  473        } else {
  474            // mSlots might have been modified by a single threaded signal call. In this case merge two vectors
  475            mOutgoingConnections.insert(
  476                mOutgoingConnections.begin(), std::make_move_iterator(outgoingConnections.begin()),
  477                std::make_move_iterator(outgoingConnections.end()));
  478        }
  479    };
  480    for (auto i = outgoingConnections.begin(); i != outgoingConnections.end();) {
  481        _<ConnectionImpl>& outgoingConnection = i->value;
  482        if (!lock.owns_lock()) lock.lock();
  483        if (outgoingConnection->toBeRemoved) {
  484            lock.unlock();
  485            i = outgoingConnections.erase(i);
  486            continue;
  487        }
  488        _weak<AObject> receiverWeakPtr;
  489        if (outgoingConnection->receiver != nullptr) {
  490            receiverWeakPtr = weakPtrFromObject(outgoingConnection->receiver);
  491            if (outgoingConnection->receiver->isSlotsCallsOnlyOnMyThread() &&
  492                outgoingConnection->receiver->getThread() != AThread::current()) {
  493                // perform crossthread call; should make weak ptr to the object and queue call to thread message queue
  494
  495                /*
  496                 * shared_ptr counting mechanism is used when doing a crossthread call.
  497                 * It could not track the object existence without shared_ptr block.
  498                 * Also, receiverWeakPtr.lock() may be null here because object is in different thread and being
  499                 * destructed by shared_ptr but have not reached clearAllIngoingConnections() yet.
  500                 */
  501                if (receiverWeakPtr.lock() != nullptr) {
  502                    outgoingConnection->receiver->getThread()->enqueue(
  503                        [senderWeakPtr = senderPtr.weak(),
  504                         receiverWeakPtr = std::move(receiverWeakPtr),
  505                         connection = outgoingConnection,
  506                         args = aui::detail::signal::makeTupleOfCopies(args)] {
  507                            static_assert(
  508                                std::is_same_v<std::tuple<std::decay_t<Args>...>, std::remove_const_t<decltype(args)>>,
  509                                "when performing a cross thread call, args is expected to hold values "
  510                                "instead of references");
  511                            auto receiverPtr = receiverWeakPtr.lock();
  512                            if (!receiverPtr) {
  513                                // receiver was destroyed while we were transferring the call to another thread.
  514                                return;
  515                            }
  516
  517                            AObject::isDisconnected() = false;
  518                            ARaiiHelper h = [&] {
  519                                if (AObject::isDisconnected()) {
  520                                    connection->disconnect();
  521                                }
  522                            };
  523                            try {
  524                                (std::apply)(connection->func, args);
  525                            } catch (...) {
  526                                if (auto senderPtr = senderWeakPtr.lock()) {
  527                                    senderPtr->handleSlotException(std::current_exception());
  528                                }
  529                            }
  530                        });
  531                }
  532                ++i;
  533                continue;
  534            }
  535        }
  536        AObject::isDisconnected() = false;
  537
  538        if (auto sharedPtr = receiverWeakPtr.lock()) {   // avoid receiver removal during signal processing
  539            receiverPtr = std::move(sharedPtr);
  540        }
  541
  542        if constexpr (std::tuple_size_v<decltype(args)> > 0) {
  543            using FirstType = decltype(std::get<0>(args));
  544            static_assert(
  545                std::is_reference_v<FirstType>,
  546                "when performing a non-threading call, args is expected to hold const references"
  547                "instead of values");
  548        }
  549        lock.unlock();
  550        try {
  551            (std::apply)(outgoingConnection->func, args);
  552        } catch (...) {
  553            if (senderPtr) {
  554                senderPtr->handleSlotException(std::current_exception());
  555            }
  556        }
  557        if (AObject::isDisconnected()) {
  558            i = outgoingConnections.erase(i);
  559            continue;
  560        }
  561        ++i;
  562    }
  563}
  564
  571template <typename... Args>
  572using emits = ASignal<Args...>;
  573
  574#define signals public
  575
  576
  577/*
  578
  579// UNCOMMENT THIS to test ProjectedSignal
  580
  581static_assert(requires (aui::detail::signal::ProjectedSignal<emits<int>, decltype([](int) { return double(0);})> t) {
  582    requires !decltype(t)::IS_PROJECTION_RETURNS_TUPLE;
  583    { decltype(t)::base } -> aui::same_as<ASignal<int>&>;
  584    { decltype(t)::projection } -> aui::not_overloaded_lambda;
  585    { decltype(t)::projection_returns_t{} } -> aui::same_as<double>;
  586    { decltype(t)::emits_args_t{} } -> aui::same_as<std::tuple<double>>;
  587    { decltype(t)::projection_info_t::args{} } -> aui::same_as<std::tuple<int>>;
  588    { decltype(t)::projection_info_t::return_t{} } -> aui::same_as<double>;
  589});
  590
  591static_assert(requires (aui::detail::signal::ProjectedSignal<emits<AString>, decltype(&AString::length)> t) {
  592    requires !decltype(t)::IS_PROJECTION_RETURNS_TUPLE;
  593    { decltype(t)::base } -> aui::same_as<ASignal<AString>&>;
  594    { decltype(t)::emits_args_t{} } -> aui::same_as<std::tuple<size_t>>;
  595    { decltype(t)::projection_returns_t{} } -> aui::same_as<size_t>;
  596});
  597*/
Base class for signal.
Definition AAbstractSignal.h:366
static void addIngoingConnectionIn(aui::no_escape< AObjectBase > object, _< Connection > connection)
Adds a connection to the specified object.
static void removeIngoingConnectionIn(aui::no_escape< AObjectBase > object, Connection &connection, std::unique_lock< ASpinlockMutex > &lock)
Removes a connection from the specified object.
Indicates an evaluation loop.
Definition AEvaluationLoopException.h:28
A base object class.
Definition AObject.h:39
static constexpr AObjectBase * GENERIC_OBSERVER
Indicates that a connection should not be explicitly linked to receiver's lifetime.
Definition AObject.h:61
Definition ARaiiHelper.h:17
Definition ASignal.h:148
void clearAllOutgoingConnectionsWith(aui::no_escape< AObjectBase > object) const noexcept override
Destroys all connections with passed receiver, if any.
Definition ASignal.h:211
bool hasOutgoingConnectionsWith(aui::no_escape< AObjectBase > object) const noexcept override
Definition ASignal.h:219
void clearAllOutgoingConnections() const noexcept override
Destroys all connections of this signal, if any.
Definition ASignal.h:210
static _< AAbstractThread > current()
void removeIf(Predicate &&predicate) noexcept
Definition AVector.h:334
An std::weak_ptr with AUI extensions.
Definition SharedPtrTypes.h:179
_weak< T > weak() const
Definition SharedPtrTypes.h:270
std::add_lvalue_reference_t< T > value() const noexcept
Dereferences the stored pointer.
Definition SharedPtrTypes.h:294
API_AUI_CORE const ACommandLineArgs & args() noexcept
ASignal< Args... > emits
A signal declaration.
Definition ASignal.h:572
#define AUI_MARK_AS_USED(variable)
Marks the variable as being used.
Definition macros.h:50
Connection handle.
Definition AAbstractSignal.h:375
Definition ASignal.h:178
An std::weak_ptr with AUI extensions.
Definition SharedPtrTypes.h:52
Does not allow escaping, allowing to accept lvalue ref, rvalue ref, shared_ptr and etc without overhe...
Definition values.h:128