diff --git a/macos/Dependencies/setup b/macos/Dependencies/setup index 5d337fa8..3d0c43a4 100755 --- a/macos/Dependencies/setup +++ b/macos/Dependencies/setup @@ -4,25 +4,32 @@ HOST = `clang -dumpmachine`.strip ARCH = HOST[/x86_64|arm64/] def run_build(arch) - puts "Building all dependencies. This'll take a while.\n" - case arch - when "x86_64" - return system("make everything -f .Intel") - when "arm64" + printf("====================================================\n") + printf("Building all dependencies. This'll take a while.\n") + + if `xcodebuild -version`.scan(/Xcode (\d+)/)[0][0].to_i >= 12 + printf("Building libraries for Apple Silicon...\n") + printf("====================================================\n") code = system("make everything -f .AppleSilicon") return code if !code - code = (system("arch -x86_64 make everything -f .Intel")) - return code if !code - return system("./make_macuniversal") - else - raise "Unsupported architecture" end + printf("====================================================\n") + printf("Building libraries for Intel...\n") + printf("====================================================\n") + code = (system("make everything -f .Intel")) + return code if !code + + printf("====================================================\n") + printf("Performing post-setup...\n") + printf("====================================================\n") + printf("Creating universal libraries ...\n") + return system("./make_macuniversal") end def fix_steam(libpath) # Don't need to do anything if it's already set to runpath return 0 if (`otool -L #{libpath}`[/@rpath/]) - puts "Patching Steamworks SDK...\n" + printf("Patching Steamworks SDK...\n") # Remove 32-bit code from the binary if `lipo -info #{libpath}`[/i386/] return 1 if !system("lipo -remove i386 #{libpath} -o #{libpath}") @@ -30,7 +37,6 @@ def fix_steam(libpath) # Set the install name to runpath return 1 if !system("install_name_tool -id @rpath/libsteam_api.dylib #{libpath}") # Resign - puts "Done.\n" return (system("codesign -fs - #{libpath}") ? 0 : 1) end @@ -42,4 +48,6 @@ if File.exists?(STEAM_LIB) exitcode = fix_steam(STEAM_LIB) end +printf("Done.\n\n") + exit(exitcode) diff --git a/src/meson.build b/src/meson.build index a14e576b..672f7c5c 100755 --- a/src/meson.build +++ b/src/meson.build @@ -82,7 +82,7 @@ global_include_dirs += include_directories('.', 'input', 'net', 'system', - 'util' + 'util', 'util/sigslot', 'util/sigslot/adapter' ) global_dependencies += [openal, zlib, bz2, sdl2, sdl_sound, pixman, physfs, vorbisfile, vorbis, ogg, sdl2_ttf, freetype, sdl2_image, png, jpeg] diff --git a/src/util/sigslot/adapter/boost.hpp b/src/util/sigslot/adapter/boost.hpp new file mode 100644 index 00000000..af3182f9 --- /dev/null +++ b/src/util/sigslot/adapter/boost.hpp @@ -0,0 +1,22 @@ +#pragma once +#include +#include + +/** + * Bring ADL conversion for boost smart pointers + */ + +namespace boost { + +template +weak_ptr to_weak(shared_ptr p) { + return {p}; +} + +template +weak_ptr to_weak(weak_ptr p) { + return p; +} + +} // namespace boost + diff --git a/src/util/sigslot/adapter/qt.hpp b/src/util/sigslot/adapter/qt.hpp new file mode 100644 index 00000000..7ac48700 --- /dev/null +++ b/src/util/sigslot/adapter/qt.hpp @@ -0,0 +1,88 @@ +#pragma once +#include +#include +#include +#include +#include + +/** + * Definition of a few adapters that allow object life tracking for QObject, + * QWeakPointer and QSharedPointer objects, when connected to signals. + */ + +namespace sigslot { +namespace detail { + +// a weak pointer adater to allow QObject life tracking +template +struct qpointer_adater { + qpointer_adater(T *o) noexcept + : m_ptr{o} + {} + + void reset() noexcept { + m_ptr.clear(); + } + + bool expired() const noexcept { + return m_ptr.isNull(); + } + + // Warning: very unsafe because QPointer does not provide weak pointer semantics + // In a multithreaded context, m_ptr may very well be destroyed in the lapse of + // time between expired() and data() (and also while data() is being used). + T* lock() const noexcept { + return expired() ? nullptr : m_ptr.data(); + } + +private: + QPointer m_ptr; +}; + +// a wrapper that exposes the right concepts for QWeakPointer +template +struct qweakpointer_adater { + qweakpointer_adater(QWeakPointer o) noexcept + : m_ptr{std::move(o)} + {} + + void reset() noexcept { + m_ptr.clear(); + } + + bool expired() const noexcept { + return m_ptr.isNull(); + } + + QSharedPointer lock() const noexcept { + return m_ptr.lock(); + } + +private: + QWeakPointer m_ptr; +}; + +} // namespace detail +} // namespace sigslot + + +QT_BEGIN_NAMESPACE + +template +std::enable_if_t::value, sigslot::detail::qpointer_adater> +to_weak(T *p) { + return {p}; +} + +template +sigslot::detail::qweakpointer_adater to_weak(QWeakPointer p) { + return {p}; +} + +template +sigslot::detail::qweakpointer_adater to_weak(QSharedPointer p) { + return {p}; +} + +QT_END_NAMESPACE + diff --git a/src/util/sigslot/signal.hpp b/src/util/sigslot/signal.hpp new file mode 100644 index 00000000..3dfd2def --- /dev/null +++ b/src/util/sigslot/signal.hpp @@ -0,0 +1,1611 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#if defined __clang__ || (__GNUC__ > 5) +#define SIGSLOT_MAY_ALIAS __attribute__((__may_alias__)) +#else +#define SIGSLOT_MAY_ALIAS +#endif + +#if defined(__GXX_RTTI) || defined(__cpp_rtti) || defined(_CPPRTTI) +#define SIGSLOT_RTTI_ENABLED 1 +#include +#endif + +namespace sigslot { + +namespace detail { + +// Used to detect an object of observer type +struct observer_type {}; + +} // namespace detail + +namespace trait { + +/// represent a list of types +template struct typelist {}; + +/** + * Pointers that can be converted to a weak pointer concept for tracking + * purpose must implement the to_weak() function in order to make use of + * ADL to convert that type and make it usable + */ + +template +std::weak_ptr to_weak(std::weak_ptr w) { + return w; +} + +template +std::weak_ptr to_weak(std::shared_ptr s) { + return s; +} + +// tools +namespace detail { + +template +struct voider { using type = void; }; + +// void_t from c++17 +template +using void_t = typename detail::voider::type; + +template +struct has_call_operator : std::false_type {}; + +template +struct has_call_operator::type::operator())>> + : std::true_type {}; + +template +struct is_callable : std::false_type {}; + +template +struct is_callable, + void_t()).*std::declval())(std::declval()...))>> + : std::true_type {}; + +template +struct is_callable, + void_t()(std::declval()...))>> + : std::true_type {}; + + +template +struct is_weak_ptr : std::false_type {}; + +template +struct is_weak_ptr().expired()), + decltype(std::declval().lock()), + decltype(std::declval().reset())>> + : std::true_type {}; + +template +struct is_weak_ptr_compatible : std::false_type {}; + +template +struct is_weak_ptr_compatible()))>> + : is_weak_ptr()))> {}; + +} // namespace detail + +static constexpr bool with_rtti = +#ifdef SIGSLOT_RTTI_ENABLED + true; +#else + false; +#endif + +/// determine if a pointer is convertible into a "weak" pointer +template +constexpr bool is_weak_ptr_compatible_v = detail::is_weak_ptr_compatible>::value; + +/// determine if a type T (Callable or Pmf) is callable with supplied arguments +template +constexpr bool is_callable_v = detail::is_callable::value; + +template +constexpr bool is_weak_ptr_v = detail::is_weak_ptr::value; + +template +constexpr bool has_call_operator_v = detail::has_call_operator::value; + +template +constexpr bool is_pointer_v = std::is_pointer::value; + +template +constexpr bool is_func_v = std::is_function::value; + +template +constexpr bool is_pmf_v = std::is_member_function_pointer::value; + +template +constexpr bool is_observer_v = std::is_base_of<::sigslot::detail::observer_type, + std::remove_pointer_t>::value; + +} // namespace trait + +template +class signal_base; + +/** + * A group_id is used to identify a group of slots + */ +using group_id = std::int32_t; + +namespace detail { + +/** + * The following function_traits and object_pointer series of templates are + * used to circumvent the type-erasing that takes place in the slot_base + * implementations. They are used to compare the stored functions and objects + * with another one for disconnection purpose. + */ + +/* + * Function pointers and member function pointers size differ from compiler to + * compiler, and for virtual members compared to non virtual members. On some + * compilers, multiple inheritance has an impact too. Hence, we form an union + * big enough to store any kind of function pointer. + */ +namespace mock { + +struct a { virtual ~a() = default; void f(); virtual void g(); }; +struct b { virtual ~b() = default; virtual void h(); }; +struct c : a, b { void g() override; }; + +union fun_types { + decltype(&c::g) m; + decltype(&a::g) v; + decltype(&a::f) d; + void (*f)(); + void *o; + }; + +} // namespace mock + +/* + * This union is used to compare function pointers + * Generic callables cannot be compared. Here we compare pointers but there is + * no guarantee that this always works. + */ +union SIGSLOT_MAY_ALIAS func_ptr { + void* value() { + return &data[0]; + } + + const void* value() const { + return &data[0]; + } + + template + T& value() { + return *static_cast(value()); + } + + template + const T& value() const { + return *static_cast(value()); + } + + inline explicit operator bool() const { + return value() != nullptr; + } + + inline bool operator==(const func_ptr &o) const { + return std::equal(std::begin(data), std::end(data), std::begin(o.data)); + } + + mock::fun_types _; + char data[sizeof(mock::fun_types)]; +}; + + +template +struct function_traits { + static void ptr(const T &/*t*/, func_ptr &d) { + d.value() = nullptr; + } + + static constexpr bool is_disconnectable = false; + static constexpr bool must_check_object = true; +}; + +template +struct function_traits>> { + static void ptr(T &t, func_ptr &d) { + d.value() = &t; + } + + static constexpr bool is_disconnectable = true; + static constexpr bool must_check_object = false; +}; + +template +struct function_traits>> { + static void ptr(T *t, func_ptr &d) { + d.value() = t; + } + + static constexpr bool is_disconnectable = true; + static constexpr bool must_check_object = false; +}; + +template +struct function_traits>> { + static void ptr(const T &t, func_ptr &d) { + d.value() = t; + } + + static constexpr bool is_disconnectable = trait::with_rtti; + static constexpr bool must_check_object = true; +}; + +// for function objects, the assumption is that we are looking for the call operator +template +struct function_traits>> { + using call_type = decltype(&std::remove_reference::type::operator()); + + static void ptr(const T &/*t*/, func_ptr &d) { + function_traits::ptr(&T::operator(), d); + } + + static constexpr bool is_disconnectable = function_traits::is_disconnectable; + static constexpr bool must_check_object = function_traits::must_check_object; +}; + +template +func_ptr get_function_ptr(const T &t) { + func_ptr d; + std::uninitialized_fill(std::begin(d.data), std::end(d.data), '\0'); + function_traits>::ptr(t, d); + return d; +} + +/* + * obj_ptr is used to store a pointer to an object. + * The object_pointer traits are needed to handle trackable objects correctly, + * as they are likely to not be pointers. + */ +using obj_ptr = const void*; + +template +obj_ptr get_object_ptr(const T &t); + +template +struct object_pointer { + static obj_ptr get(const T&) { + return nullptr; + } +}; + +template +struct object_pointer>> { + static obj_ptr get(const T *t) { + return reinterpret_cast(t); + } +}; + +template +struct object_pointer>> { + static obj_ptr get(const T &t) { + auto p = t.lock(); + return get_object_ptr(p); + } +}; + +template +struct object_pointer && + !trait::is_weak_ptr_v && + trait::is_weak_ptr_compatible_v>> +{ + static obj_ptr get(const T &t) { + return t ? reinterpret_cast(t.get()) : nullptr; + } +}; + +template +obj_ptr get_object_ptr(const T &t) { + return object_pointer::get(t); +} + + +// noop mutex for thread-unsafe use +struct null_mutex { + null_mutex() noexcept = default; + ~null_mutex() noexcept = default; + null_mutex(const null_mutex &) = delete; + null_mutex& operator=(const null_mutex &) = delete; + null_mutex(null_mutex &&) = delete; + null_mutex& operator=(null_mutex &&) = delete; + + inline bool try_lock() noexcept { return true; } + inline void lock() noexcept {} + inline void unlock() noexcept {} +}; + +/** + * A spin mutex that yields, mostly for use in benchmarks and scenarii that invoke + * slots at a very high pace. + * One should almost always prefer a standard mutex over this. + */ +struct spin_mutex { + spin_mutex() noexcept = default; + ~spin_mutex() noexcept = default; + spin_mutex(spin_mutex const&) = delete; + spin_mutex& operator=(const spin_mutex &) = delete; + spin_mutex(spin_mutex &&) = delete; + spin_mutex& operator=(spin_mutex &&) = delete; + + void lock() noexcept { + while (true) { + while (!state.load(std::memory_order_relaxed)) { + std::this_thread::yield(); + } + + if (try_lock()) { + break; + } + } + } + + bool try_lock() noexcept { + return state.exchange(false, std::memory_order_acquire); + } + + void unlock() noexcept { + state.store(true, std::memory_order_release); + } + +private: + std::atomic state {true}; +}; + +/** + * A simple copy on write container that will be used to improve slot lists + * access efficiency in a multithreaded context. + */ +template +class copy_on_write { + struct payload { + payload() = default; + + template + explicit payload(Args && ...args) + : value(std::forward(args)...) + {} + + std::atomic count{1}; + T value; + }; + +public: + using element_type = T; + + copy_on_write() + : m_data(new payload) + {} + + template + explicit copy_on_write(U && x, std::enable_if_t, + copy_on_write>::value>* = nullptr) + : m_data(new payload(std::forward(x))) + {} + + copy_on_write(const copy_on_write &x) noexcept + : m_data(x.m_data) + { + ++m_data->count; + } + + copy_on_write(copy_on_write && x) noexcept + : m_data(x.m_data) + { + x.m_data = nullptr; + } + + ~copy_on_write() { + if (m_data && (--m_data->count == 0)) { + delete m_data; + } + } + + copy_on_write& operator=(const copy_on_write &x) noexcept { + if (&x != this) { + *this = copy_on_write(x); + } + return *this; + } + + copy_on_write& operator=(copy_on_write && x) noexcept { + auto tmp = std::move(x); + swap(*this, tmp); + return *this; + } + + element_type& write() { + if (!unique()) { + *this = copy_on_write(read()); + } + return m_data->value; + } + + const element_type& read() const noexcept { + return m_data->value; + } + + friend inline void swap(copy_on_write &x, copy_on_write &y) noexcept { + using std::swap; + swap(x.m_data, y.m_data); + } + +private: + bool unique() const noexcept { + return m_data->count == 1; + } + +private: + payload *m_data; +}; + +/** + * Specializations for thread-safe code path + */ +template +const T& cow_read(const T &v) { + return v; +} + +template +const T& cow_read(copy_on_write &v) { + return v.read(); +} + +template +T& cow_write(T &v) { + return v; +} + +template +T& cow_write(copy_on_write &v) { + return v.write(); +} + +/** + * std::make_shared instantiates a lot a templates, and makes both compilation time + * and executable size far bigger than they need to be. We offer a make_shared + * equivalent that will avoid most instantiations with the following tradeoffs: + * - Not exception safe, + * - Allocates a separate control block, and will thus make the code slower. + */ +#ifdef SIGSLOT_REDUCE_COMPILE_TIME +template +inline std::shared_ptr make_shared(Arg && ... arg) { + return std::shared_ptr(static_cast(new D(std::forward(arg)...))); +} +#else +template +inline std::shared_ptr make_shared(Arg && ... arg) { + return std::static_pointer_cast(std::make_shared(std::forward(arg)...)); +} +#endif + +/* slot_state holds slot type independent state, to be used to interact with + * slots indirectly through connection and scoped_connection objects. + */ +class slot_state { +public: + constexpr slot_state(group_id gid) noexcept + : m_index(0) + , m_group(gid) + , m_connected(true) + , m_blocked(false) + {} + + virtual ~slot_state() = default; + + virtual bool connected() const noexcept { return m_connected; } + + bool disconnect() noexcept { + bool ret = m_connected.exchange(false); + if (ret) { + do_disconnect(); + } + return ret; + } + + bool blocked() const noexcept { return m_blocked.load(); } + void block() noexcept { m_blocked.store(true); } + void unblock() noexcept { m_blocked.store(false); } + +protected: + virtual void do_disconnect() {} + + auto index() const { + return m_index; + } + + auto& index() { + return m_index; + } + + group_id group() const { + return m_group; + } + +private: + template + friend class ::sigslot::signal_base; + + std::size_t m_index; // index into the array of slot pointers inside the signal + const group_id m_group; // slot group this slot belongs to + std::atomic m_connected; + std::atomic m_blocked; +}; + +} // namespace detail + +/** + * connection_blocker is a RAII object that blocks a connection until destruction + */ +class connection_blocker { +public: + connection_blocker() = default; + ~connection_blocker() noexcept { release(); } + + connection_blocker(const connection_blocker &) = delete; + connection_blocker & operator=(const connection_blocker &) = delete; + + connection_blocker(connection_blocker && o) noexcept + : m_state{std::move(o.m_state)} + {} + + connection_blocker & operator=(connection_blocker && o) noexcept { + release(); + m_state.swap(o.m_state); + return *this; + } + +private: + friend class connection; + explicit connection_blocker(std::weak_ptr s) noexcept + : m_state{std::move(s)} + { + if (auto d = m_state.lock()) { + d->block(); + } + } + + void release() noexcept { + if (auto d = m_state.lock()) { + d->unblock(); + } + } + +private: + std::weak_ptr m_state; +}; + + +/** + * A connection object allows interaction with an ongoing slot connection + * + * It allows common actions such as connection blocking and disconnection. + * Note that connection is not a RAII object, one does not need to hold one + * such object to keep the signal-slot connection alive. + */ +class connection { +public: + connection() = default; + virtual ~connection() = default; + + connection(const connection &) noexcept = default; + connection & operator=(const connection &) noexcept = default; + connection(connection &&) noexcept = default; + connection & operator=(connection &&) noexcept = default; + + bool valid() const noexcept { + return !m_state.expired(); + } + + bool connected() const noexcept { + const auto d = m_state.lock(); + return d && d->connected(); + } + + bool disconnect() noexcept { + auto d = m_state.lock(); + return d && d->disconnect(); + } + + bool blocked() const noexcept { + const auto d = m_state.lock(); + return d && d->blocked(); + } + + void block() noexcept { + if (auto d = m_state.lock()) { + d->block(); + } + } + + void unblock() noexcept { + if (auto d = m_state.lock()) { + d->unblock(); + } + } + + connection_blocker blocker() const noexcept { + return connection_blocker{m_state}; + } + +protected: + template friend class signal_base; + explicit connection(std::weak_ptr s) noexcept + : m_state{std::move(s)} + {} + +protected: + std::weak_ptr m_state; +}; + +/** + * scoped_connection is a RAII version of connection + * It disconnects the slot from the signal upon destruction. + */ +class scoped_connection final : public connection { +public: + scoped_connection() = default; + ~scoped_connection() override { + disconnect(); + } + + /*implicit*/ scoped_connection(const connection &c) noexcept : connection(c) {} + /*implicit*/ scoped_connection(connection &&c) noexcept : connection(std::move(c)) {} + + scoped_connection(const scoped_connection &) noexcept = delete; + scoped_connection & operator=(const scoped_connection &) noexcept = delete; + + scoped_connection(scoped_connection && o) noexcept + : connection{std::move(o.m_state)} + {} + + scoped_connection & operator=(scoped_connection && o) noexcept { + disconnect(); + m_state.swap(o.m_state); + return *this; + } + +private: + template friend class signal_base; + explicit scoped_connection(std::weak_ptr s) noexcept + : connection{std::move(s)} + {} +}; + +/** + * Observer is a base class for intrusive lifetime tracking of objects. + * + * This is an alternative to trackable pointers, such as std::shared_ptr, + * and manual connection management by keeping connection objects in scope. + * Deriving from this class allows automatic disconnection of all the slots + * connected to any signal when an instance is destroyed. + */ +template +struct observer_base : private detail::observer_type { + virtual ~observer_base() = default; + +protected: + /** + * Disconnect all signals connected to this object. + * + * To avoid invocation of slots on a semi-destructed instance, which may happen + * in multi-threaded contexts, derived classes should call this method in their + * destructor. This will ensure proper disconnection prior to the destruction. + */ + void disconnect_all() { + std::unique_lock _{m_mutex}; + m_connections.clear(); + } + +private: + template + friend class signal_base; + + void add_connection(connection conn) { + std::unique_lock _{m_mutex}; + m_connections.emplace_back(std::move(conn)); + } + + Lockable m_mutex; + std::vector m_connections; +}; + +/** + * Specialization of observer_base to be used in single threaded contexts. + */ +using observer_st = observer_base; + +/** + * Specialization of observer_base to be used in multi-threaded contexts. + */ +using observer = observer_base; + + +namespace detail { + +// interface for cleanable objects, used to cleanup disconnected slots +struct cleanable { + virtual ~cleanable() = default; + virtual void clean(slot_state *) = 0; +}; + +template +class slot_base; + +template +using slot_ptr = std::shared_ptr>; + + +/* A base class for slot objects. This base type only depends on slot argument + * types, it will be used as an element in an intrusive singly-linked list of + * slots, hence the public next member. + */ +template +class slot_base : public slot_state { +public: + using base_types = trait::typelist; + + explicit slot_base(cleanable &c, group_id gid) + : slot_state(gid) + , cleaner(c) + {} + ~slot_base() override = default; + + // method effectively responsible for calling the "slot" function with + // supplied arguments whenever emission happens. + virtual void call_slot(Args...) = 0; + + template + void operator()(U && ...u) { + if (slot_state::connected() && !slot_state::blocked()) { + call_slot(std::forward(u)...); + } + } + + // check if we are storing callable c + template + bool has_callable(const C &c) const { + auto cp = get_function_ptr(c); + auto p = get_callable(); + return cp && p && cp == p; + } + + template + std::enable_if_t::must_check_object, bool> + has_full_callable(const C &c) const { + return has_callable(c) && check_class_type>(); + } + + template + std::enable_if_t::must_check_object, bool> + has_full_callable(const C &c) const { + return has_callable(c); + } + + // check if we are storing object o + template + bool has_object(const O &o) const { + return get_object() == get_object_ptr(o); + } + +protected: + void do_disconnect() final { + cleaner.clean(this); + } + + // retieve a pointer to the object embedded in the slot + virtual obj_ptr get_object() const noexcept { + return nullptr; + } + + // retieve a pointer to the callable embedded in the slot + virtual func_ptr get_callable() const noexcept { + return get_function_ptr(nullptr); + } + +#ifdef SIGSLOT_RTTI_ENABLED + // retieve a pointer to the callable embedded in the slot + virtual const std::type_info& get_callable_type() const noexcept { + return typeid(nullptr); + } + +private: + template + bool check_class_type() const { + return typeid(U) == get_callable_type(); + } + +#else + template + bool check_class_type() const { + return false; + } +#endif + +private: + cleanable &cleaner; +}; + +/* + * A slot object holds state information, and a callable to to be called + * whenever the function call operator of its slot_base base class is called. + */ +template +class slot final : public slot_base { +public: + template + constexpr slot(cleanable &c, F && f, Gid gid) + : slot_base(c, gid) + , func{std::forward(f)} {} + +protected: + void call_slot(Args ...args) override { + func(args...); + } + + func_ptr get_callable() const noexcept override { + return get_function_ptr(func); + } + +#ifdef SIGSLOT_RTTI_ENABLED + const std::type_info& get_callable_type() const noexcept override { + return typeid(func); + } +#endif + +private: + std::decay_t func; +}; + +/* + * Variation of slot that prepends a connection object to the callable + */ +template +class slot_extended final : public slot_base { +public: + template + constexpr slot_extended(cleanable &c, F && f, group_id gid) + : slot_base(c, gid) + , func{std::forward(f)} {} + + connection conn; + +protected: + void call_slot(Args ...args) override { + func(conn, args...); + } + + func_ptr get_callable() const noexcept override { + return get_function_ptr(func); + } + +#ifdef SIGSLOT_RTTI_ENABLED + const std::type_info& get_callable_type() const noexcept override { + return typeid(func); + } +#endif + +private: + std::decay_t func; +}; + +/* + * A slot object holds state information, an object and a pointer over member + * function to be called whenever the function call operator of its slot_base + * base class is called. + */ +template +class slot_pmf final : public slot_base { +public: + template + constexpr slot_pmf(cleanable &c, F && f, P && p, group_id gid) + : slot_base(c, gid) + , pmf{std::forward(f)} + , ptr{std::forward

