Merge remote-tracking branch 'suokko/runtime_debug_prints' into develop
Also fix and reorganize changelogdevelop
commit
a2f8742128
@ -0,0 +1,32 @@
|
|||||||
|
#include <regex>
|
||||||
|
|
||||||
|
namespace DFHack {
|
||||||
|
namespace neverCalled {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gcc/linstdc++ seems to generate code that links libstdc++ back to first
|
||||||
|
* shared object using std::regex. To allow plugins unload with std::regex in
|
||||||
|
* the code we need the std::regex functions inside libdfhack.so.
|
||||||
|
*
|
||||||
|
* If your plugin decides to use any overloads that aren't listed here it may
|
||||||
|
* stay in memory after dlclose.
|
||||||
|
*/
|
||||||
|
std::regex stdRegexPluginUnloadWorkaround()
|
||||||
|
{
|
||||||
|
std::regex fake("foo");
|
||||||
|
std::string haystack("bar is foo in the world");
|
||||||
|
std::regex fake2(std::string("bar"));
|
||||||
|
if (std::regex_match(haystack, fake))
|
||||||
|
std::swap(fake, fake2);
|
||||||
|
if (std::regex_search(haystack, fake))
|
||||||
|
std::swap(fake, fake2);
|
||||||
|
const char* haystack2 = "foo";
|
||||||
|
if (std::regex_match(haystack2, fake))
|
||||||
|
std::swap(fake, fake2);
|
||||||
|
if (std::regex_search(haystack2, fake))
|
||||||
|
std::swap(fake, fake2);
|
||||||
|
return fake;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,186 @@
|
|||||||
|
/**
|
||||||
|
Copyright © 2018 Pauli <suokkos@gmail.com>
|
||||||
|
|
||||||
|
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 <chrono>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#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<std::mutex> 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<std::mutex> 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<uint32_t> 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<std::chrono::milliseconds>(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<DebugCategory::level> test;
|
||||||
|
if (test.is_lock_free())
|
||||||
|
return;
|
||||||
|
std::cerr << __FILE__ << ':' << __LINE__
|
||||||
|
<< ": error: std::atomic<DebugCategory::level> 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
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,369 @@
|
|||||||
|
/**
|
||||||
|
Copyright © 2018 Pauli <suokkos\gmail.com>
|
||||||
|
|
||||||
|
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 <atomic>
|
||||||
|
#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 <PluginCommand> &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<uint32_t>(msgLevel);
|
||||||
|
// Compile time filtering to allow compiling out debug checks prints
|
||||||
|
// from binary.
|
||||||
|
return static_cast<uint32_t>(DBG_FILTER) <= intLevel &&
|
||||||
|
// Runtime filtering for debug messages
|
||||||
|
static_cast<uint32_t>(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<level> allowed_;
|
||||||
|
#if __cplusplus >= 201703L || __cpp_lib_atomic_is_always_lock_free >= 201603
|
||||||
|
static_assert(std::atomic<level>::is_always_lock_free,
|
||||||
|
"std::atomic<level> should be lock free. You are using a very old CPU or code needs to use std::atomic<int>");
|
||||||
|
#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<DebugCategory* category>
|
||||||
|
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__)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
Copyright © 2018 Pauli <suokkos@gmail.com>
|
||||||
|
|
||||||
|
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 <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<DebugCategory*> {
|
||||||
|
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<void (signalType, DebugCategory&)>;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* 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:
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,782 @@
|
|||||||
|
/**
|
||||||
|
Copyright © 2018 Pauli <suokkos@gmail.com>
|
||||||
|
|
||||||
|
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 <climits>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <list>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#ifdef __SSE__
|
||||||
|
#include <xmmintrin.h>
|
||||||
|
#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<ConnectedBase> 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<typename Signature, typename tag = signal_inline_tag>
|
||||||
|
class Signal;
|
||||||
|
|
||||||
|
namespace details {
|
||||||
|
|
||||||
|
template<typename Signature, typename tag>
|
||||||
|
struct SignalImpl;
|
||||||
|
|
||||||
|
template<typename Signature, typename tag>
|
||||||
|
struct selectImpl;
|
||||||
|
|
||||||
|
//! Manage callback states in thread safe manner
|
||||||
|
template<typename Signature, typename tag>
|
||||||
|
class CallbackHolderImpl;
|
||||||
|
|
||||||
|
template<typename RT, typename... Args>
|
||||||
|
struct CallbackHolderBase {
|
||||||
|
using Callback = std::function<RT(Args...)>;
|
||||||
|
|
||||||
|
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_t> 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<typename RT, typename... Args>
|
||||||
|
class CallbackHolderImpl<RT(Args...), signal_inline_tag> :
|
||||||
|
public CallbackHolderBase<RT, Args...> {
|
||||||
|
using parent_t = CallbackHolderBase<RT, Args...>;
|
||||||
|
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<typename RT, typename... Args>
|
||||||
|
class CallbackHolderImpl<RT(Args...), signal_shared_tag> :
|
||||||
|
public CallbackHolderBase<RT, Args...> {
|
||||||
|
using parent_t = CallbackHolderBase<RT, Args...>;
|
||||||
|
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<ConnectedBase>
|
||||||
|
return *holder_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
//! Reference to the connection
|
||||||
|
CallbackHolderImpl* holder_;
|
||||||
|
std::shared_ptr<ConnectedBase> 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<ConnectedBase> weak_ref_;
|
||||||
|
friend CallGuard;
|
||||||
|
public:
|
||||||
|
//! Construct the callback state for an automatically synchronized object
|
||||||
|
CallbackHolderImpl(const Callback& cb,
|
||||||
|
std::shared_ptr<ConnectedBase>& 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<typename RT, typename... Args, typename tag>
|
||||||
|
struct SignalImpl<RT(Args...), tag> : public selectImpl<RT(Args...), tag>::parent_t {
|
||||||
|
protected:
|
||||||
|
using select_t = selectImpl<RT(Args...), tag>;
|
||||||
|
using parent_t = typename select_t::parent_t;
|
||||||
|
public:
|
||||||
|
using CallbackHolder = CallbackHolderImpl<RT(Args...), tag>;
|
||||||
|
using Callback = typename CallbackHolder::Callback;
|
||||||
|
|
||||||
|
//! The container type used to store callbacks
|
||||||
|
using CallbackContainer = std::list<CallbackHolder>;
|
||||||
|
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<std::mutex> lock(access_);
|
||||||
|
auto iter = callbacks_.emplace(callbacks_.begin(), f);
|
||||||
|
return {iter, parent_t::shared_from_this()};
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection connect(std::shared_ptr<ConnectedBase> c, const Callback& f)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(access_);
|
||||||
|
auto iter = callbacks_.emplace(callbacks_.begin(), f, c);
|
||||||
|
return {iter, parent_t::shared_from_this()};
|
||||||
|
}
|
||||||
|
|
||||||
|
void disconnect(Connection& connection) {
|
||||||
|
std::lock_guard<std::mutex> lock(access_);
|
||||||
|
if (recursion_) {
|
||||||
|
deleted_ = true;
|
||||||
|
connection.iter_->erase();
|
||||||
|
} else {
|
||||||
|
callbacks_.erase(connection.iter_);
|
||||||
|
}
|
||||||
|
select_t::reset(connection.signal_);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Combiner>
|
||||||
|
void operator()(Combiner &combiner, Args&&... arg)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(access_);
|
||||||
|
struct RecursionGuard {
|
||||||
|
SignalImpl* signal_;
|
||||||
|
std::unique_lock<std::mutex>* lock_;
|
||||||
|
//! Increment access count to make sure disconnect doesn't erase
|
||||||
|
RecursionGuard(SignalImpl *signal, std::unique_lock<std::mutex>* 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<Args>(arg)...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void operator()(Args&&... arg)
|
||||||
|
{
|
||||||
|
auto combiner = [](const Callback& cb, Args&&... arg2)
|
||||||
|
{
|
||||||
|
cb(std::forward<Args>(arg2)...);
|
||||||
|
};
|
||||||
|
(*this)(combiner,std::forward<Args>(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<tag, signal_inline_tag>::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<RT(Args...), tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename RT, typename... Args>
|
||||||
|
struct selectImpl<RT(Args...), signal_inline_tag> {
|
||||||
|
using impl_t = SignalImpl<RT(Args...), signal_inline_tag>;
|
||||||
|
using interface_t = Signal<RT(Args...), signal_inline_tag>;
|
||||||
|
using type = impl_t;
|
||||||
|
using weak_ptr = impl_t*;
|
||||||
|
struct ptr_from_this {
|
||||||
|
weak_ptr shared_from_this()
|
||||||
|
{
|
||||||
|
return static_cast<weak_ptr>(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<typename RT, typename... Args>
|
||||||
|
struct selectImpl<RT(Args...), signal_shared_tag> {
|
||||||
|
using impl_t = SignalImpl<RT(Args...), signal_shared_tag>;
|
||||||
|
using interface_t = Signal<RT(Args...), signal_shared_tag>;
|
||||||
|
using type = std::shared_ptr<impl_t>;
|
||||||
|
using weak_ptr = std::weak_ptr<impl_t>;
|
||||||
|
using parent_t = std::enable_shared_from_this<impl_t>;
|
||||||
|
|
||||||
|
// Allow copies for shared version
|
||||||
|
|
||||||
|
static type make() {
|
||||||
|
return std::make_shared<SignalImpl<RT(Args...), signal_shared_tag>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<typename RT, typename... Args, typename tag>
|
||||||
|
class Signal<RT(Args...), tag> : protected details::selectImpl<RT(Args...), tag> {
|
||||||
|
public:
|
||||||
|
//! Type of callable that can be connected to the signal.
|
||||||
|
using Callback = std::function<RT(Args...)>;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
using select_t = details::selectImpl<RT(Args...), tag>;
|
||||||
|
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<ConnectedBase> c, const Callback& f)
|
||||||
|
{
|
||||||
|
static_assert(std::is_same<tag, signal_shared_tag>::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<typename Combiner>
|
||||||
|
void operator()(Combiner &combiner, Args&&... arg)
|
||||||
|
{
|
||||||
|
(*pimpl)(combiner, std::forward<Args>(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<Args>(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;
|
||||||
|
};
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue