Skip to content

Commit

Permalink
api: add a new signal API based on signal types
Browse files Browse the repository at this point in the history
The old signal API relies on plugins connecting to the same signal as
described by its signal name. This is very error-prone:

1. Typos can be hard to debug.
2. If the signal changes/is removed/was not added in this version of
   Wayfire, everything may still compile.
3. We need to cast the untyped signal data to the proper type.
4. We need to match the string name to the type.

C++'s templates offer a good solution by allowing us to replace string
signal names with structs of different types and identifying different
structs by their type_index.
With the new API, we know at compile time which signal the connection_t
should connect to, which effectively eliminates any duplication (e.g.
manual casts, specifying the stringified signal name, etc).
This also results in more efficient signals, as we do not have to hash a
string every time, instead, we hash only a type_index.

One last consideration which has also been painful in the past is
mapping all structs to C or other languages. The new signal structs
should no longer use inheritance and also export an optional `::cname`
static member so that a signal is emitted to special callbacks in a
signal provider. These callbacks can be used by C or other languages to
connect to all signals on a given provider.
  • Loading branch information
ammen99 committed Jun 7, 2022
1 parent 5dbf0d0 commit 2274148
Show file tree
Hide file tree
Showing 2 changed files with 226 additions and 0 deletions.
215 changes: 215 additions & 0 deletions src/api/wayfire/signal-provider.hpp
@@ -0,0 +1,215 @@
#pragma once

#include <functional>
#include <memory>
#include <unordered_set>
#include <wayfire/nonstd/safe-list.hpp>
#include <cassert>
#include <typeindex>

namespace wf
{
namespace signal
{
class provider_t;

/**
* A base class for all connection_t, needed to store list of connections in a
* type-safe way.
*/
class connection_base_t
{
public:
connection_base_t(const connection_base_t&) = delete;
connection_base_t(connection_base_t&&) = delete;
connection_base_t& operator =(const connection_base_t&) = delete;
connection_base_t& operator =(connection_base_t&&) = delete;

/**
* Automatically disconnects from every connected provider.
*/
virtual ~connection_base_t()
{
disconnect();
}

/** Disconnect from all connected signal providers */
void disconnect();

protected:
connection_base_t()
{}

// Allow provider to deregister itself
friend class provider_t;
std::unordered_set<provider_t*> connected_to;
};

namespace detail
{
template<class Type, class U = void>
struct has_c_name : public std::false_type {};
template<class Type>
struct has_c_name<Type, decltype(Type::c_name)> : public std::true_type {};
}

/**
* A connection to a signal on an object.
* Uses RAII to automatically disconnect the signal when it goes out of scope.
*/
template<class SignalType>
class connection_t final : public connection_base_t
{
public:
using callback = std::function<void (SignalType*)>;

/** Initialize an empty signal connection */
connection_t()
{}

/** Automatically disconnects from all providers */
virtual ~connection_t()
{}

template<class CallbackType> using convertible_to_callback_t =
std::enable_if_t<std::is_constructible_v<callback, CallbackType>, void>;

/** Initialize a signal connection with the given callback */
template<class T, class U = convertible_to_callback_t<T>>
connection_t(const T& callback) : connection_t()
{
set_callback(callback);
}

template<class T>
connection_t(std::function<void(T*)>& callback) : connection_t()
{
set_callback(callback);
}

/** Set the signal callback or override the existing signal callback. */
void set_callback(callback cb)
{
this->current_callback = cb;
}

/** Call the stored callback with the given data. */
void emit(SignalType *data)
{
if (current_callback)
{
current_callback(data);
}
}

private:
// Non-copyable and non-movable, as that would require updating/duplicating
// the signal handler. But this is usually not what users of this API want.
// Also provider_t holds pointers to this object.
connection_t(const connection_t&) = delete;
connection_t(connection_t&&) = delete;
connection_t& operator =(const connection_t&) = delete;
connection_t& operator =(connection_t&&) = delete;

callback current_callback;
};

class provider_t
{
public:
/**
* Signals are designed to be useful for C++ plugins, however, they are
* generally quite difficult to bind in other languages.
* To avoid this problem, signal::provider_t also provides C-friendlier
* callback support.
*
* The order of arguments is: (this_pointer, signal_name, data_pointer)
*/
using c_api_callback = std::function<void (void*, const char*, void*)>;

/** Register a connection to be called when the given signal is emitted. */
template<class SignalType>
void connect(connection_t<SignalType> *callback)
{
typed_connections[index<SignalType>()].push_back(callback);
callback->connected_to.insert(this);
}

void connect(c_api_callback *cb)
{
untyped_connections.push_back(cb);
}

/** Unregister a connection. */
void disconnect(connection_base_t *callback)
{
callback->connected_to.erase(this);
for (auto& [id, connected] : typed_connections)
{
connected.remove_all(callback);
}
}

void disconnect(c_api_callback *cb)
{
untyped_connections.remove_all(cb);
}

/** Emit the given signal. */
template<class SignalType>
void emit(SignalType *data)
{
auto& conns = typed_connections[std::type_index(typeid(SignalType))];
conns.for_each([&] (connection_base_t *tc)
{
auto real_type = dynamic_cast<connection_t<SignalType>*>(tc);
assert(real_type);
real_type->emit(data);
});

// If the signal provides C-style signal name, then emit it to C-API
// users as well.
if constexpr (detail::has_c_name<SignalType>::value)
{
untyped_connections.for_each([&] (c_api_callback *cb)
{
(*cb)(this, SignalType::c_name, data);
});
}
}

provider_t()
{}

~provider_t()
{
for (auto& [id, connected] : typed_connections)
{
connected.for_each([&] (connection_base_t *base)
{
base->connected_to.erase(this);
});
}
}

// Non-movable, non-copyable: connection_t keeps reference to this object.
// Unclear what happens if this object is duplicated, and plugins usually
// don't want this either.
provider_t(const provider_t& other) = delete;
provider_t& operator =(const provider_t& other) = delete;
provider_t(provider_t&& other) = delete;
provider_t& operator =(provider_t&& other) = delete;

private:
template<class SignalType>
static inline std::type_index index()
{
return std::type_index(typeid(SignalType));
}

wf::safe_list_t<c_api_callback*> untyped_connections;
std::unordered_map<std::type_index, wf::safe_list_t<connection_base_t*>>
typed_connections;
};
}
}
11 changes: 11 additions & 0 deletions src/core/object.cpp
Expand Up @@ -3,6 +3,17 @@
#include <unordered_map>
#include <set>

#include <wayfire/signal-provider.hpp>

void wf::signal::connection_base_t::disconnect()
{
auto connected_copy = this->connected_to;
for (auto& x : connected_copy)
{
x->disconnect(this);
}
}

/* Implementation note: because of circular dependencies between
* signal_connection_t and signal_provider_t, the chosen way to resolve
* them is to have signal_provider_t directly modify signal_connection_t
Expand Down

0 comments on commit 2274148

Please sign in to comment.