(p)} {} + +protected: + void call_slot(Args ...args) override { + ((*ptr).*pmf)(args...); + } + + func_ptr get_callable() const noexcept override { + return get_function_ptr(pmf); + } + + obj_ptr get_object() const noexcept override { + return get_object_ptr(ptr); + } + +#ifdef SIGSLOT_RTTI_ENABLED + const std::type_info& get_callable_type() const noexcept override { + return typeid(pmf); + } +#endif + +private: + std::decay_t pmf; + std::decay_t ptr; +}; + +/* + * Variation of slot that prepends a connection object to the callable + */ +template +class slot_pmf_extended final : public slot_base { +public: + template + constexpr slot_pmf_extended(cleanable &c, F && f, P && p, group_id gid) + : slot_base(c, gid) + , pmf{std::forward(f)} + , ptr{std::forward

(p)} {} + + connection conn; + +protected: + void call_slot(Args ...args) override { + ((*ptr).*pmf)(conn, args...); + } + + func_ptr get_callable() const noexcept override { + return get_function_ptr(pmf); + } + obj_ptr get_object() const noexcept override { + return get_object_ptr(ptr); + } + +#ifdef SIGSLOT_RTTI_ENABLED + const std::type_info& get_callable_type() const noexcept override { + return typeid(pmf); + } +#endif + +private: + std::decay_t pmf; + std::decay_t ptr; +}; + +/* + * An implementation of a slot that tracks the life of a supplied object + * through a weak pointer in order to automatically disconnect the slot + * on said object destruction. + */ +template +class slot_tracked final : public slot_base { +public: + template + constexpr slot_tracked(cleanable &c, F && f, P && p, group_id gid) + : slot_base(c, gid) + , func{std::forward(f)} + , ptr{std::forward

