Commit 33489ea5 authored by David Frank's avatar David Frank
Browse files

Refactor Error class

Backtraces have sadly been broken from some time, and it was a major
pain to find the source (still searching). This commit removes all the
self rolled code with the backward-cpp library
(https://github.com/bombela/backward-cpp)

This library features a much more comprehensive set of options, pretty
printing and deals with dependencies for us.
parent 0fa4cc78
Pipeline #747140 failed with stages
in 15 minutes and 17 seconds
......@@ -41,6 +41,7 @@ set(ELSA_SANITIZER
set(ELSA_CUDA_ARCH_TYPE "auto" CACHE STRING "Set CUDA architectures")
option(SYSTEM_EIGEN "Build elsa using the system eigen installation" OFF)
option(SYSTEM_BACKWARD "Build elsa using the system backward-cpp installation" OFF)
option(SYSTEM_SPDLOG "Build elsa using the system spdlog installation" OFF)
# ------------ general setup -----------
......@@ -78,10 +79,12 @@ set(INSTALL_CONFIG_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/elsa)
# only add the dependencies if elsa is stand-alone
if(ELSA_MASTER_PROJECT)
if(SYSTEM_EIGEN)
message(STATUS "Using system-wide Eigen library...")
find_package(Eigen3 REQUIRED)
else()
# setup custom Eigen Library
# once there's a proper release,
# the system-eigen has to be a drop-in replacement for the git-version
......@@ -153,6 +156,13 @@ if(ELSA_MASTER_PROJECT)
# SPDLOG_INSTALL will manage install and includedir setup
# to $CMAKE_INSTALL_INCLUDEDIR/spdlog
endif()
if(SYSTEM_BACKWARD)
message(STATUS "Using system-wide backward-cpp library...")
find_package(Eigen3 REQUIRED)
else()
CPMAddPackage(NAME backward GIT_REPOSITORY https://github.com/bombela/backward-cpp VERSION 1.6)
endif()
# setup Dnnl library
find_package(Dnnl)
......
......@@ -22,7 +22,7 @@ macro(ADD_ELSA_MODULE elsa_module_name module_headers module_sources)
set(ELSA_MODULE_EXPORT_TARGET elsa_${elsa_module_name}Targets)
# build the module library
add_library(${ELSA_MODULE_TARGET_NAME} ${module_headers} ${module_sources})
add_library(${ELSA_MODULE_TARGET_NAME} ${module_headers} ${module_sources} ${BACKWARD_ENABLE})
add_library(elsa::${ELSA_MODULE_NAME} ALIAS ${ELSA_MODULE_TARGET_NAME})
# Add public dependencies
......@@ -31,6 +31,9 @@ macro(ADD_ELSA_MODULE elsa_module_name module_headers module_sources)
target_link_libraries(${ELSA_MODULE_TARGET_NAME} PRIVATE "${ADD_MODULE_PRIVATE_DPES}")
# require C++17
target_compile_features(${ELSA_MODULE_TARGET_NAME} PUBLIC cxx_std_17)
# Add Backward dependecy
add_backward(${ELSA_MODULE_TARGET_NAME})
# set -fPIC and -fvisibility=hidden
set_target_properties(${ELSA_MODULE_TARGET_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON
CXX_VISIBILITY_PRESET hidden)
......@@ -43,7 +46,7 @@ macro(ADD_ELSA_MODULE elsa_module_name module_headers module_sources)
if(ELSA_MASTER_PROJECT)
set_target_warnings(${ELSA_MODULE_TARGET_NAME} LEVEL ${ELSA_WARNINGS_LEVEL})
endif()
# build the tests (if enabled)
if(ELSA_TESTING AND NOT ADD_MODULE_NO_TESTS)
add_subdirectory(tests)
......@@ -74,7 +77,7 @@ macro(ELSA_DOCTEST name)
# set the name for the test once
set(_testname test_${name})
# create the test executable
add_executable(${_testname} EXCLUDE_FROM_ALL ${_testname}.cpp test_docmain.cpp)
add_executable(${_testname} EXCLUDE_FROM_ALL ${_testname}.cpp test_docmain.cpp ${BACKWARD_ENABLE})
# add catch and the corresponding elsa library
target_link_libraries(
${_testname} PRIVATE doctest::doctest ${ELSA_MODULE_TARGET_NAME} elsa::test_routines
......@@ -86,6 +89,12 @@ macro(ELSA_DOCTEST name)
target_include_directories(${_testname} PRIVATE ${CMAKE_SOURCE_DIR}/elsa/test_routines/)
# Improve compile times for tests
target_compile_definitions(${_testname} PRIVATE DOCTEST_CONFIG_SUPER_FAST_ASSERTS)
set_target_properties(${ELSA_MODULE_TARGET_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON
CXX_VISIBILITY_PRESET hidden)
# Add Backward dependecy
add_backward(${_testname})
# Set output directory to bin/tests/<module>
set_target_properties(
${_testname} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/tests/${ELSA_MODULE_TARGET_NAME}"
......
#include "Backtrace.h"
#include <cxxabi.h>
#include <dlfcn.h>
#include <execinfo.h>
#include <sstream>
namespace elsa::detail
{
std::string symbolDemangle(const char* symbol)
{
int status;
char* buf = abi::__cxa_demangle(symbol, nullptr, nullptr, &status);
if (status != 0) {
return symbol;
} else {
std::string result{buf};
::free(buf);
return result;
}
}
std::string addrToString(const void* addr)
{
std::ostringstream out;
out << "[" << addr << "]";
return out.str();
}
std::string symbolName(const void* addr, bool require_exact_addr, bool no_pure_addrs)
{
Dl_info addr_info;
if (::dladdr(addr, &addr_info) == 0) {
// dladdr has... failed.
return no_pure_addrs ? "" : addrToString(addr);
} else {
size_t symbol_offset =
reinterpret_cast<size_t>(addr) - reinterpret_cast<size_t>(addr_info.dli_saddr);
if (addr_info.dli_sname == nullptr or (symbol_offset != 0 and require_exact_addr)) {
return no_pure_addrs ? "" : addrToString(addr);
}
if (symbol_offset == 0) {
// this is our symbol name.
return symbolDemangle(addr_info.dli_sname);
} else {
std::ostringstream out;
out << symbolDemangle(addr_info.dli_sname) << "+0x" << std::hex << symbol_offset
<< std::dec;
return out.str();
}
}
}
} // namespace elsa::detail
namespace elsa
{
void Backtrace::analyze()
{
std::vector<void*> buffer{32};
// increase buffer size until it's enough
while (true) {
int buff_size = static_cast<int>(buffer.size());
auto elements = static_cast<size_t>(::backtrace(buffer.data(), buff_size));
if (elements < buffer.size()) {
buffer.resize(elements);
break;
}
buffer.resize(buffer.size() * 2);
}
for (void* element : buffer) {
this->stack_addrs.push_back(element);
}
}
void Backtrace::getSymbols(const std::function<void(const backtrace_symbol*)>& cb,
bool reversed) const
{
backtrace_symbol symbol;
if (reversed) {
for (size_t idx = this->stack_addrs.size(); idx-- > 0;) {
void* pc = this->stack_addrs[idx];
symbol.functionname = detail::symbolName(pc, false, true);
symbol.pc = pc;
cb(&symbol);
}
} else {
for (void* pc : this->stack_addrs) {
symbol.functionname = detail::symbolName(pc, false, true);
symbol.pc = pc;
cb(&symbol);
}
}
}
void Backtrace::trimToCurrentStackFrame()
{
Backtrace current;
current.analyze();
while (not current.stack_addrs.empty() and not this->stack_addrs.empty()) {
if (this->stack_addrs.back() != current.stack_addrs.back()) {
break;
}
this->stack_addrs.pop_back();
current.stack_addrs.pop_back();
}
}
} // namespace elsa
#pragma once
#include <string>
#include <functional>
#include <vector>
namespace elsa::detail
{
/**
* Demangles a symbol name.
*
* On failure, the mangled symbol name is returned.
*/
std::string symbolDemangle(const char* symbol);
/**
* Convert a pointer address to string.
*/
std::string addrToString(const void* addr);
/**
* Return the demangled symbol name for a given code address.
*/
std::string symbolName(const void* addr, bool require_exact_addr = true,
bool no_pure_addrs = false);
} // namespace elsa::detail
namespace elsa
{
/**
* A single symbol, as determined from a program counter, and returned by
* Backtrace::getSymbols().
*/
struct backtrace_symbol {
std::string functionname; // empty if unknown
void* pc; // nullptr if unknown
};
/**
* Provide execution backtrace information through getSymbols().
*/
class Backtrace
{
public:
Backtrace() = default;
virtual ~Backtrace() = default;
/**
* Analyzes the current stack, and stores the program counter values in
* this->stack_addrs.
*/
void analyze();
/**
* Provide the names for all stack frames via the callback.
*
* The most recent call is returned last (alike Python).
*
* @param cb
* is called for every symbol in the backtrace,
* starting with the top-most frame.
* @param reversed
* if true, the most recent call is given last.
*/
void getSymbols(const std::function<void(const backtrace_symbol*)>& cb,
bool reversed = true) const;
/**
* Removes all the lower frames that are also present
* in the current stack.
*
* Designed to be used in catch clauses,
* to simulate stack trace collection
* from throw to catch, instead of from throw to the process entry point.
*/
void trimToCurrentStackFrame();
protected:
/**
* All program counters of this backtrace.
*/
std::vector<void*> stack_addrs;
};
} // namespace elsa
# list all the headers of the module
set(MODULE_HEADERS
elsaDefines.h
Backtrace.h
Cloneable.h
Utilities/Badge.hpp
Utilities/DataContainerFormatter.hpp
......@@ -32,7 +31,6 @@ set(MODULE_HEADERS
# list all the code files of the module
set(MODULE_SOURCES
Backtrace.cpp
Descriptors/DataDescriptor.cpp
Descriptors/VolumeDescriptor.cpp
Descriptors/PlanarDetectorDescriptor.cpp
......
......@@ -2,145 +2,49 @@
#include <sstream>
#include "Backtrace.h"
#include "backward.hpp"
namespace elsa
{
constexpr const char* runtime_error_message = "polymorphic elsa error, catch by reference!";
Error::Error(std::string msg, bool generate_backtrace, bool store_cause)
: std::runtime_error{runtime_error_message}, backtrace{nullptr}, msg{std::move(msg)}
{
if (generate_backtrace) {
this->backtrace = std::make_shared<Backtrace>();
this->backtrace->analyze();
}
if (store_cause) {
this->storeCause();
}
}
std::string Error::str() const { return this->msg; }
const char* Error::what() const noexcept
{
this->what_cache = this->str();
return this->what_cache.c_str();
}
void Error::storeCause()
{
if (not std::current_exception()) {
return;
}
try {
throw;
} catch (Error& cause) {
cause.trimBacktrace();
this->cause = std::current_exception();
} catch (...) {
this->cause = std::current_exception();
}
}
void Error::trimBacktrace()
{
if (this->backtrace) {
this->backtrace->trimToCurrentStackFrame();
}
}
void Error::rethrowCause() const
namespace detail
{
if (this->cause) {
std::rethrow_exception(this->cause);
}
}
std::string Error::typeName() const { return detail::symbolDemangle(typeid(*this).name()); }
static std::string get_trace(std::string&& msg)
{
namespace bw = backward;
Backtrace* Error::getBacktrace() const { return this->backtrace.get(); }
std::ostringstream stream;
const std::string& Error::getMsg() const { return this->msg; }
stream << msg << "\n\n";
std::ostream& operator<<(std::ostream& os, const Error& e)
{
// output the exception cause
bool had_a_cause = true;
try {
e.rethrowCause();
had_a_cause = false;
} catch (Error& cause) {
os << cause << std::endl;
} catch (std::exception& cause) {
os << detail::symbolDemangle(typeid(cause).name()) << ": " << cause.what() << std::endl;
}
bw::StackTrace stackTrace;
bw::TraceResolver resolver;
stackTrace.load_here();
resolver.load_stacktrace(stackTrace);
if (had_a_cause) {
os << std::endl
<< "The above exception was the direct cause "
"of the following exception:"
<< std::endl
<< std::endl;
}
bw::Printer printer;
printer.color_mode = bw::ColorMode::always; // I always want pretty colors :^)
printer.print(stackTrace, stream);
// output the exception backtrace
auto* bt = e.getBacktrace();
if (bt != nullptr) {
os << *bt;
} else {
os << "origin:" << std::endl;
return stream.str();
}
os << e.typeName() << ":" << std::endl;
os << e.str();
return os;
}
BaseError::BaseError(std::string msg) : std::runtime_error(get_trace(std::move(msg))) {}
/**
* Prints a backtrace_symbol object.
*/
std::ostream& operator<<(std::ostream& os, const backtrace_symbol& bt_sym)
{
// imitate the looks of a Python traceback.
os << " -> ";
if (bt_sym.functionname.empty()) {
os << '?';
} else {
os << bt_sym.functionname;
}
if (bt_sym.pc != nullptr) {
os << " " << detail::addrToString(bt_sym.pc);
std::ostream& operator<<(std::ostream& os, const BaseError& err)
{
os << err.what();
return os;
}
} // namespace detail
return os;
}
/**
* Prints an entire Backtrace object.
*/
std::ostream& operator<<(std::ostream& os, const Backtrace& bt)
{
// imitate the looks of a Python traceback.
os << "Traceback (most recent call last):" << std::endl;
bt.getSymbols([&os](const backtrace_symbol* symbol) { os << *symbol << std::endl; }, true);
return os;
}
Error::Error(const std::string& msg) : BaseError(msg) {}
InternalError::InternalError(const std::string& msg) : Error{msg} {}
InternalError::InternalError(const std::string& msg) : BaseError(msg) {}
LogicError::LogicError(const std::string& msg) : Error{msg} {}
LogicError::LogicError(const std::string& msg) : BaseError(msg) {}
InvalidArgumentError::InvalidArgumentError(const std::string& msg) : Error{msg} {}
InvalidArgumentError::InvalidArgumentError(const std::string& msg) : BaseError(msg) {}
BadCastError::BadCastError(const std::string& msg) : Error{msg} {}
BadCastError::BadCastError(const std::string& msg) : BaseError(msg) {}
} // namespace elsa
......@@ -9,121 +9,32 @@
namespace elsa
{
// fwd-decls for Backtrace.h
struct backtrace_symbol;
class Backtrace;
/**
* Base exception for every error that occurs in elsa.
*
* Usage:
* try {
* throw elsa::Error{"some info"};
* }
* catch (elsa::Error &err) {
* std::cout << "\x1b[31;1merror:\x1b[m\n"
* << err << std::endl;
* }
*
*/
class Error : public std::runtime_error
namespace detail
{
public:
Error(std::string msg, bool generate_backtrace = true, bool store_cause = true);
~Error() override = default;
/**
* String representation of this exception, as
* specialized by a child exception.
*/
virtual std::string str() const;
/**
* Returns the message's content.
*/
const char* what() const noexcept override;
/**
* Stores a pointer to the currently-handled exception in this->cause.
* @brief Base exception/error class relying on backward-cpp to retrieve the stacktrace on
* error
*/
void storeCause();
class BaseError : public std::runtime_error
{
public:
BaseError(std::string msg);
};
/**
* Calls this->backtrace->trimToCurrentStackFrame(),
* if this->backtrace is not nullptr.
*
* Designed to be used in catch clauses, to strip away all those
* unneeded symbols from program init upwards.
*/
void trimBacktrace();
/**
* Re-throws the exception cause, if the exception has one.
* Otherwise, does nothing.
*
* Use this when handling the exception, to handle the cause.
*/
void rethrowCause() const;
/**
* Get the type name of of the exception.
* Use it to display the name of a child exception.
*/
virtual std::string typeName() const;
std::ostream& operator<<(std::ostream& os, const BaseError& err);
/**
* Return the backtrace where the exception was thrown.
* nullptr if no backtrace was collected.
*/
Backtrace* getBacktrace() const;
} // namespace detail
/**
* Directly return the message stored in the exception.
*/
const std::string& getMsg() const;
protected:
/**
* The (optional) backtrace where the exception came from.
*/
std::shared_ptr<Backtrace> backtrace;
/**
* The error message text.
*/
std::string msg;
/**
* Cached error message text for returning C string via what().
*/
mutable std::string what_cache;
/**
* Re-throw this with rethrowCause().
*/
std::exception_ptr cause;
class Error : public detail::BaseError
{
public:
Error(const std::string& msg);
};
/**
* Output stream concat for Errors.
*/
std::ostream& operator<<(std::ostream& os, const Error& e);
/**
* Output stream concat for backtrace symbols.
*/
std::ostream& operator<<(std::ostream& os, const backtrace_symbol& bt_sym);
/**
* Output stream concat for backtraces.
*/
std::ostream& operator<<(std::ostream& os, const Backtrace& bt);
/**
* Internal Error, thrown when some interal sanity check failed.
*/
class InternalError : public Error
class InternalError : public detail::BaseError
{
public:
InternalError(const std::string& msg);
......@@ -132,7 +43,7 @@ namespace elsa
/**
* Faulty logic due to violation of expected preconditions.