Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exception stack support and stacktrace improvements #24

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 69 additions & 16 deletions cmake/backtrace_deps.cmake
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
# (C) Copyright 2021 UCAR.
# (C) Copyright 2021-2024 UCAR.
#
# This software is licensed under the terms of the Apache Licence Version 2.0
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.

# This CMake file tests which stack trace provider libraries are available on
# a particular system. This is needed to set appropriate flags for
# boost stacktrace (used in src/oops/util/signal_trap.cc).
# Thankfully, this eventually may be superseded by C++23's std::stacktrace feature.

# When debugging, unsetting variables triggers re-detection. Ex:
#unset(STACKTRACE_AVAILABLE_none CACHE)
#unset(STACKTRACE_AVAILABLE_libbacktrace CACHE)
#unset(STACKTRACE_AVAILABLE_addr2line CACHE)

find_path(backtrace_header_dir backtrace.h DOC "Path to the backtrace headers")
find_library(backtrace_lib backtrace DOC "Path to the backtrace library")
if ( backtrace_lib )
cmake_path(GET backtrace_lib PARENT_PATH backtrace_lib_dir)
cmake_path(GET backtrace_lib EXTENSION LAST_ONLY backtrace_ext)
set(backtrace_is_static FALSE)
if (backtrace_ext STREQUAL "a")
set(backtrace_is_static TRUE)
endif()
endif()

include(CheckCXXSourceCompiles)

