#pragma once #include "boost.hpp" #include "intrusive_ptr_target.hpp" #include "Memory.hpp" #include "intrusive_weak_ptr.hpp" #include "boost/any.hpp" #include "boost/type_traits.hpp" #include "threadsafe.hpp" #include "Debug.hpp" #include #include "callable.hpp" // aya - instead of intrusive_ptr, use shared_ptr on unix using boost::shared_ptr; using boost::weak_ptr; LOGGROUP(ScopedConnection); #ifdef _DEBUG #define AYA_SIGNALS_DEBUGGING #endif #ifdef AYA_SIGNALS_DEBUGGING #define AYA_SIGNALS_ASSERT AYA_CRASH_ASSERT #pragma optimize("", off) #else #define AYA_SIGNALS_ASSERT AYAASSERT #endif namespace Aya { // The classes in this namespace mimic a small fraction of the features contained // in boost signals namespace signals { /* This signal class is similar to boost::signal, but with a few important differences. First, it doesn't implement nearly as much functionality as boost's. It merely implements those portions that Roblox uses. The big advantage to this implementation is its (limited) thread safety. Any thread is allowed to connect new slots to this signal and also disconnect them at any time. The firing of a signal is not thread safe - only one thread is allowed to fire at a time. */ // Set this to whatever you want to handle exceptions thrown by a slot extern boost::function slot_exception_handler; class connection { public: class islot : boost::noncopyable #if 0 // No need to test maxStrong, since all strong references are internal // However, weak references are external via connection object. It is // expected that the total weak references to a slot are much less than 64000 // NOTE: unsigned short is slightly slower than int on Win32. However, it // saves 4 bytes. , public Aya::intrusive_ptr_target #else , public Aya::intrusive_ptr_target #endif { protected: islot() {} public: virtual ~islot() {} virtual void disconnect() = 0; virtual bool connected() const = 0; }; inline connection(islot* slot) : weak_slot(slot) { } inline connection(const connection& con) : weak_slot(con.weak_slot) { } inline connection() {} connection& operator=(const connection& con); void disconnect() const; bool connected() const; bool operator==(const connection& other) const; bool operator!=(const connection& other) const; void flogPrint() { boost::intrusive_ptr s(weak_slot.lock()); FASTLOG2(FLog::Always, "Connection %p, slot %p", this, s.get()); } private: // to make connections copyable, the data for a connection are shared Aya::intrusive_weak_ptr weak_slot; // must be weak to avoid memory leaks }; class scoped_connection : boost::noncopyable { // Has-a instead of Is-a. We do this because demoting scoped_connection reference // to a connection will alter the meaning of the = operator, leading to strange // bugs. connection con; public: inline scoped_connection() {} inline scoped_connection(const connection& con) : con(con) { } inline scoped_connection& operator=(const connection& con) { if (this->con != con) { disconnect(); this->con = con; } return *this; } inline ~scoped_connection() { disconnect(); } // Accessor to underlying connection, if you really want it inline connection& get() { return con; } // implementation of connection contract inline void disconnect() const { con.disconnect(); } inline bool connected() const { return con.connected(); } inline bool operator==(const connection& other) const { return con == other; } inline bool operator!=(const connection& other) const { return con != other; } }; class scoped_connection_logged : boost::noncopyable { // Has-a instead of Is-a. We do this because demoting scoped_connection reference // to a connection will alter the meaning of the = operator, leading to strange // bugs. connection con; bool logged; public: inline scoped_connection_logged() : logged(false) { } inline scoped_connection_logged(bool logged) : logged(logged) { } // Helper for using FastLog groups as trigger inline scoped_connection_logged(FLog::Channel channelId) : logged(channelId != 0) { } inline scoped_connection_logged(const connection& con) : con(con) { } inline scoped_connection_logged& operator=(const connection& con) { if (this->con != con) { disconnect(); this->con = con; if (logged) { FASTLOG2(FLog::Always, "Scoped connection %p assign: %p", this, &con); } } return *this; } inline ~scoped_connection_logged() { if (logged) FASTLOG1(FLog::Always, "Scoped connection %p destructor", this); disconnect(); } // Accessor to underlying connection, if you really want it inline connection& get() { return con; } inline void setLogged(bool logged) { this->logged = logged; } // implementation of connection contract inline void disconnect() const { if (logged) FASTLOG2(FLog::Always, "Scoped connection %p disconnect, previously connected: %u", this, con.connected()); con.disconnect(); } inline bool connected() const { return con.connected(); } inline bool operator==(const connection& other) const { return con == other; } inline bool operator!=(const connection& other) const { return con != other; } }; template class signal : boost::noncopyable { protected: friend class slot; class slot : public connection::islot , public icallable::arity, Signature> { public: boost::intrusive_ptr next; signal* sig; inline slot(signal* sig) : sig(sig) { } virtual bool connected() const { return sig != NULL; } public: SAFE_HEAP_STATIC(boost::mutex, mutex); virtual void disconnect() { if (!sig) return; boost::mutex::scoped_lock lock(mutex()); if (sig) { signal* s = sig; sig = NULL; s->remove(this); } } }; template class callable_slot : public callable::arity, Signature> { public: inline callable_slot(const Delegate& deleg, signal* sig) : callable::arity, Signature>(deleg, sig) { } }; private: // The slots are stored in a linked list, with "head" as a dummy slot used to anchor the list. boost::intrusive_ptr head; // TODO: Avoid contention by using one mutex per signal? Or an array of signals? // TODO: Is boost::mutex the best choice? Does it start up with a spin? // However, at least we have a separate mutex for each signature // SAFE_HEAP_STATIC is used instead of SAFE_STATIC to work around global variables using signals (like GameSettings) // If you use SAFE_STATIC, mutex can be destroyed before other global variables using signals, // so signal destructor will fail on mutex access SAFE_HEAP_STATIC(boost::mutex, mutex) void remove(slot* item) { // Invariant: the value of item->next does not change AYAASSERT(!boost::intrusive_ptr_expired(item)); if (item == head) head = item->next; else { // Find "prev". This is O(n) slot* prev = head.get(); // TODO: Can we just assert that prev!=NULL? while (prev && prev->next != item) prev = prev->next.get(); // In theory prev should never be NULL, because for it to be NULL // the slot would be destroyed, in which case remove() can't be // called. Let's play it safe and null-check anyway. AYA_SIGNALS_ASSERT(!prev || prev->next.get() == item); if (prev) prev->next = item->next; } AYAASSERT(!boost::intrusive_ptr_expired(item)); // item is now deletable } void insert(slot* item) { AYA_SIGNALS_ASSERT(item); boost::mutex::scoped_lock lock(mutex()); if (!head) { head = item; } else { item->next = head; head = item; } } public: inline signal() { mutex(); } inline ~signal() { disconnectAll(); } void disconnectAll() { while (head) { boost::intrusive_ptr node; { boost::mutex::scoped_lock lock(mutex()); // See DE131 for a justification of this "chunk" code const int chunkSize = 10; int count = chunkSize; for (node = head; node; node = node->next) { node->sig = NULL; if (count-- == 0) { // After 10 iterations we need to break out and collect // the slots. Otherwise we risk a stack crash break; } } } // the next line will cause nodes to be destroyed. // Notice that we want them to be destroyed // outside of the mutex lock because // destruction could have side-effects. head = node; } } inline bool empty() const { return !head; } template connection connect(const Delegate& function) { slot* item = new callable_slot(function, this); insert(item); return connection(item); } // For debugging: static size_t sizeof_slot() { return sizeof(slot); } void flogPrint() { printf("Signal - %p\n", this); boost::intrusive_ptr::slot> item; while (this->next(item)) printf("Signal slot = %p\n", item.get()); // FASTLOG1(FLog::Always, "Signal slot = %p", item.get()); } protected: void on_error(std::exception& e) { if (slot_exception_handler) slot_exception_handler(e); } bool next(boost::intrusive_ptr& item) { if (!item) { // Start iterating; this is safe to read from // If another thread is in the process of prepending, we can get old head or new head // If we do get the new head it should already have the new next so this is race-free item = this->head; } else { // Advance the iterator; we keep item alive so next is safe to read from // If another thread is in the process of removing the 'item->next' connection we may see // the next pointer either pointing to the element that's being removed or to the next one // Since replacing next is atomic and we can't observe any other values than these two this is // also race-free. item = item->next; } if (!item) { // Done iterating return false; } else { // Iteration succeeded return true; } } }; template class signal_with_args; template class signal_with_args<0, Signature> : public signal { static inline void fireItem(typename signal::slot* item) { if (item->sig) // Make sure this guy hasn't been disconnected item->call(); } public: void operator()() { if (this->empty()) return; typedef typename Aya::signals::signal::slot slot; boost::intrusive_ptr item; begin: try { while (this->next(item)) fireItem(item.get()); } catch (Aya::base_exception& e) { Aya::signals::signal::on_error(e); // Note: We put this handler on the outside of the for loop // as an optimization. This is why we have a goto statement. goto begin; } } }; template class signal_with_args<1, Signature> : public signal { static inline void fireItem(typename signal::slot* item, typename boost::function_traits::arg1_type arg1) { if (item->sig) // Make sure this guy hasn't been disconnected item->call(arg1); } public: void operator()(typename boost::function_traits::arg1_type arg1) { if (this->empty()) return; boost::intrusive_ptr::slot> item; begin: try { while (this->next(item)) fireItem(item.get(), arg1); } catch (Aya::base_exception& e) { Aya::signals::signal::on_error(e); // Note: We put this handler on the outside of the for loop // as an optimization. This is why we have a goto statement. goto begin; } } }; template class signal_with_args<2, Signature> : public signal { static inline void fireItem(typename signal::slot* item, typename boost::function_traits::arg1_type arg1, typename boost::function_traits::arg2_type arg2) { if (item->sig) // Make sure this guy hasn't been disconnected item->call(arg1, arg2); } public: void operator()(typename boost::function_traits::arg1_type arg1, typename boost::function_traits::arg2_type arg2) { if (this->empty()) return; boost::intrusive_ptr::slot> item; begin: try { while (this->next(item)) fireItem(item.get(), arg1, arg2); } catch (Aya::base_exception& e) { Aya::signals::signal::on_error(e); // Note: We put this handler on the outside of the for loop // as an optimization. This is why we have a goto statement. goto begin; } } }; template class signal_with_args<3, Signature> : public signal { static inline void fireItem(typename signal::slot* item, typename boost::function_traits::arg1_type arg1, typename boost::function_traits::arg2_type arg2, typename boost::function_traits::arg3_type arg3) { if (item->sig) // Make sure this guy hasn't been disconnected item->call(arg1, arg2, arg3); } public: void operator()(typename boost::function_traits::arg1_type arg1, typename boost::function_traits::arg2_type arg2, typename boost::function_traits::arg3_type arg3) { if (this->empty()) return; boost::intrusive_ptr::slot> item; begin: try { while (this->next(item)) fireItem(item.get(), arg1, arg2, arg3); } catch (Aya::base_exception& e) { Aya::signals::signal::on_error(e); // Note: We put this handler on the outside of the for loop // as an optimization. This is why we have a goto statement. goto begin; } } }; template class signal_with_args<4, Signature> : public signal { static inline void fireItem(typename signal::slot* item, typename boost::function_traits::arg1_type arg1, typename boost::function_traits::arg2_type arg2, typename boost::function_traits::arg3_type arg3, typename boost::function_traits::arg4_type arg4) { if (item->sig) // Make sure this guy hasn't been disconnected item->call(arg1, arg2, arg3, arg4); } public: void operator()(typename boost::function_traits::arg1_type arg1, typename boost::function_traits::arg2_type arg2, typename boost::function_traits::arg3_type arg3, typename boost::function_traits::arg4_type arg4) { if (this->empty()) return; boost::intrusive_ptr::slot> item; begin: try { while (this->next(item)) fireItem(item.get(), arg1, arg2, arg3, arg4); } catch (Aya::base_exception& e) { Aya::signals::signal::on_error(e); // Note: We put this handler on the outside of the for loop // as an optimization. This is why we have a goto statement. goto begin; } } }; template class signal_with_args<5, Signature> : public signal { static inline void fireItem(typename signal::slot* item, typename boost::function_traits::arg1_type arg1, typename boost::function_traits::arg2_type arg2, typename boost::function_traits::arg3_type arg3, typename boost::function_traits::arg4_type arg4, typename boost::function_traits::arg5_type arg5) { if (item->sig) // Make sure this guy hasn't been disconnected item->call(arg1, arg2, arg3, arg4, arg5); } public: void operator()(typename boost::function_traits::arg1_type arg1, typename boost::function_traits::arg2_type arg2, typename boost::function_traits::arg3_type arg3, typename boost::function_traits::arg4_type arg4, typename boost::function_traits::arg5_type arg5) { if (this->empty()) return; boost::intrusive_ptr::slot> item; begin: try { while (this->next(item)) fireItem(item.get(), arg1, arg2, arg3, arg4, arg5); } catch (Aya::base_exception& e) { Aya::signals::signal::on_error(e); // Note: We put this handler on the outside of the for loop // as an optimization. This is why we have a goto statement. goto begin; } } }; template class signal_with_args<6, Signature> : public signal { static inline void fireItem(typename signal::slot* item, typename boost::function_traits::arg1_type arg1, typename boost::function_traits::arg2_type arg2, typename boost::function_traits::arg3_type arg3, typename boost::function_traits::arg4_type arg4, typename boost::function_traits::arg5_type arg5, typename boost::function_traits::arg6_type arg6) { if (item->sig) // Make sure this guy hasn't been disconnected item->call(arg1, arg2, arg3, arg4, arg5, arg6); } public: void operator()(typename boost::function_traits::arg1_type arg1, typename boost::function_traits::arg2_type arg2, typename boost::function_traits::arg3_type arg3, typename boost::function_traits::arg4_type arg4, typename boost::function_traits::arg5_type arg5, typename boost::function_traits::arg6_type arg6) { if (this->empty()) return; boost::intrusive_ptr::slot> item; begin: try { while (this->next(item)) fireItem(item.get(), arg1, arg2, arg3, arg4, arg5, arg6); } catch (Aya::base_exception& e) { Aya::signals::signal::on_error(e); // Note: We put this handler on the outside of the for loop // as an optimization. This is why we have a goto statement. goto begin; } } }; template class signal_with_args<7, Signature> : public signal { static inline void fireItem(typename signal::slot* item, typename boost::function_traits::arg1_type arg1, typename boost::function_traits::arg2_type arg2, typename boost::function_traits::arg3_type arg3, typename boost::function_traits::arg4_type arg4, typename boost::function_traits::arg5_type arg5, typename boost::function_traits::arg6_type arg6, typename boost::function_traits::arg7_type arg7) { if (item->sig) // Make sure this guy hasn't been disconnected item->call(arg1, arg2, arg3, arg4, arg5, arg6, arg7); } public: void operator()(typename boost::function_traits::arg1_type arg1, typename boost::function_traits::arg2_type arg2, typename boost::function_traits::arg3_type arg3, typename boost::function_traits::arg4_type arg4, typename boost::function_traits::arg5_type arg5, typename boost::function_traits::arg6_type arg6, typename boost::function_traits::arg7_type arg7) { if (this->empty()) return; boost::intrusive_ptr::slot> item; begin: try { while (this->next(item)) fireItem(item.get(), arg1, arg2, arg3, arg4, arg5, arg6, arg7); } catch (Aya::base_exception& e) { Aya::signals::signal::on_error(e); // Note: We put this handler on the outside of the for loop // as an optimization. This is why we have a goto statement. goto begin; } } }; } // namespace signals template class signal : public signals::signal_with_args::arity, Signature> { }; // Note that remote signal is *not* virtualized against signal. This only works because the Event class is templatized, and not using polymorphism. // If Event becomes polymorphics, THIS CODE WILL FAIL template class remote_signal : public signal { private: typedef signal Super; public: signal connectionSignal; remote_signal() {} template signals::connection connect(const F& function) { connectionSignal(); return Super::connect(function); } }; } // namespace Aya #ifdef AYA_SIGNALS_DEBUGGING #pragma optimize("", on) #endif