(p)} + {} + + bool connected() const noexcept override { + return !ptr.expired() && slot_state::connected(); + } + +protected: + void call_slot(Args ...args) override { + auto sp = ptr.lock(); + if (!sp) { + slot_state::disconnect(); + return; + } + if (slot_state::connected()) { + func(args...); + } + } + + func_ptr get_callable() const noexcept override { + return get_function_ptr(func); + } + + obj_ptr get_object() const noexcept override { + return get_object_ptr(ptr); + } + +#ifdef SIGSLOT_RTTI_ENABLED + const std::type_info& get_callable_type() const noexcept override { + return typeid(func); + } +#endif + +private: + std::decay_t func; + std::decay_t ptr; +}; + +/* + * An implementation of a slot as a pointer over member function, that tracks + * the life of a supplied object through a weak pointer in order to automatically + * disconnect the slot on said object destruction. + */ +template +class slot_pmf_tracked final : public slot_base { +public: + template + constexpr slot_pmf_tracked(cleanable &c, F && f, P && p, group_id gid) + : slot_base(c, gid) + , pmf{std::forward(f)} + , ptr{std::forward

(p)} + {} + + bool connected() const noexcept override { + return !ptr.expired() && slot_state::connected(); + } + +protected: + void call_slot(Args ...args) override { + auto sp = ptr.lock(); + if (!sp) { + slot_state::disconnect(); + return; + } + if (slot_state::connected()) { + ((*sp).*pmf)(args...); + } + } + + func_ptr get_callable() const noexcept override { + return get_function_ptr(pmf); + } + + obj_ptr get_object() const noexcept override { + return get_object_ptr(ptr); + } + +#ifdef SIGSLOT_RTTI_ENABLED + const std::type_info& get_callable_type() const noexcept override { + return typeid(pmf); + } +#endif + +private: + std::decay_t pmf; + std::decay_t ptr; +}; + +} // namespace detail + + +/** + * signal_base is an implementation of the observer pattern, through the use + * of an emitting object and slots that are connected to the signal and called + * with supplied arguments when a signal is emitted. + * + * signal_base is the general implementation, whose locking policy must be + * set in order to decide thread safety guarantees. signal and signal_st + * are partial specializations for multi-threaded and single-threaded use. + * + * It does not allow slots to return a value. + * + * Slot execution order can be constrained by assigning group ids to the slots. + * The execution order of slots in a same group is unspecified and should not be + * relied upon, however groups are executed in ascending group ids order. When + * the group id of a slot is not set, it is assigned to the group 0. Group ids + * can have any value in the range of signed 32 bit integers. + * + * @tparam Lockable a lock type to decide the lock policy + * @tparam T... the argument types of the emitting and slots functions. + */ +template +class signal_base final : public detail::cleanable { + template + using is_thread_safe = std::integral_constant::value>; + + template + using cow_type = std::conditional_t::value, + detail::copy_on_write, U>; + + template + using cow_copy_type = std::conditional_t::value, + detail::copy_on_write, const U&>; + + using lock_type = std::unique_lock; + using slot_base = detail::slot_base; + using slot_ptr = detail::slot_ptr; + using slots_type = std::vector; + struct group_type { slots_type slts; group_id gid; }; + using list_type = std::vector; // kept ordered by ascending gid + +public: + using arg_list = trait::typelist; + using ext_arg_list = trait::typelist; + + signal_base() noexcept : m_block(false) {} + ~signal_base() override { + disconnect_all(); + } + + signal_base(const signal_base&) = delete; + signal_base & operator=(const signal_base&) = delete; + + signal_base(signal_base && o) /* not noexcept */ + : m_block{o.m_block.load()} + { + lock_type lock(o.m_mutex); + using std::swap; + swap(m_slots, o.m_slots); + } + + signal_base & operator=(signal_base && o) /* not noexcept */ { + lock_type lock1(m_mutex, std::defer_lock); + lock_type lock2(o.m_mutex, std::defer_lock); + std::lock(lock1, lock2); + + using std::swap; + swap(m_slots, o.m_slots); + m_block.store(o.m_block.exchange(m_block.load())); + return *this; + } + + /** + * Emit a signal + * + * Effect: All non blocked and connected slot functions will be called + * with supplied arguments. + * Safety: With proper locking (see pal::signal), emission can happen from + * multiple threads simultaneously. The guarantees only apply to the + * signal object, it does not cover thread safety of potentially + * shared state used in slot functions. + * + * @param a... arguments to emit + */ + template + void operator()(U && ...a) { + if (m_block) { + return; + } + + // Reference to the slots to execute them out of the lock + // a copy may occur if another thread writes to it. + cow_copy_type ref = slots_reference(); + + for (const auto &group : detail::cow_read(ref)) { + for (const auto &s : group.slts) { + s->operator()(a...); + } + } + } + + /** + * Connect a callable of compatible arguments + * + * Effect: Creates and stores a new slot responsible for executing the + * supplied callable for every subsequent signal emission. + * Safety: Thread-safety depends on locking policy. + * + * @param c a callable + * @param gid an identifier that can be used to order slot execution + * @return a connection object that can be used to interact with the slot + */ + template + std::enable_if_t, connection> + connect(Callable && c, group_id gid = 0) { + using slot_t = detail::slot; + auto s = make_slot(std::forward(c), gid); + connection conn(s); + add_slot(std::move(s)); + return conn; + } + + /** + * Connect a callable with an additional connection argument + * + * The callable's first argument must be of type connection. This overload + * the callable to manage it's own connection through this argument. + * + * @param c a callable + * @param gid an identifier that can be used to order slot execution + * @return a connection object that can be used to interact with the slot + */ + template + std::enable_if_t, connection> + connect_extended(Callable && c, group_id gid = 0) { + using slot_t = detail::slot_extended; + auto s = make_slot(std::forward(c), gid); + connection conn(s); + std::static_pointer_cast(s)->conn = conn; + add_slot(std::move(s)); + return conn; + } + + /** + * Overload of connect for pointers over member functions derived from + * observer + * + * @param pmf a pointer over member function + * @param ptr an object pointer derived from observer + * @param gid an identifier that can be used to order slot execution + * @return a connection object that can be used to interact with the slot + */ + template + std::enable_if_t && + trait::is_observer_v, connection> + connect(Pmf && pmf, Ptr && ptr, group_id gid = 0) { + using slot_t = detail::slot_pmf; + auto s = make_slot(std::forward(pmf), std::forward(ptr), gid); + connection conn(s); + add_slot(std::move(s)); + ptr->add_connection(conn); + return conn; + } + + /** + * Overload of connect for pointers over member functions + * + * @param pmf a pointer over member function + * @param ptr an object pointer + * @param gid an identifier that can be used to order slot execution + * @return a connection object that can be used to interact with the slot + */ + template + std::enable_if_t && + !trait::is_observer_v && + !trait::is_weak_ptr_compatible_v, connection> + connect(Pmf && pmf, Ptr && ptr, group_id gid = 0) { + using slot_t = detail::slot_pmf; + auto s = make_slot(std::forward(pmf), std::forward(ptr), gid); + connection conn(s); + add_slot(std::move(s)); + return conn; + } + + /** + * Overload of connect for pointer over member functions and + * + * @param pmf a pointer over member function + * @param ptr an object pointer + * @param gid an identifier that can be used to order slot execution + * @return a connection object that can be used to interact with the slot + */ + template + std::enable_if_t && + !trait::is_weak_ptr_compatible_v, connection> + connect_extended(Pmf && pmf, Ptr && ptr, group_id gid = 0) { + using slot_t = detail::slot_pmf_extended; + auto s = make_slot(std::forward(pmf), std::forward(ptr), gid); + connection conn(s); + std::static_pointer_cast(s)->conn = conn; + add_slot(std::move(s)); + return conn; + } + + /** + * Overload of connect for lifetime object tracking and automatic disconnection + * + * Ptr must be convertible to an object following a loose form of weak pointer + * concept, by implementing the ADL-detected conversion function to_weak(). + * + * This overload covers the case of a pointer over member function and a + * trackable pointer of that class. + * + * Note: only weak references are stored, a slot does not extend the lifetime + * of a suppied object. + * + * @param pmf a pointer over member function + * @param ptr a trackable object pointer + * @param gid an identifier that can be used to order slot execution + * @return a connection object that can be used to interact with the slot + */ + template + std::enable_if_t && + trait::is_weak_ptr_compatible_v, connection> + connect(Pmf && pmf, Ptr && ptr, group_id gid = 0) { + using trait::to_weak; + auto w = to_weak(std::forward(ptr)); + using slot_t = detail::slot_pmf_tracked; + auto s = make_slot(std::forward(pmf), w, gid); + connection conn(s); + add_slot(std::move(s)); + return conn; + } + + /** + * Overload of connect for lifetime object tracking and automatic disconnection + * + * Trackable must be convertible to an object following a loose form of weak + * pointer concept, by implementing the ADL-detected conversion function to_weak(). + * + * This overload covers the case of a standalone callable and unrelated trackable + * object. + * + * Note: only weak references are stored, a slot does not extend the lifetime + * of a suppied object. + * + * @param c a callable + * @param ptr a trackable object pointer + * @param gid an identifier that can be used to order slot execution + * @return a connection object that can be used to interact with the slot + */ + template + std::enable_if_t && + trait::is_weak_ptr_compatible_v, connection> + connect(Callable && c, Trackable && ptr, group_id gid = 0) { + using trait::to_weak; + auto w = to_weak(std::forward(ptr)); + using slot_t = detail::slot_tracked; + auto s = make_slot(std::forward(c), w, gid); + connection conn(s); + add_slot(std::move(s)); + return conn; + } + + /** + * Creates a connection whose duration is tied to the return object + * Use the same semantics as connect + */ + template + scoped_connection connect_scoped(CallArgs && ...args) { + return connect(std::forward(args)...); + } + + /** + * Disconnect slots bound to a callable + * + * Effect: Disconnects all the slots bound to the callable in argument. + * Safety: Thread-safety depends on locking policy. + * + * If the callable is a free or static member function, this overload is always + * available. However, RTTI is needed for it to work for pointer to member + * functions, function objects or and (references to) lambdas, because the + * C++ spec does not mandate the pointers to member functions to be unique. + * + * @param c a callable + * @return the number of disconnected slots + */ + template + std::enable_if_t<(trait::is_callable_v || + trait::is_callable_v || + trait::is_pmf_v) && + detail::function_traits::is_disconnectable, size_t> + disconnect(const Callable &c) { + return disconnect_if([&] (const auto &s) { + return s->has_full_callable(c); + }); + } + + /** + * Disconnect slots bound to this object + * + * Effect: Disconnects all the slots bound to the object or tracked object + * in argument. + * Safety: Thread-safety depends on locking policy. + * + * The object may be a pointer or trackable object. + * + * @param obj an object + * @return the number of disconnected slots + */ + template + std::enable_if_t && + !trait::is_callable_v && + !trait::is_pmf_v, size_t> + disconnect(const Obj &obj) { + return disconnect_if([&] (const auto &s) { + return s->has_object(obj); + }); + } + + /** + * Disconnect slots bound both to a callable and object + * + * Effect: Disconnects all the slots bound to the callable and object in argument. + * Safety: Thread-safety depends on locking policy. + * + * For naked pointers, the Callable is expected to be a pointer over member + * function. If obj is trackable, any kind of Callable can be used. + * + * @param c a callable + * @param obj an object + * @return the number of disconnected slots + */ + template + size_t disconnect(const Callable &c, const Obj &obj) { + return disconnect_if([&] (const auto &s) { + return s->has_object(obj) && s->has_callable(c); + }); + } + + /** + * Disconnect slots in a particular group + * + * Effect: Disconnects all the slots in the group id in argument. + * Safety: Thread-safety depends on locking policy. + * + * @param gid a group id + * @return the number of disconnected slots + */ + size_t disconnect(group_id gid) { + lock_type lock(m_mutex); + for (auto &group : detail::cow_write(m_slots)) { + if (group.gid == gid) { + size_t count = group.slts.size(); + group.slts.clear(); + return count; + } + } + return 0; + } + + /** + * Disconnects all the slots + * Safety: Thread safety depends on locking policy + */ + void disconnect_all() { + lock_type lock(m_mutex); + clear(); + } + + /** + * Blocks signal emission + * Safety: thread safe + */ + void block() noexcept { + m_block.store(true); + } + + /** + * Unblocks signal emission + * Safety: thread safe + */ + void unblock() noexcept { + m_block.store(false); + } + + /** + * Tests blocking state of signal emission + */ + bool blocked() const noexcept { + return m_block.load(); + } + + /** + * Get number of connected slots + * Safety: thread safe + */ + size_t slot_count() noexcept { + cow_copy_type ref = slots_reference(); + size_t count = 0; + for (const auto &g : detail::cow_read(ref)) { + count += g.slts.size(); + } + return count; + } + +protected: + /** + * remove disconnected slots + */ + void clean(detail::slot_state *state) override { + lock_type lock(m_mutex); + const auto idx = state->index(); + const auto gid = state->group(); + + // find the group + for (auto &group : detail::cow_write(m_slots)) { + if (group.gid == gid) { + auto &slts = group.slts; + + // ensure we have the right slot, in case of concurrent cleaning + if (idx < slts.size() && slts[idx] && slts[idx].get() == state) { + std::swap(slts[idx], slts.back()); + slts[idx]->index() = idx; + slts.pop_back(); + } + + return; + } + } + } + +private: + // used to get a reference to the slots for reading + inline cow_copy_type slots_reference() { + lock_type lock(m_mutex); + return m_slots; + } + + // create a new slot + template + inline auto make_slot(A && ...a) { + return detail::make_shared(*this, std::forward(a)...); + } + + // add the slot to the list of slots of the right group + void add_slot(slot_ptr &&s) { + const group_id gid = s->group(); + + lock_type lock(m_mutex); + auto &groups = detail::cow_write(m_slots); + + // find the group + auto it = groups.begin(); + while (it != groups.end() && it->gid < gid) { + it++; + } + + // create a new group if necessary + if (it == groups.end() || it->gid != gid) { + it = groups.insert(it, {{}, gid}); + } + + // add the slot + s->index() = it->slts.size(); + it->slts.push_back(std::move(s)); + } + + // disconnect a slot if a condition occurs + template + size_t disconnect_if(Cond && cond) { + lock_type lock(m_mutex); + auto &groups = detail::cow_write(m_slots); + + size_t count = 0; + + for (auto &group : groups) { + auto &slts = group.slts; + size_t i = 0; + while (i < slts.size()) { + if (cond(slts[i])) { + std::swap(slts[i], slts.back()); + slts[i]->index() = i; + slts.pop_back(); + ++count; + } else { + ++i; + } + } + } + + return count; + } + + // to be called under lock: remove all the slots + void clear() { + detail::cow_write(m_slots).clear(); + } + +private: + Lockable m_mutex; + cow_type m_slots; + std::atomic m_block; +}; + +/** + * Specialization of signal_base to be used in single threaded contexts. + * Slot connection, disconnection and signal emission are not thread-safe. + * The performance improvement over the thread-safe variant is not impressive, + * so this is not very useful. + */ +template +using signal_st = signal_base; + +/** + * Specialization of signal_base to be used in multi-threaded contexts. + * Slot connection, disconnection and signal emission are thread-safe. + * + * Recursive signal emission and emission cycles are supported too. + */ +template +using signal = signal_base; + +} // namespace sigslot