diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 390efdcfe..416e42c41 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -28,6 +28,8 @@ include/Core.h include/ColorText.h include/DataDefs.h include/DataIdentity.h +include/Debug.h +include/DebugManager.h include/VTableInterpose.h include/LuaWrapper.h include/LuaTools.h @@ -38,6 +40,7 @@ include/MiscUtils.h include/Module.h include/Pragma.h include/MemAccess.h +include/Signal.hpp include/TileTypes.h include/Types.h include/VersionInfo.h @@ -55,6 +58,7 @@ SET(MAIN_SOURCES Core.cpp ColorText.cpp DataDefs.cpp +Debug.cpp Error.cpp VTableInterpose.cpp LuaWrapper.cpp diff --git a/library/Debug.cpp b/library/Debug.cpp new file mode 100644 index 000000000..aa0cf8cdf --- /dev/null +++ b/library/Debug.cpp @@ -0,0 +1,186 @@ +/** +Copyright © 2018 Pauli + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any +damages arising from the use of this software. + +Permission is granted to anyone to use this software for any +purpose, including commercial applications, and to alter it and +redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and + must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + */ +#define _POSIX_C_SOURCE 200809L +#include "Core.h" + +#include "Debug.h" +#include "DebugManager.h" + +#include +#include +#include + +#ifdef _MSC_VER +static tm* localtime_r(const time_t* time, tm* result) +{ + localtime_s(result, time); + return result; +} +#endif + +namespace DFHack { +DBG_DECLARE(core,debug); + +void DebugManager::registerCategory(DebugCategory& cat) +{ + DEBUG(debug) << "register DebugCategory '" << cat.category() + << "' from '" << cat.plugin() + << "' allowed " << cat.allowed() << std::endl; + std::lock_guard guard(access_mutex_); + push_back(&cat); + categorySignal(CAT_ADD, cat); +} + +void DebugManager::unregisterCategory(DebugCategory& cat) +{ + DEBUG(debug) << "unregister DebugCategory '" << cat.category() + << "' from '" << cat.plugin() + << "' allowed " << cat.allowed() << std::endl; + std::lock_guard guard(access_mutex_); + auto iter = std::find(begin(), end(), &cat); + std::swap(*iter, back()); + pop_back(); + categorySignal(CAT_REMOVE, cat); +} + +DebugRegisterBase::DebugRegisterBase(DebugCategory* cat) +{ + // Make sure Core lives at least as long any DebugCategory to + // allow debug prints until all Debugcategories has been destructed + Core::getInstance(); + DebugManager::getInstance().registerCategory(*cat); +} + +void DebugRegisterBase::unregister(DebugCategory* cat) +{ + DebugManager::getInstance().unregisterCategory(*cat); +} + +static color_value selectColor(const DebugCategory::level msgLevel) +{ + switch(msgLevel) { + case DebugCategory::LTRACE: + return COLOR_GREY; + case DebugCategory::LDEBUG: + return COLOR_LIGHTBLUE; + case DebugCategory::LINFO: + return COLOR_CYAN; + case DebugCategory::LWARNING: + return COLOR_YELLOW; + case DebugCategory::LERROR: + return COLOR_LIGHTRED; + } + return COLOR_WHITE; +} + +#if __GNUC__ +// Allow gcc to optimize tls access. It also makes sure initialized is done as +// early as possible. The early initialization helps to give threads same ids as +// gdb shows. +#define EXEC_ATTR __attribute__((tls_model("initial-exec"))) +#else +#define EXEC_ATTR +#endif + +namespace { +static std::atomic nextId{0}; +static EXEC_ATTR thread_local uint32_t thread_id{nextId.fetch_add(1)+1}; +} + +DebugCategory::ostream_proxy_prefix::ostream_proxy_prefix( + const DebugCategory& cat, + color_ostream& target, + const DebugCategory::level msgLevel) : + color_ostream_proxy(target) +{ + color(selectColor(msgLevel)); + auto now = std::chrono::system_clock::now(); + tm local{}; + //! \todo c++ 2020 will have std::chrono::to_stream(fmt, system_clock::now()) + //! but none implements it yet. + std::time_t now_c = std::chrono::system_clock::to_time_t(now); + auto ms = std::chrono::duration_cast(now.time_since_epoch()) % 1000; + // Output time in format %02H:%02M:%02S.%03ms +#if __GNUC__ < 5 + // Fallback for gcc 4 + char buffer[32]; + size_t sz = strftime(buffer, sizeof(buffer)/sizeof(buffer[0]), + "%T.", localtime_r(&now_c, &local)); + *this << (sz > 0 ? buffer : "HH:MM:SS.") +#else + *this << std::put_time(localtime_r(&now_c, &local),"%T.") +#endif + << std::setfill('0') << std::setw(3) << ms.count() + // Thread id is allocated in the thread creation order to a thread_local + // variable + << ":t" << thread_id + // Output plugin and category names to make it easier to locate where + // the message is coming. It would be easy replaces these with __FILE__ + // and __LINE__ passed from the macro if that would be preferred prefix. + << ':' << cat.plugin() << ':' << cat.category() << ": "; +} + + +DebugCategory::level DebugCategory::allowed() const noexcept +{ + return allowed_.load(std::memory_order_relaxed); +} + +void DebugCategory::allowed(DebugCategory::level value) noexcept +{ + level old = allowed_.exchange(value, std::memory_order_relaxed); + if (old == value) + return; + TRACE(debug) << "modify DebugCategory '" << category() + << "' from '" << plugin() + << "' allowed " << value << std::endl; + auto& manager = DebugManager::getInstance(); + manager.categorySignal(DebugManager::CAT_MODIFIED, *this); +} + +DebugCategory::cstring_ref DebugCategory::category() const noexcept +{ + return category_; +} + +DebugCategory::cstring_ref DebugCategory::plugin() const noexcept +{ + return plugin_; +} + +#if __cplusplus < 201703L && __cpp_lib_atomic_is_always_lock_free < 201603 +//! C++17 has std::atomic::is_always_lock_free for static_assert. Older +//! standards only provide runtime checks if an atomic type is lock free +struct failIfEnumAtomicIsNotLockFree { + failIfEnumAtomicIsNotLockFree() { + std::atomic test; + if (test.is_lock_free()) + return; + std::cerr << __FILE__ << ':' << __LINE__ + << ": error: std::atomic should be lock free. Your compiler reports the atomic requires runtime locks. Either you are using a very old CPU or we need to change code to use integer atomic type." << std::endl; + std::abort(); + } +} failIfEnumAtomicIsNotLockFree; +#endif + +} diff --git a/library/include/Debug.h b/library/include/Debug.h new file mode 100644 index 000000000..976172c8d --- /dev/null +++ b/library/include/Debug.h @@ -0,0 +1,369 @@ +/** +Copyright © 2018 Pauli + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any +damages arising from the use of this software. + +Permission is granted to anyone to use this software for any +purpose, including commercial applications, and to alter it and +redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and + must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + */ + +#pragma once + +#include "ColorText.h" + +#include +#include "Core.h" + +namespace DFHack { + +/*! \file Debug.h + * Light weight wrappers to runtime debug output filtering. Idea is to add as + * little as possible code compared to debug output without filtering. The + * effect is archived using #TRACE, #DEBUG, #INFO, #WARN and #ERR macros. They + * "return" color_ostream object or reference that can be then used normally for + * either printf or stream style debug output. + * + * Internally macros do inline filtering check which allows compiler to have a + * fast path without debug output only checking unlikely condition. But if + * output is enabled then runtime code will jump to debug printing function + * calls. The macro setup code will also print standardized leading part of + * debug string including time stamp, plugin name and debug category name. + * + * \code{.cpp} + * #include "Debug.h" + * DBG_DECLARE(myplugin,init); + * + * DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) + * { + * command_result rv = CR_OK; + * DEBUG(init, out).print("initializing\n") + * if ((rv = initWork()) != CR_OK) { + * ERR(init, out) << "initWork failed with " + * << rv << " error code" << std::endl; + * return rv; + * } + * return rv + * } + * \endcode + * + * The debug print filtering levels can be changed using debugger. Following + * gdb example would automatically setup core/init and core/render to trace + * level when SDL_init is called. + * + * \code{.unparsed} + * break SDL_init + * commands + * silent + * p DFHack::debug::core::debug_init.allowed_ = 0 + * p DFHack::debug::core::debug_render.allowed_ = 0 + * c + * end + * \endcode + * + */ + +#ifndef __has_cpp_attribute +#define __has_cpp_attribute(x) 0 +#endif + +/*! + * \defgroup debug_branch_prediction Branch prediction helper macros + * Helper macro tells compiler that debug output branch is unlikely and should + * be optimized to cold section of the function. + * \{ + */ +#if __cplusplus >= 202000L || __has_cpp_attribute(likely) +// c++20 will have standard branch prediction hint attributes +#define likely(x) (x) [[likely]] +#define unlikely(x) (x) [[unlikely]] +#elif defined(__GNUC__) +// gcc has builtin functions that give hints to the branch prediction +#define likely(x) (__builtin_expect(!!(x), 1)) +#define unlikely(x) (__builtin_expect(!!(x), 0)) +#else +#define likely(x) (x) +#define unlikely(x) (x) +#endif +//! \} + +#ifdef NDEBUG +//! Reduce minimum compiled in debug levels if NDEBUG is defined +#define DBG_FILTER DFHack::DebugCategory::LINFO +#else +//! Set default compiled in debug levels to include all prints +#define DBG_FILTER DFHack::DebugCategory::LTRACE +#endif + +/*! + * DebugCategory is used to enable and disable debug messages in runtime. + * Declaration and definition are handled by #DBG_DECLARE and #DBG_DEFINE + * macros. Runtime filtering support is handled by #TRACE, #DEBUG, #INFO, #WARN + * and #ERR macros. + */ +class DFHACK_EXPORT DebugCategory final { +public: + //! type helper to maybe make it easier to convert to std::string_view when + //! c++17 can be required. + using cstring = const char*; + using cstring_ref = const char*; + /*! + * Debug level enum for message filtering + */ + enum level { + LTRACE = 0, + LDEBUG = 1, + LINFO = 2, + LWARNING = 3, + LERROR = 4, + }; + + /*! + * \param plugin the name of plugin the category belongs to + * \param category the name of category + * \param defaultLevel optional default filtering level for the category + */ + constexpr DebugCategory(cstring_ref plugin, + cstring_ref category, + level defaultLevel = LWARNING) noexcept : + plugin_{plugin}, + category_{category}, + allowed_{defaultLevel} + {} + + + DebugCategory(const DebugCategory&) = delete; + DebugCategory(DebugCategory&&) = delete; + DebugCategory& operator=(DebugCategory) = delete; + DebugCategory& operator=(DebugCategory&&) = delete; + + /*! + * Used by debug macros to check if message should be printed. + * + * It is defined in the header to allow compiler inline it and make disabled + * state a fast path without function calls. + * + * \param msgLevel the debug message level the following print belongs to + * \return boolean with true indicating that message should be printed + */ + bool isEnabled(const level msgLevel) const noexcept { + const uint32_t intLevel = static_cast(msgLevel); + // Compile time filtering to allow compiling out debug checks prints + // from binary. + return static_cast(DBG_FILTER) <= intLevel && + // Runtime filtering for debug messages + static_cast(allowed_.load(std::memory_order_relaxed)) <= intLevel; + } + + struct DFHACK_EXPORT ostream_proxy_prefix : public color_ostream_proxy { + ostream_proxy_prefix(const DebugCategory& cat, + color_ostream& target, + DebugCategory::level level); + ~ostream_proxy_prefix() + { + flush(); + } + }; + + /*! + * Fetch a steam object proxy object for output. It also adds standard + * message components like time and plugin and category names to the line. + * + * User must make sure that the line is terminated with a line end. + * + * \param msgLevel Specifies the level which next debug message belongs + * \return color_ostream_proxy that can be used to print the message + * \sa DFHack::Core::getConsole() + */ + ostream_proxy_prefix getStream(const level msgLevel) const + { + return {*this,Core::getInstance().getConsole(),msgLevel}; + } + /*! + * Add standard message components to existing output stream object to begin + * a new message line to an shared buffered object. + * + * \param msgLevel Specifies the level which next debug message belongs + * \param target An output stream object where a debug output is printed + * \return color_ostream reference that was passed as second parameter + */ + ostream_proxy_prefix getStream(const level msgLevel, color_ostream& target) const + { + return {*this,target,msgLevel}; + } + + /*! + * \brief Allow management code to set a new filtering level + * Caller must have locked DebugManager::access_mutex_. + */ + void allowed(level value) noexcept; + //! Query current filtering level + level allowed() const noexcept; + //! Query plugin name + cstring_ref plugin() const noexcept; + //! Query category name + cstring_ref category() const noexcept; +private: + + cstring plugin_; + cstring category_; + std::atomic allowed_; +#if __cplusplus >= 201703L || __cpp_lib_atomic_is_always_lock_free >= 201603 + static_assert(std::atomic::is_always_lock_free, + "std::atomic should be lock free. You are using a very old CPU or code needs to use std::atomic"); +#endif +}; + +/** + * Handle actual registering wrong template parameter generated pointer + * calculation. + */ +class DFHACK_EXPORT DebugRegisterBase { +protected: + DebugRegisterBase(DebugCategory* category); + void unregister(DebugCategory* category); +}; + +/** + * Register DebugCategory to DebugManager + */ +template +class DebugRegister final : public DebugRegisterBase { +public: + DebugRegister() : + DebugRegisterBase{category} + {} + ~DebugRegister() { + unregister(category); + } +}; + +#define DBG_NAME(category) debug_ ## category + + +/*! + * Declares a debug category. There must be only a declaration per category. + * Declaration should be in same plugin where it is used. If same category name + * is used in core and multiple plugins they all are changed with same command + * unless user specifies explicitly plugin name. + * + * Must be used in one translation unit only. + * + * \param plugin the name of plugin where debug category is used + * \param category the name of category + * \param level the initial DebugCategory::level filtering level. + */ +#define DBG_DECLARE(plugin,category, ...) \ + namespace debug { namespace plugin { \ + DebugCategory DBG_NAME(category){#plugin,#category,__VA_ARGS__}; \ + DebugRegister<&DBG_NAME(category)> register_ ## category; \ + } } \ + using debug::plugin::DBG_NAME(category) + +/*! + * Can be used to access a shared DBG_DECLARE category. But may not be used from + * static initializer because translation unit order is undefined. + * + * Can be used in shared headers to gain access to one definition from + * DBG_DECLARE. + * \param plugin The plugin name that must match DBG_DECLARE + * \param category The category name that must matnch DBG_DECLARE + */ +#define DBG_EXTERN(plugin,category) \ + namespace debug { namespace plugin { \ + extern DFHack::DebugCategory DBG_NAME(category); \ + } } \ + using debug::plugin::DBG_NAME(category) + +#define DBG_PRINT(category,pred,level,...) \ + if pred(!DFHack::DBG_NAME(category).isEnabled(level)) \ + ; /* nop fast path when debug category is disabled */ \ + else /* else to allow macro use in if-else branches */ \ + DFHack::DBG_NAME(category).getStream(level, ## __VA_ARGS__) \ +/* end of DBG_PRINT */ + +/*! + * Open a line for trace level debug output if enabled + * + * Preferred category for inside loop debug messages or callbacks/methods called + * multiple times per second. Good example would be render or onUpdate methods. + * + * \param category the debug category + * \param optional the optional second parameter is an existing + * color_ostream_proxy object + * \return color_ostream object that can be used for stream output + */ +#define TRACE(category, ...) DBG_PRINT(category, likely, \ + DFHack::DebugCategory::LTRACE, ## __VA_ARGS__) + +/*! + * Open a line for debug level debug output if enabled + * + * Preferred place to use it would be commonly called functions that don't fall + * into trace category. + * + * \param category the debug category + * \param optional the optional second parameter is an existing + * color_ostream_proxy object + * \return color_ostream object that can be used for stream output + */ +#define DEBUG(category, ...) DBG_PRINT(category, likely, \ + DFHack::DebugCategory::LDEBUG, ## __VA_ARGS__) + +/*! + * Open a line for error level debug output if enabled + * + * Important debug messages when some rarely changed state changes. Example + * would be when a debug category filtering level changes. + * + * \param category the debug category + * \param optional the optional second parameter is an existing + * color_ostream_proxy object + * \return color_ostream object that can be used for stream output + */ +#define INFO(category, ...) DBG_PRINT(category, likely, \ + DFHack::DebugCategory::LINFO, ## __VA_ARGS__) + +/*! + * Open a line for warning level debug output if enabled + * + * Warning category is for recoverable errors. This generally signals that + * something unusual happened but there is code handling the error which should + * allow df continue running without issues. + * + * \param category the debug category + * \param optional the optional second parameter is an existing + * color_ostream_proxy object + * \return color_ostream object that can be used for stream output + */ +#define WARN(category, ...) DBG_PRINT(category, unlikely, \ + DFHack::DebugCategory::LWARNING, ## __VA_ARGS__) + +/*! + * Open a line for error level error output if enabled + * + * Errors should be printed only for cases where plugin or dfhack can't recover + * from reported error and it requires manual handling from the user. + * + * \param category the debug category + * \param optional the optional second parameter is an existing + * color_ostream_proxy object + * \return color_ostream object that can be used for stream output + */ +#define ERR(category, ...) DBG_PRINT(category, unlikely, \ + DFHack::DebugCategory::LERROR, ## __VA_ARGS__) + +} diff --git a/library/include/DebugManager.h b/library/include/DebugManager.h new file mode 100644 index 000000000..fb4c635fb --- /dev/null +++ b/library/include/DebugManager.h @@ -0,0 +1,111 @@ +/** + Copyright © 2018 Pauli + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any + damages arising from the use of this software. + + Permission is granted to anyone to use this software for any + purpose, including commercial applications, and to alter it and + redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and + must not be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. + */ + +#pragma once + +#include "Export.h" +#include "Signal.hpp" + +#include +#include + +namespace DFHack { + +/*! \file DebugManager.h + * Expose an simple interface to runtime debug output filtering. The management + * interface is separated from output interface because output is required in + * many places while management is expected to be required only in a few places. + */ + +class DebugCategory; + +/*! + * \brief Container holding all registered runtime debug categories + * Singleton DebugManager is a minor extension to std::vector that allows signal + * callbacks to be attached from ui code that manages. + * + * To avoid parallel plugin unload causing issues access to DebugManager must be + * protected by mutex. The access mutex will be taken when + * DFHack::DebugCategory::~DebugCategory performs unregister calls to + * DFHack::DebugManager. The mutex will protect from memory disappearing while + * ui code is accessing or changing the runtime state. + * + * Signal emitting happens from a locked contexts. Taking the + * DFHack::DebugManager::access_mutex_ in a signal callback will results to a + * deadlock. + * + * The interface is extremely simple but enough to implement persistent filter + * states and runtime configuration code in a plugin. + */ +class DFHACK_EXPORT DebugManager : public std::vector { +public: + friend class DebugRegisterBase; + + //! access_mutex_ protects all readers and writers to DFHack::DebugManager + std::mutex access_mutex_; + + //! Different signals that all will be routed to + //! DebugManager::categorySignal + enum signalType { + CAT_ADD, + CAT_REMOVE, + CAT_MODIFIED, + }; + + //! type to help access signal features like Connection and BlockGuard + using categorySignal_t = Signal; + + /*! + * Signal object where callbacks can be connected. Connecting to a class + * method can use a lambda wrapper to the capture object pointer and correctly + * call required method. + * + * Signal is internally serialized allowing multiple threads call it + * freely. + */ + categorySignal_t categorySignal; + + //! Get the singleton object + static DebugManager& getInstance() { + static DebugManager instance; + return instance; + } + + //! Prevent copies + DebugManager(const DebugManager&) = delete; + //! Prevent copies + DebugManager(DebugManager&&) = delete; + //! Prevent copies + DebugManager& operator=(DebugManager) = delete; + //! Prevent copies + DebugManager& operator=(DebugManager&&) = delete; +protected: + DebugManager() = default; + + //! Helper for automatic category registering and signaling + void registerCategory(DebugCategory &); + //! Helper for automatic category unregistering and signaling + void unregisterCategory(DebugCategory &); +private: +}; +} diff --git a/library/include/Signal.hpp b/library/include/Signal.hpp new file mode 100644 index 000000000..acc8f4a0a --- /dev/null +++ b/library/include/Signal.hpp @@ -0,0 +1,782 @@ +/** +Copyright © 2018 Pauli + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any +damages arising from the use of this software. + +Permission is granted to anyone to use this software for any +purpose, including commercial applications, and to alter it and +redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and + must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#ifdef __SSE__ +#include +#endif + +namespace DFHack { + +/*! + * Select inline implementation for Signal members + * This requires careful destruction order where all connection has been + * disconnected before Signal::~Signal() + */ +class signal_inline_tag; +/*! + * Select share_ptr managed implementation for Signal members. + * + * If Connection holding object may be deleted without full serialization + * between disconnect and signal emit the holding object must be managed by + * shared_ptr and derive from ConnectedBase. It will also have to pass the + * std::shared_ptr to connect. + + * It uses two way std::weak_ptr reference to guarantee destruction of either + * object doesn't happen when call is made to them. + * + * It is still possible to get a callback call after manual disconnect from + * outside destructor. But without destruction risk the disconnect race can be + * handled by slot implementation side. + */ +class signal_shared_tag; + +/** + * Used for signal_shared_tag holders that may race with destructor triggered + * disconnect and emit from Signal. + */ +class ConnectedBase { +}; + +template +class Signal; + +namespace details { + +template +struct SignalImpl; + +template +struct selectImpl; + +//! Manage callback states in thread safe manner +template +class CallbackHolderImpl; + +template +struct CallbackHolderBase { + using Callback = std::function; + + CallbackHolderBase(const Callback& cb) : + cb_{cb}, + state_{} + {} + + //! Block the connection + void block() noexcept + { + state_ += blocked; + } + + //! Unblock the connection + void unblock() noexcept + { + state_ -= blocked; + } + + //! Check if connection is deleted + bool erased() const noexcept + { + return state_ & deleted; + } + + //! Check if connection is still active (not blocked or erased) + operator bool() const noexcept + { + return !(state_ & ~inCall); + } + +protected: + //! Immutable callback object + const Callback cb_; + using state_t = unsigned; + //! Single shared state as a bitfield to simplify synchronization + //! between state changes. + std::atomic state_; + static constexpr state_t deleted = 0x1 << (sizeof(state_t)*CHAR_BIT - 1); + static constexpr state_t inCall = deleted >> (sizeof(state_t)*CHAR_BIT/2); + static constexpr state_t blocked = 0x1; + static constexpr state_t blockedMask = inCall - 1; + static constexpr state_t inCallMask = (deleted - 1) ^ blockedMask; +}; + +template +class CallbackHolderImpl : + public CallbackHolderBase { + using parent_t = CallbackHolderBase; +public: + using Callback = typename parent_t::Callback; +private: + using state_t = typename parent_t::state_t; + //! Make sure callback pointed object doesn't disappear under us + //! while we call it. + struct CallGuard { + + //! Prevent copies but allow copy elision + CallGuard(const CallGuard&); + + //! Allow implicit conversion to callback for simply syntax + const Callback& operator*() const noexcept + { + return holder_->cb_; + } + + operator bool() const noexcept + { + return *holder_; + } + + //! Mark call not to be called any more + ~CallGuard() { + holder_->state_ -= parent_t::inCall; + } + private: + //! Reference to the connection + CallbackHolderImpl* holder_; + + //! Mark call to be in process + CallGuard(CallbackHolderImpl* holder) : + holder_{holder} + { + holder_->state_ += parent_t::inCall; + } + //! Only allow construction from the CallbackHolderImpl::prepareCall + friend class CallbackHolderImpl; + }; +public: + //! Construct the callback state for a callback + CallbackHolderImpl(const Callback& cb) : + parent_t{cb} + {} + + /*! + * Data race free disconnection for the connection. It spins until + * no more callers to wait. Spinning should be problem as callbacks + * are expected to be simple and fast to execute. + * + * Must not be called from withing callback! + * + * \todo Maybe use monitor instruction to avoid busy wait and call + * std::thread::yield() if wait is longer than expected. + */ + void erase() noexcept + { + state_t oldstate; + state_t newstate; + /** Spin until no callers to this callback */ +spin: + while ((oldstate = parent_t::state_) & parent_t::inCallMask) { + // pause would be portable to all old processors but there + // isn't portable way to generate it without SSE header. +#ifdef __SSE__ + _mm_pause(); +#endif + } + do { + if (oldstate & parent_t::inCallMask) + goto spin; + newstate = oldstate | parent_t::deleted; + } while(!parent_t::state_.compare_exchange_weak(oldstate, newstate)); + } + + //! Return RAII CallGuard to protect race between callback and + //! disconnect. + CallGuard prepareCall() + { + return {this}; + } +}; + +template +class CallbackHolderImpl : + public CallbackHolderBase { + using parent_t = CallbackHolderBase; +public: + using Callback = typename parent_t::Callback; +private: + using state_t = typename parent_t::state_t; + //! Make sure callback pointed object doesn't disappear under us + //! while we call it. + struct CallGuard { + + //! Prevent copies but allow copy elision + CallGuard(const CallGuard&); + + //! Allow implicit conversion to callback for simply syntax + const Callback& operator*() const noexcept + { + return holder_->cb_; + } + + operator bool() const noexcept + { + // If this is not marked erased then weak_ref->lock succeeded or + // the slot isn't managed by shared_ptr + return *holder_; + } + + private: + //! Reference to the connection + CallbackHolderImpl* holder_; + std::shared_ptr strong_ref_; + + //! Mark call to be in process + CallGuard(CallbackHolderImpl* holder) : + holder_{holder}, + strong_ref_{holder->weak_ref_.lock()} + { + } + //! Only allow construction from the CallbackHolderImpl::prepareCall + friend class CallbackHolderImpl; + }; + + std::weak_ptr weak_ref_; + friend CallGuard; +public: + //! Construct the callback state for an automatically synchronized object + CallbackHolderImpl(const Callback& cb, + std::shared_ptr& ref) : + parent_t{cb}, + weak_ref_{ref} + {} + + //! Construct the callback state for an externally synchronized object + CallbackHolderImpl(const Callback& cb) : + parent_t{cb}, + weak_ref_{} + {} + + /*! + * erase from destructor can't happen while we are in call because + */ + void erase() noexcept + { + parent_t::state_ |= parent_t::deleted; + } + + //! Return RAII CallGuard to protect race between callback and + //! disconnect. + CallGuard prepareCall() + { + return {this}; + } +}; + +template +struct SignalImpl : public selectImpl::parent_t { +protected: + using select_t = selectImpl; + using parent_t = typename select_t::parent_t; +public: + using CallbackHolder = CallbackHolderImpl; + using Callback = typename CallbackHolder::Callback; + + //! The container type used to store callbacks + using CallbackContainer = std::list; + struct BlockGuard; + + //! Simple connection class that is required to disconnect from the + //! signal. + struct Connection { + //! Construct a default Connection object but using it will result + //! to undefined behavior unless proper connection is assigned to it + Connection() = default; + + Connection(Connection&& o) : + iter_{o.iter_}, + signal_{} + { + std::swap(signal_, o.signal_); + } + + Connection& operator=(Connection&& o) + { + disconnect(); + iter_ = o.iter_; + std::swap(signal_, o.signal_); + return *this; + } + + Connection(const Connection&) = delete; + Connection& operator=(const Connection&) = delete; + + //! Disconnect from signal + void disconnect() + { + auto s = select_t::lock(signal_); + if (!s) + return; + + s->disconnect(*this); + } + + ~Connection() + { + disconnect(); + } + private: + //! Block the connection temporary + void block() + { + auto s = select_t::lock(signal_); + if (!s) + return; + iter_->block(); + } + + //! Restore blocked connection + void unblock() + { + auto s = select_t::lock(signal_); + if (!s) + return; + iter_->unblock(); + } + + //! Construct connection object + Connection(const typename CallbackContainer::iterator &iter, + typename select_t::weak_ptr ptr) : + iter_{iter}, + signal_{ptr} + {} + + //! std::list iterator that is used to access the callback and allow + //! removal from the list. + typename CallbackContainer::iterator iter_; + //! Reference to signal object + typename select_t::weak_ptr signal_; + friend SignalImpl; + friend BlockGuard; + }; + + /*! + * BlockGuard allows temporary RAII guard managed blocking of a + * connection object. + */ + struct BlockGuard { + /*! + * Block a connection that belongs to signal + * \param connection The connection that will be temporary blocked + */ + BlockGuard(Connection& connection) : + blocked_{&connection} + { + connection.block(); + } + + /*! + * Unblock the temporary blocked connection + */ + ~BlockGuard() + { + blocked_->unblock(); + } + + //! Prevent copies but allow copy elision + BlockGuard(const BlockGuard&); + private: + Connection* blocked_; + }; + + Connection connect(const Callback& f) + { + std::lock_guard lock(access_); + auto iter = callbacks_.emplace(callbacks_.begin(), f); + return {iter, parent_t::shared_from_this()}; + } + + Connection connect(std::shared_ptr c, const Callback& f) + { + std::lock_guard lock(access_); + auto iter = callbacks_.emplace(callbacks_.begin(), f, c); + return {iter, parent_t::shared_from_this()}; + } + + void disconnect(Connection& connection) { + std::lock_guard lock(access_); + if (recursion_) { + deleted_ = true; + connection.iter_->erase(); + } else { + callbacks_.erase(connection.iter_); + } + select_t::reset(connection.signal_); + } + + template + void operator()(Combiner &combiner, Args&&... arg) + { + std::unique_lock lock(access_); + struct RecursionGuard { + SignalImpl* signal_; + std::unique_lock* lock_; + //! Increment access count to make sure disconnect doesn't erase + RecursionGuard(SignalImpl *signal, std::unique_lock* lock) : + signal_{signal}, + lock_{lock} + { + ++signal_->recursion_; + } + + /*! + * Clean up deleted functions in data race free and exception + * safe manner. + */ + ~RecursionGuard() + { + lock_->lock(); + if (--signal_->recursion_ == 0 && signal_->deleted_) { + for (auto iter = signal_->callbacks_.begin(); iter != signal_->callbacks_.end();) { + if (iter->erased()) + iter = signal_->callbacks_.erase(iter); + else + ++iter; + } + signal_->deleted_ = false; + } + } + + } guard{this, &lock}; + // Call begin in locked context to allow data race free iteration + // even if there is parallel inserts to the begin after unlocking. + auto iter = callbacks_.begin(); + lock.unlock(); + for (; iter != callbacks_.end(); ++iter) { + // Quickly skip blocked calls without memory writes + if (!*iter) + continue; + // Protect connection from deletion while we are about to call + // it. + auto cb = iter->prepareCall(); + if (cb) + combiner(*cb, std::forward(arg)...); + } + } + + void operator()(Args&&... arg) + { + auto combiner = [](const Callback& cb, Args&&... arg2) + { + cb(std::forward(arg2)...); + }; + (*this)(combiner,std::forward(arg)...); + } + + ~SignalImpl() { + // Check that callbacks are empty. If this triggers then signal may + // have to be extended to allow automatic disconnection of active + // connections in the destructor. + if (std::is_same::value) + assert(callbacks_.empty() && "It is very likely that this signal should use signal_shared_tag"); + } + + //! Simplify access to pimpl when it is inline + SignalImpl* operator->() { + return this; + } + SignalImpl& operator*() { + return *this; + } + + SignalImpl() = default; +private: + SignalImpl(const SignalImpl&) : + SignalImpl{} + {} + std::mutex access_; + CallbackContainer callbacks_; + int recursion_; + bool deleted_; + friend Signal; +}; + +template +struct selectImpl { + using impl_t = SignalImpl; + using interface_t = Signal; + using type = impl_t; + using weak_ptr = impl_t*; + struct ptr_from_this { + weak_ptr shared_from_this() + { + return static_cast(this); + } + }; + using parent_t = ptr_from_this; + + selectImpl() = default; + + // Disallow copies for inline version. + selectImpl(const selectImpl&) = delete; + selectImpl(selectImpl&&) = delete; + selectImpl& operator=(const selectImpl&) = delete; + selectImpl& operator=(selectImpl&&) = delete; + + + static type make() { + return {}; + } + + static void reset(weak_ptr& ptr) { + ptr = nullptr; + } + + static weak_ptr lock(weak_ptr& ptr) { + return ptr; + } + + static weak_ptr get(interface_t& signal) { + return &signal.pimpl; + } +}; + +template +struct selectImpl { + using impl_t = SignalImpl; + using interface_t = Signal; + using type = std::shared_ptr; + using weak_ptr = std::weak_ptr; + using parent_t = std::enable_shared_from_this; + + // Allow copies for shared version + + static type make() { + return std::make_shared>(); + } + + static void reset(weak_ptr& ptr) { + ptr.reset(); + } + + static type lock(weak_ptr& ptr) { + return ptr.lock(); + } + + static weak_ptr get(interface_t& signal) { + return signal.pimpl; + } +}; +} + +/*! + * As I couldn't figure out which signal library would be a good. Too bad all + * signal libraries seem to be either heavy with unnecessary features or written + * before C++11/14 have become useable targets. That seems to indicate everyone + * is now building signal system with standard components. + * + * Implementation and interface is build around std::function holding delegates + * to a function pointer or a functor. One can put there example lambda function + * that captures this pointer from connect side. The lambda function then calls + * the slot method of object correctly. + * + * It is fairly simple to change the signal signature to directly call methods + * but internally that std::function becomes more complex. The pointer to + * member function is problematic because multiple inheritance requires + * adjustments to this. The lambda capture approach should be easy to use while + * letting compiler optimize method call in the callee side. + * + * DFHack::Signal::Connection is an connection handle. The handle can be used to + * disconnect and block a callback. Connection destructor will automatically + * disconnect from the signal. + * + * DFHack::Signal::BlockGuard is an automatic blocked callback guard object. It + * prevents any signals from calling the slot as long the BlockGuard object is + * alive. Internally it replaces the callback with an empty callback and stores + * the real callback in a member variable. Destructor then puts back the real + * callback. This allows easily recursive BlockGuard work correctly because only + * the first BlockGuard has the real callback. + * + * signal_inline_tag requires careful destruction order where all connection are + * disconnected before signal destruction. The implementation is specifically + * targeting places like static and singleton variables and widget hierarchies. + * It provides data race free connect, disconnect and emit operations. + * + * signal_shared_tag allows a bit more freedom when destroying the Signal. It + * adds data race safety between Connection, BlockGuard and destructor. If + * multiple callers need access to Signal with potential of destruction of + * original owner then callers can use Signal copy constructor to take a strong + * reference managed by shared_ptr or weak_ptr with Signal::weak_from_this(). + * weak_from_this returns an object that forwards call directly to + * implementation when the shared_ptr is created using Signal::lock + * + * \param RT return type is derived from a single signature template argument + * \param Args Variable argument type list that is derived from a signature + * template argument. + * \param tag The tag type which selects between shared_ptr managed pimpl and + * inline member variables. + */ +template +class Signal : protected details::selectImpl { +public: + //! Type of callable that can be connected to the signal. + using Callback = std::function; + +protected: + using select_t = details::selectImpl; + using CallbackContainer = typename select_t::impl_t::CallbackContainer; +public: + using weak_ptr = typename select_t::weak_ptr; + + /*! + * Simple connection class that is required to disconnect from the + * signal. + * \sa SignalImpl::Connection + */ + using Connection = typename select_t::impl_t::Connection; + /*! + * BlockGuard allows temporary RAII guard managed blocking of a + * connection object. + * \sa SignalImpl::BlockGuard + */ + using BlockGuard = typename select_t::impl_t::BlockGuard; + + /*! + * Connect a callback function to the signal + * + * Safe to call from any context as long as SignalImpl destructor can't be + * called simultaneously from other thread. + * + * \param f callable that will connected to the signal + * \return connection handle that can be used to disconnect it + */ + Connection connect(const Callback& f) + { + return pimpl->connect(f); + } + + /*! + * Thread safe connect variant connection and Connected object destruction + * can't race with emit from different threads. + * + * Safe to call from any context as long as SignalImpl destructor can't be + * called simultaneously from other thread. + */ + Connection connect(std::shared_ptr c, const Callback& f) + { + static_assert(std::is_same::value, + "Race free destruction is only possible with signal_shared_tag"); + return pimpl->connect(c, f); + } + + /*! + * Disconnection a callback from slots + * + * signal_inline_tag: + * This may not be called if the callback has been called in same + * thread. If callback should trigger destruction an object then + * deletion must use deferred. This rule prevents issues if other thread + * are trying to call the callback when disconnecting. + * + * signal_shared_tag: + * disconnect can be freely called from anywhere as long as caller holds a + * strong reference to the Signal. Strong reference can be obtained by using + * Connection::disconnect, Signal copy constructor to have a copy of signal + * or weak_ptr from weak_from_this() passed to Signal::lock(). + * + * \param connection the object returned from DFHack::Signal::connect + */ + void disconnect(Connection& connection) + { + pimpl->disconnect(connection); + } + + /*! + * Call all connected callbacks using passed arguments. + * + * signal_inline_tag: + * Must not call operator() from callbacks. + * Must not disconnect called callback from inside callback. Solution often + * is to set just atomic state variables in callback and do actual + * processing including deletion in update handler or logic vmethod. + * + * signal_shared_tag: + * Safe to call from any context as long as SignalImpl destructor can't be + * called simultaneously from other thread. + * Safe to disconnect any connection from callbacks. + * + * \param combiner that calls callbacks and processes return values + * \param arg arguments list defined by template parameter signature. + */ + template + void operator()(Combiner &combiner, Args&&... arg) + { + (*pimpl)(combiner, std::forward(arg)...); + } + + /*! + * Call all connected callbacks using passed arguments. + * + * signal_inline_tag: + * Must not call operator() from callbacks. + * Must not disconnect called callback from inside callback. Solution often + * is to set just atomic state variables in callback and do actual + * processing including deletion in update handler or logic vmethod. + * + * signal_shared_tag: + * Safe to call from any context as long as SignalImpl destructor can't be + * called simultaneously from other thread. + * Safe to disconnect any connection from callbacks. + * + * \param arg arguments list defined by template parameter signature. + */ + void operator()(Args&&... arg) + { + (*pimpl)(std::forward(arg)...); + } + + /*! + * Helper to lock the weak_ptr + */ + static typename select_t::type lock(weak_ptr& ptr) + { + return select_t::lock(ptr); + } + + /*! + * Helper to create a weak reference to pimpl which can be used to access + * pimpl directly. If the tag is signal_shared_tag then it provides race + * free access to Signal when using Signal::lock and checking returned + * shared_ptr. + */ + weak_ptr weak_from_this() noexcept + { + return select_t::get(*this); + } + + Signal() : + pimpl{select_t::make()} + {} +private: + typename select_t::type pimpl; + friend select_t; +}; +}