Skip to content

Commit

Permalink
Deferred connection evaluation (#48)
Browse files Browse the repository at this point in the history
* Deferred connection evaluation

* handle the evaluator object lifetime and added tests

* update connection_evaluator.h

* test and documentation update

* implementing dequeueSlotInvocation on disconnect and update the documentations

* Update connection_evaluator.h

* Adding the connection_handle file

* change the deferred slot signature and manage connectionHandle correctly

* deque on deconstruct and nits

* Refactor & Fix up disconnections on deconstruct

* Remove some duplicate checks

* Incorporate feedback from @MiKom

- Add license header to connection_evaluator.h
- Use slotInvocation instead of Connection in the ConnectionEvaluator
- Add warnings that deferred connections are experimental
- Improved documentation for deferred connections

---------

Co-authored-by: Shivam Kunwar <[email protected]>
Co-authored-by: Shivam <[email protected]>
  • Loading branch information
3 people authored Jan 8, 2024
1 parent 6211e44 commit 86d6bd5
Show file tree
Hide file tree
Showing 6 changed files with 597 additions and 180 deletions.
2 changes: 2 additions & 0 deletions src/kdbindings/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ set(HEADERS
property.h
property_updater.h
signal.h
connection_evaluator.h
connection_handle.h
utils.h
)

Expand Down
96 changes: 96 additions & 0 deletions src/kdbindings/connection_evaluator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
This file is part of KDBindings.
SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
Author: Shivam Kunwar <[email protected]>
SPDX-License-Identifier: MIT
Contact KDAB at <[email protected]> for commercial licensing options.
*/
#pragma once

#include <functional>
#include <mutex>

#include <kdbindings/connection_handle.h>

namespace KDBindings {

/**
* @brief Manages and evaluates deferred Signal connections.
*
* @warning Deferred connections are experimental and may be removed or changed in the future.
*
* The ConnectionEvaluator class is responsible for managing and evaluating connections
* to Signals. It provides mechanisms to delay and control the evaluation of connections.
* It therefore allows controlling when and on which thread slots connected to a Signal are executed.
*
* @see Signal::connectDeferred()
*/
class ConnectionEvaluator
{

public:
/** ConnectionEvaluators are default constructible */
ConnectionEvaluator() = default;

/** Connectionevaluators are not copyable */
// As it is designed to manage connections,
// and copying it could lead to unexpected behavior, including duplication of connections and issues
// related to connection lifetimes. Therefore, it is intentionally made non-copyable.
ConnectionEvaluator(const ConnectionEvaluator &) noexcept = delete;

ConnectionEvaluator &operator=(const ConnectionEvaluator &) noexcept = delete;

/** ConnectionEvaluators are not moveable */
// As they are captures by-reference
// by the Signal, so moving them would lead to a dangling reference.
ConnectionEvaluator(ConnectionEvaluator &&other) noexcept = delete;

ConnectionEvaluator &operator=(ConnectionEvaluator &&other) noexcept = delete;

/**
* @brief Evaluate the deferred connections.
*
* This function is responsible for evaluating and executing deferred connections.
* This function is thread safe.
*/
void evaluateDeferredConnections()
{
std::lock_guard<std::mutex> lock(m_slotInvocationMutex);

for (auto &pair : m_deferredSlotInvocations) {
pair.second();
}
m_deferredSlotInvocations.clear();
}

private:
template<typename...>
friend class Signal;

void enqueueSlotInvocation(const ConnectionHandle &handle, const std::function<void()> &slotInvocation)
{
std::lock_guard<std::mutex> lock(m_slotInvocationMutex);
m_deferredSlotInvocations.push_back({ handle, std::move(slotInvocation) });
}

void dequeueSlotInvocation(const ConnectionHandle &handle)
{
std::lock_guard<std::mutex> lock(m_slotInvocationMutex);

auto handleMatches = [&handle](const auto &invocationPair) {
return invocationPair.first == handle;
};

// Remove all invocations that match the handle
m_deferredSlotInvocations.erase(
std::remove_if(m_deferredSlotInvocations.begin(), m_deferredSlotInvocations.end(), handleMatches),
m_deferredSlotInvocations.end());
}

std::vector<std::pair<ConnectionHandle, std::function<void()>>> m_deferredSlotInvocations;
std::mutex m_slotInvocationMutex;
};
} // namespace KDBindings
209 changes: 209 additions & 0 deletions src/kdbindings/connection_handle.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*
This file is part of KDBindings.
SPDX-FileCopyrightText: 2021-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
Author: Sean Harmer <[email protected]>
SPDX-License-Identifier: MIT
Contact KDAB at <[email protected]> for commercial licensing options.
*/

#include <kdbindings/genindex_array.h>
#include <kdbindings/utils.h>
#include <memory>

namespace KDBindings {

template<typename... Args>
class Signal;

class ConnectionHandle;

namespace Private {
//
// This class defines a virtual interface, that the Signal this ConnectionHandle refers
// to must implement.
// It allows ConnectionHandle to refer to this non-template class, which then dispatches
// to the template implementation using virtual function calls.
// It allows ConnectionHandle to be a non-template class.
class SignalImplBase : public std::enable_shared_from_this<SignalImplBase>
{
public:
SignalImplBase() = default;

virtual ~SignalImplBase() = default;

virtual void disconnect(const ConnectionHandle &handle) = 0;
virtual bool blockConnection(const GenerationalIndex &id, bool blocked) = 0;
virtual bool isConnectionActive(const GenerationalIndex &id) const = 0;
virtual bool isConnectionBlocked(const GenerationalIndex &id) const = 0;
};

} // namespace Private

/**
* @brief A ConnectionHandle represents the connection of a Signal
* to a slot (i.e. a function that is called when the Signal is emitted).
*
* It is returned from a Signal when a connection is created and used to
* manage the connection by disconnecting, (un)blocking it and checking its state.
**/
class ConnectionHandle
{
public:
/**
* A ConnectionHandle can be default constructed.
* In this case the ConnectionHandle will not reference any active connection (i.e. isActive() will return false),
* and not belong to any Signal.
**/
ConnectionHandle() = default;

/**
* A ConnectionHandle can be copied.
**/
ConnectionHandle(const ConnectionHandle &) = default;
ConnectionHandle &operator=(const ConnectionHandle &) = default;

/**
* A ConnectionHandle can be moved.
**/
ConnectionHandle(ConnectionHandle &&) = default;
ConnectionHandle &operator=(ConnectionHandle &&) = default;

/**
* Disconnect the slot.
*
* When this function is called, the function that was passed to Signal::connect
* to create this ConnectionHandle will no longer be called when the Signal is emitted.
*
* If the ConnectionHandle is not active or the connection has already been disconnected,
* nothing happens.
*
* After this call, the ConnectionHandle will be inactive (i.e. isActive() returns false)
* and will no longer belong to any Signal (i.e. belongsTo returns false).
**/
void disconnect()
{
if (auto shared_impl = checkedLock()) {
shared_impl->disconnect(*this);
}

// ConnectionHandle is no longer active;
m_signalImpl.reset();
}

/**
* Check whether the connection of this ConnectionHandle is active.
*
* @return true if the ConnectionHandle refers to an active Signal
* and the connection was not disconnected previously, false otherwise.
**/
bool isActive() const
{
return static_cast<bool>(checkedLock());
}

/**
* Sets the block state of the connection.
* If a connection is blocked, emitting the Signal will no longer call this
* connections slot, until the connection is unblocked.
*
* Behaves the same as calling Signal::blockConnection with this
* ConnectionHandle as argument.
*
* To temporarily block a connection, consider using an instance of ConnectionBlocker,
* which offers a RAII-style implementation that makes sure the connection is always
* returned to its original state.
*
* @param blocked The new blocked state of the connection.
* @return whether the connection was previously blocked.
* @throw std::out_of_range Throws if the connection is not active (i.e. isActive() returns false).
**/
bool block(bool blocked)
{
if (auto shared_impl = checkedLock()) {
return shared_impl->blockConnection(*m_id, blocked);
}
throw std::out_of_range("Cannot block a non-active connection!");
}

/**
* Checks whether the connection is currently blocked.
*
* To change the blocked state of a connection, call ConnectionHandle::block.
*
* @return whether the connection is currently blocked.
**/
bool isBlocked() const
{
if (auto shared_impl = checkedLock()) {
return shared_impl->isConnectionBlocked(*m_id);
}
throw std::out_of_range("Cannot check whether a non-active connection is blocked!");
}

/**
* Check whether this ConnectionHandle belongs to the given Signal.
*
* @return true if this ConnectionHandle refers to a connection within the given Signal
**/
template<typename... Args>
bool belongsTo(const Signal<Args...> &signal) const
{
auto shared_impl = m_signalImpl.lock();
return shared_impl && shared_impl == std::static_pointer_cast<Private::SignalImplBase>(signal.m_impl);
}

// Define an operator== function to compare ConnectionHandle objects.
bool operator==(const ConnectionHandle &other) const
{
auto thisSignalImpl = m_signalImpl.lock();
auto otherSignalImpl = other.m_signalImpl.lock();

// If both signalImpl pointers are valid, compare them along with the IDs.
if (thisSignalImpl && otherSignalImpl) {
return (thisSignalImpl == otherSignalImpl) && (m_id == other.m_id);
}

// If neither instance has an ID, and both signalImpl pointers are invalid, consider them equal.
if (!m_id.has_value() && !other.m_id.has_value() && !thisSignalImpl && !otherSignalImpl) {
return true;
}

// In all other cases, they are not equal.
return false;
}

private:
template<typename...>
friend class Signal;

std::weak_ptr<Private::SignalImplBase> m_signalImpl;
std::optional<Private::GenerationalIndex> m_id;

// private, so it is only available from Signal
ConnectionHandle(std::weak_ptr<Private::SignalImplBase> signalImpl, std::optional<Private::GenerationalIndex> id)
: m_signalImpl{ std::move(signalImpl) }, m_id{ std::move(id) }
{
}
void setId(const Private::GenerationalIndex &id)
{
m_id = id;
}

// Checks that the weak_ptr can be locked and that the connection is
// still active
std::shared_ptr<Private::SignalImplBase> checkedLock() const
{
if (m_id.has_value()) {
auto shared_impl = m_signalImpl.lock();
if (shared_impl && shared_impl->isConnectionActive(*m_id)) {
return shared_impl;
}
}
return nullptr;
}
};

} // namespace KDBindings
7 changes: 6 additions & 1 deletion src/kdbindings/genindex_array.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ namespace Private {
struct GenerationalIndex {
uint32_t index = 0;
uint32_t generation = 0;

bool operator==(const GenerationalIndex &rhs) const
{
return index == rhs.index && generation == rhs.generation;
}
};

class GenerationalIndexAllocator
Expand Down Expand Up @@ -202,6 +207,6 @@ class GenerationalIndexArray
}
};

} //namespace Private
} // namespace Private

} // namespace KDBindings
Loading

0 comments on commit 86d6bd5

Please sign in to comment.