Expand All @@ -31,33 +48,59 @@ string(CONFIGURE [[

# Different configs

list( APPEND OOPS_STACKTRACE_none_LIBS "")
list( APPEND OOPS_STACKTRACE_none_DEFS -DBOOST_STACKTRACE_USE_NOOP)
list( APPEND OOPS_STACKTRACE_LIBS_none "")
list( APPEND OOPS_STACKTRACE_DEFS_none -DBOOST_STACKTRACE_USE_NOOP)

list( APPEND OOPS_STACKTRACE_LIBS_default -ldl)
list( APPEND OOPS_STACKTRACE_DEFS_default "")

list( APPEND OOPS_STACKTRACE_default_LIBS -ldl)
list( APPEND OOPS_STACKTRACE_default_DEFS "")
if( backtrace_lib )
list( APPEND OOPS_STACKTRACE_LIBS_libbacktrace -ldl -L${backtrace_lib_dir} -lbacktrace)
list( APPEND OOPS_STACKTRACE_DEFS_libbacktrace -DBOOST_STACKTRACE_USE_BACKTRACE)
if ( backtrace_header_dir )
list( APPEND OOPS_STACKTRACE_DEFS_libbacktrace -DBOOST_STACKTRACE_BACKTRACE_INCLUDE_FILE=<${backtrace_header_dir}/backtrace.h>)
endif()
endif()

list( APPEND OOPS_STACKTRACE_libbacktrace_LIBS -ldl -lbacktrace)
list( APPEND OOPS_STACKTRACE_libbacktrace_DEFS -DBOOST_STACKTRACE_USE_BACKTRACE)
list( APPEND OOPS_STACKTRACE_LIBS_addr2line -ldl -lbacktrace)
list( APPEND OOPS_STACKTRACE_DEFS_addr2line -DBOOST_STACKTRACE_USE_ADDR2LINE)

list( APPEND OOPS_STACKTRACE_addr2line_LIBS -ldl -lbacktrace)
list( APPEND OOPS_STACKTRACE_addr2line_DEFS -DBOOST_STACKTRACE_USE_ADDR2LINE)
find_program(addr2line_PATH addr2line)
if(addr2line_PATH)
message( STATUS "Found addr2line at ${addr2line_PATH}." )
list(APPEND OOPS_STACKTRACE_addr2line_DEFS -DBOOST_STACKTRACE_ADDR2LINE_LOCATION=${addr2line_PATH})
list(APPEND OOPS_STACKTRACE_DEFS_addr2line -DBOOST_STACKTRACE_ADDR2LINE_LOCATION=${addr2line_PATH})
endif()

list( APPEND OOPS_STACKTRACE_POTENTIAL_PROVIDERS addr2line default none )
if( backtrace_lib )
#list( PREPEND ...) arrived in CMake 3.15. oops depends on CMake 3.12 and above.
list( INSERT OOPS_STACKTRACE_POTENTIAL_PROVIDERS 0 libbacktrace )
endif()

# Generate the "gnus" and "nognus" configuration variants.
# These reflect variations of the boost definitions that may be required to compile.
foreach ( provider IN LISTS OOPS_STACKTRACE_POTENTIAL_PROVIDERS )
set( OOPS_STACKTRACE_LIBS_${provider}_nognus ${OOPS_STACKTRACE_LIBS_${provider}})
set( OOPS_STACKTRACE_DEFS_${provider}_nognus ${OOPS_STACKTRACE_DEFS_${provider}})
list( APPEND OOPS_STACKTRACE_DEFS_${provider}_nognus -DBOOST_STACKTRACE_GNU_SOURCE_NOT_REQUIRED)

set( OOPS_STACKTRACE_LIBS_${provider}_gnus ${OOPS_STACKTRACE_LIBS_${provider}})
set( OOPS_STACKTRACE_DEFS_${provider}_gnus ${OOPS_STACKTRACE_DEFS_${provider}})
list( APPEND OOPS_STACKTRACE_DEFS_${provider}_gnus -D_GNU_SOURCE)

list( APPEND OOPS_STACKTRACE_POTENTIAL_PROVIDERS_2 ${provider} ${provider}_nognus ${provider}_gnus)
endforeach()

# Test each configuration here.
foreach ( provider IN ITEMS libbacktrace addr2line default none )
list(APPEND CMAKE_REQUIRED_LIBRARIES ${OOPS_STACKTRACE_${provider}_LIBS})
list(APPEND CMAKE_REQUIRED_DEFINITIONS ${OOPS_STACKTRACE_${provider}_DEFS})
check_cxx_source_compiles("${stacktracecode}" OOPS_STACKTRACE_${provider}_AVAILABLE)
foreach ( provider IN LISTS OOPS_STACKTRACE_POTENTIAL_PROVIDERS_2 )
list(APPEND CMAKE_REQUIRED_LIBRARIES ${OOPS_STACKTRACE_LIBS_${provider}})
list(APPEND CMAKE_REQUIRED_DEFINITIONS ${OOPS_STACKTRACE_DEFS_${provider}})
check_cxx_source_compiles("${stacktracecode}" OOPS_STACKTRACE_AVAILABLE_${provider})
set(CMAKE_REQUIRED_LIBRARIES ${saved_libraries})
set(CMAKE_REQUIRED_DEFINITIONS ${saved_defs})
if ( OOPS_STACKTRACE_${provider}_AVAILABLE )
if ( OOPS_STACKTRACE_AVAILABLE_${provider} )
list( APPEND OOPS_STACKTRACE_AVAILABLE_PROVIDERS ${provider} )
break() # Just find the first available provider.
endif()
endforeach()

Expand All @@ -69,8 +112,18 @@ if (NOT OOPS_STACKTRACE_AVAILABLE_PROVIDERS)
message(FATAL_ERROR "Cannot find a stacktrace provider.")
endif()

message( STATUS "Boost stacktrace supports these providers: ${OOPS_STACKTRACE_AVAILABLE_PROVIDERS}.")
list(GET OOPS_STACKTRACE_AVAILABLE_PROVIDERS 0 OOPS_STACKTRACE_PROVIDER)
message( STATUS "Using this provider for stacktraces: ${OOPS_STACKTRACE_PROVIDER}.")

# Small bit of extra logic to ensure that libbacktrace always is linked wherever necessary.
# See src/CMakeLists.txt for usage.
if (OOPS_STACKTRACE_PROVIDER MATCHES "backtrace")
if (backtrace_is_static)
add_library(backtrace STATIC IMPORTED)
else()
add_library(backtrace SHARED IMPORTED)
endif()
set_property(TARGET backtrace PROPERTY
IMPORTED_LOCATION "${backtrace_lib}")
endif()

16 changes: 14 additions & 2 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,8 @@ oops/util/RandomField.h
oops/util/Range.h
oops/util/ScalarOrMap.h
oops/util/Serializable.h
oops/util/Stacktrace.h
oops/util/Stacktrace.cc
oops/util/signal_trap.h
oops/util/signal_trap.cc
oops/util/sqr.h
Expand Down Expand Up @@ -626,8 +628,13 @@ endif()
# Stack traces on floating point exceptions
include( backtrace_deps )

target_link_libraries( ${PROJECT_NAME} PUBLIC ${OOPS_STACKTRACE_${OOPS_STACKTRACE_PROVIDER}_LIBS} )
target_compile_definitions( ${PROJECT_NAME} PRIVATE ${OOPS_STACKTRACE_${OOPS_STACKTRACE_PROVIDER}_DEFS} )
# By using target_link_libraries on a CMake target, CMake knows to add this to the RPATH.
# We effectively link to link libbacktrace twice in this case, but that's okay.
if (OOPS_STACKTRACE_PROVIDER MATCHES "backtrace")
target_link_libraries( ${PROJECT_NAME} PUBLIC backtrace)
endif()
target_link_libraries( ${PROJECT_NAME} PUBLIC ${OOPS_STACKTRACE_LIBS_${OOPS_STACKTRACE_PROVIDER}} )
target_compile_definitions( ${PROJECT_NAME} PRIVATE ${OOPS_STACKTRACE_DEFS_${OOPS_STACKTRACE_PROVIDER}} )

#Configure include directory layout for build-tree to match install-tree
set(OOPS_BUILD_DIR_INCLUDE_PATH ${CMAKE_BINARY_DIR}/${PROJECT_NAME}/include)
Expand Down Expand Up @@ -794,6 +801,11 @@ ecbuild_add_test( TARGET test_util_signal_trap_fpe_valid_op
SOURCES test/util/signal_trap_fpe_valid_op.cc
LIBS oops)

# This test asks for a stacktrace. It should never fail.
ecbuild_add_test( TARGET test_util_stacktrace
SOURCES test/util/Stacktrace.cc
LIBS oops)

ecbuild_add_test( TARGET test_util_random
SOURCES test/base/Random.cc
ARGS "test/testinput/random.yaml"
Expand Down
3 changes: 3 additions & 0 deletions src/oops/runs/Run.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "oops/util/Logger.h"
#include "oops/util/ObjectCountHelper.h"
#include "oops/util/printRunStats.h"
#include "oops/util/Stacktrace.h"
#include "oops/util/TimerHelper.h"
#include "oops/util/workflow.h"

Expand Down Expand Up @@ -196,11 +197,13 @@ int Run::execute(const Application & app, const eckit::mpi::Comm & comm) {
Log::error() << e.what() << " caught in " << Here() << std::endl;
Log::error() << "Exception: " << app << " terminating..." << std::endl;
eckit::Exception::exceptionStack(eckit::Log::error(), true);
util::unwind_exception_stack(e, eckit::Log::error());
}
catch(const std::exception & e) {
status = 1;
Log::error() << "Exception: " << e.what() << std::endl;
Log::error() << "Exception: " << app << " terminating..." << std::endl;
util::unwind_exception_stack(e, eckit::Log::error());
}
catch(...) {
status = 1;
Expand Down
43 changes: 43 additions & 0 deletions src/oops/util/Stacktrace.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* (C) Copyright 2024 The Tomorrow Companies, Inc.
*
* This software is licensed under the terms of the Apache Licence Version 2.0
* which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
*/

#ifndef BOOST_STACKTRACE_USE_NOOP
# include <boost/stacktrace.hpp>
#endif

#include <iostream>
#include <sstream>
#include <string>

#include "oops/util/Stacktrace.h"

namespace util {

std::string stacktrace_current() {
#ifndef BOOST_STACKTRACE_USE_NOOP
std::ostringstream s;
s << boost::stacktrace::stacktrace() << std::endl;
std::string sout = s.str();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be const.

return sout;
#else
return "Stacktrace not available";
#endif
}

void unwind_exception_stack(const std::exception& e, std::ostream& out, int level) {
out << "Exception: level: " << level << "\n" << e.what() << std::endl;
try {
std::rethrow_if_nested(e);
} catch (const std::exception& f) {
unwind_exception_stack(f, out, level + 1);
} catch (...) {
out << "exception: level: " << level
<< "\n\tException at this level is not derived from std::exception." << std::endl;
}
}

} // end namespace util
39 changes: 39 additions & 0 deletions src/oops/util/Stacktrace.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* (C) Copyright 2024 The Tomorrow Companies, Inc.
*
* This software is licensed under the terms of the Apache Licence Version 2.0
* which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
*/

#ifndef OOPS_UTIL_STACKTRACE_H_
#define OOPS_UTIL_STACKTRACE_H_

#include <exception>
#include <iostream>
#include <memory>
#include <string>

namespace util {

/// @brief This is a basic backport of C++23's stacktrace feature.
/// @details This uses Boost instead of eckit's Backtrace code because
/// the eckit implementation has a lot of platform-specific code
/// and libstdc++-related code for symbol demangling that is more
/// generalized by Boost. Boost's stacktrace code also provides
/// line numbers whenever possible.
/// @returns A string with the current stacktrace.
std::string stacktrace_current();

/// @brief Print details of the exception stack.
/// @details This C++11 feature was added after eckit's Exception classes
/// were written. It is more general, as all of the JEDI-related exceptions
/// ultimately derive from std::exception, including the eckit-related ones.
/// @param e is the exception.
/// @param out is the output stream.
/// @param level denotes current depth in the stack. Used because
/// unwind_exception_stack is a recursive function.
void unwind_exception_stack(const std::exception& e, std::ostream& out = std::cerr, int level = 0);

} // namespace util

#endif // OOPS_UTIL_STACKTRACE_H_
8 changes: 2 additions & 6 deletions src/oops/util/signal_trap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,8 @@
// exceptions that cause an abort, and then use the CMake WILL_FAIL test property to catch
// the abort.

// The behavior of boost::stacktrace is controlled by preprocessor macros. See
// https://www.boost.org/doc/libs/1_76_0/doc/html/stacktrace/configuration_and_build.html
// for details. The correct libs and macros are set using CMake (in backtrace_deps.cmake).
#include <boost/stacktrace.hpp>

#include "oops/util/abor1_cpp.h"
#include "oops/util/Stacktrace.h"

namespace util {

Expand Down Expand Up @@ -250,7 +246,7 @@ void trap_sigfpe(const int abortFlag) {

//------------------------------------------------------------------------
void sigfpe_abort() {
std::cerr << std::endl << boost::stacktrace::stacktrace() << std::endl;
std::cerr << std::endl << stacktrace_current() << std::endl;
ABORT("Trapped a floating point exception");
}

Expand Down
19 changes: 19 additions & 0 deletions src/test/util/Stacktrace.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* (C) Copyright 2021-2023 UCAR
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2024

*
* This software is licensed under the terms of the Apache Licence Version 2.0
* which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
*/
/*
* This test deliberately produces a stacktrace.
*/

#include "oops/util/Stacktrace.h"

#include <iostream>

int main(int argc, char **argv) {
std::cout << "Stacktrace:\n" << util::stacktrace_current() << std::endl;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a suggestion but you could use oops::Log::info() here instead of std::cout.

return 0;
}