Add cpptrace::from_current (#155)

This commit is contained in:
Jeremy Rifkin 2024-08-18 10:11:47 -06:00 committed by GitHub
parent d5eed55dcd
commit 26e009c688
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 612 additions and 9 deletions

View File

@ -214,6 +214,7 @@ target_sources(
src/binary/safe_dl.cpp src/binary/safe_dl.cpp
src/cpptrace.cpp src/cpptrace.cpp
src/ctrace.cpp src/ctrace.cpp
src/from_current.cpp
src/demangle/demangle_with_cxxabi.cpp src/demangle/demangle_with_cxxabi.cpp
src/demangle/demangle_with_nothing.cpp src/demangle/demangle_with_nothing.cpp
src/demangle/demangle_with_winapi.cpp src/demangle/demangle_with_winapi.cpp

109
README.md
View File

@ -28,7 +28,10 @@ Cpptrace also has a C API, docs [here](docs/c-api.md).
- [Raw Traces](#raw-traces) - [Raw Traces](#raw-traces)
- [Utilities](#utilities) - [Utilities](#utilities)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Traced Exceptions](#traced-exceptions) - [Traces From All Exceptions](#traces-from-all-exceptions)
- [How it works](#how-it-works)
- [Performance](#performance)
- [Traced Exception Objects](#traced-exception-objects)
- [Wrapping std::exceptions](#wrapping-stdexceptions) - [Wrapping std::exceptions](#wrapping-stdexceptions)
- [Exception handling with cpptrace](#exception-handling-with-cpptrace) - [Exception handling with cpptrace](#exception-handling-with-cpptrace)
- [Signal-Safe Tracing](#signal-safe-tracing) - [Signal-Safe Tracing](#signal-safe-tracing)
@ -80,7 +83,30 @@ const auto raw_trace = cpptrace::generate_raw_trace();
raw_trace.resolve().print(); raw_trace.resolve().print();
``` ```
Cpptrace also provides exception types that store stack traces: Cpptrace provides a way to produce stack traces on arbitrary exceptions. More information on this system
[below](#traces-from-all-exceptions).
```cpp
#include <cpptrace/from_current.hpp>
void foo() {
throw std::runtime_error("foo failed");
}
int main() {
CPPTRACE_TRY {
foo();
} CPPTRACE_CATCH(const std::exception& e) {
std::cerr<<"Exception: "<<e.what()<<std::endl;
cpptrace::from_current_exception().print();
}
}
```
![from_current](res/from_current.png)
There are a few extraneous frames at the top of the stack corresponding to internals of exception handling in the
standard library. These are a small price to pay for stack traces on all exceptions.
Cpptrace also provides a handful of traced exception objects that store stack traces when thrown. This is useful when
the exceptions might not be caught by `CPPTRACE_CATCH`:
```cpp ```cpp
#include <cpptrace/cpptrace.hpp> #include <cpptrace/cpptrace.hpp>
@ -89,7 +115,7 @@ void trace() {
} }
``` ```
![Inlining](res/exception.png) ![Exception](res/exception.png)
Additional notable features: Additional notable features:
@ -342,11 +368,70 @@ namespace cpptrace {
} }
``` ```
### Traced Exceptions ### Traces From All Exceptions
Cpptrace provides an interface for a traced exceptions, `cpptrace::exception`, as well as a set of exception classes Cpptrace provides `CPPTRACE_TRY` and `CPPTRACE_CATCH` macros that allow a stack trace to be collected from the current
that that generate stack traces when thrown. These exceptions generate relatively lightweight raw traces and resolve thrown exception object, with no overhead in the non-throwing path:
symbols and line numbers lazily if and when requested.
```cpp
CPPTRACE_TRY {
foo();
} CPPTRACE_CATCH(const std::exception& e) {
std::cout<<"Exception: "<<e.what()<<std::endl;
std::cout<<cpptrace::from_current_exception().to_string(true)<<std::endl;
}
```
Any declarator `catch` accepts works with `CPPTRACE_CATCH`, including `...`.
![from_current](res/from_current.png)
There are a few extraneous frames at the top of the stack corresponding to internals of exception handling in the
standard library. These are a small price to pay for stack traces on all exceptions.
API functions:
- `cpptrace::raw_trace_from_current_exception`: Returns `const raw_trace&` from the current exception.
- `cpptrace::from_current_exception`: Returns a resolved `const stacktrace&` from the current exception. Invalidates
references to traces returned by `cpptrace::raw_trace_from_current_exception`.
#### How it works
C++ does not provide any language support for collecting stack traces when exceptions are thrown, however, exception
handling under both the Itanium ABI and by SEH (used to implement C++ exceptions on windows) involves unwinding the
stack twice, the first unwind searches for an appropriate `catch` handler, the second actually unwinds the stack and
calls destructors. Since the stack remains intact during the search phase it's possible to collect a stack trace with
zero overhead when the `catch` is considered for matching the exception.
N.b.: Cpptrace uses the same mechanism proposed for use in [P2490R3][P2490R3].
#### Performance
`CPPTRACE_CATCH` internally generates lightweight raw traces when considered in the search phase. These are quite fast
to generate and are only resolved when `cpptrace::from_current_exception` is called.
Currently `CPPTRACE_CATCH` always generates a raw trace when considered as a candidate. That means that if there is a
nesting of handlers, either directly in code or as a result of the current call stack, the current stack may be traced
mutliple times until the appropriate handler is found.
This should not matter for the vast majority applications given that performance very rarely is critical in throwing
paths, how exception handling is usually used, and the shallowness of most call stacks. However, it's an important
consideration to be aware of.
To put the scale of this performance consideration into perspective: In my benchmarking I have found generation of raw
traces to take on the order of `75ns` per frame. Thus, even if there were 100 non-matching handlers before a matching
handler in a 100-deep call stack the total time would stil be on the order of less than one millisecond.
It's possible to avoid this by adding some bookkeeping to the `CPPTRACE_TRY` block. With the tradeoff between
zero-overhead try-catch in the happy path and a little extra overhead in the unhappy throwing path I decided to keep
try-catch zero-overhead. Should this be a concern to anyone, I'm happy to facilitate both solutions.
### Traced Exception Objects
Cpptrace provides a handful of traced exception classes which automatically collect stack traces when thrown. These
are useful when throwing exceptions that may not be caught by `CPPTRACE_CATCH`.
The base traced exception class is `cpptrace::exception` and cpptrace provides a handful of helper classes for working
with traced exceptions. These exceptions generate relatively lightweight raw traces and resolve symbols and line numbers
lazily if and when requested.
These are provided both as a useful utility and as a reference implementation for traced exceptions. These are provided both as a useful utility and as a reference implementation for traced exceptions.
@ -429,6 +514,10 @@ namespace cpptrace {
## Wrapping std::exceptions ## Wrapping std::exceptions
> [!NOTE]
> This section is largely obsolete now that cpptrace provides a better mechanism for collecting
> [traces from exceptions](#traces-from-exceptions)
Cpptrace exceptions can provide great information for user-controlled exceptions. For non-cpptrace::exceptions that may Cpptrace exceptions can provide great information for user-controlled exceptions. For non-cpptrace::exceptions that may
originate outside of code you control, e.g. the standard library, cpptrace provides some wrapper utilities that can originate outside of code you control, e.g. the standard library, cpptrace provides some wrapper utilities that can
rethrow these exceptions nested in traced cpptrace exceptions. The trace won't be perfect, the trace will start where rethrow these exceptions nested in traced cpptrace exceptions. The trace won't be perfect, the trace will start where
@ -446,6 +535,10 @@ std::cout<<CPPTRACE_WRAP(foo.at(12))<<std::endl;
## Exception handling with cpptrace ## Exception handling with cpptrace
> [!NOTE]
> This section pertains to cpptrace traced exception objects and not the mechanism for collecting
> [traces from arbitrary exceptions](#traces-from-exceptions)
Working with cpptrace exceptions in your code: Working with cpptrace exceptions in your code:
```cpp ```cpp
try { try {
@ -1000,3 +1093,5 @@ This library is under the MIT license.
Cpptrace uses libdwarf on linux, macos, and mingw/cygwin unless configured to use something else. If this library is Cpptrace uses libdwarf on linux, macos, and mingw/cygwin unless configured to use something else. If this library is
statically linked with libdwarf then the library's binary will itself be LGPL. statically linked with libdwarf then the library's binary will itself be LGPL.
[P2490R3]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2490r3.html

View File

@ -298,6 +298,7 @@ namespace cpptrace {
lazy_trace_holder& operator=(lazy_trace_holder&& other) noexcept; lazy_trace_holder& operator=(lazy_trace_holder&& other) noexcept;
~lazy_trace_holder(); ~lazy_trace_holder();
// access // access
const raw_trace& get_raw_trace() const;
stacktrace& get_resolved_trace(); stacktrace& get_resolved_trace();
const stacktrace& get_resolved_trace() const; const stacktrace& get_resolved_trace() const;
private: private:

View File

@ -0,0 +1,61 @@
#ifndef CPPTRACE_FROM_CURRENT_HPP
#define CPPTRACE_FROM_CURRENT_HPP
#include <cpptrace/cpptrace.hpp>
namespace cpptrace {
CPPTRACE_EXPORT const raw_trace& raw_trace_from_current_exception();
CPPTRACE_EXPORT const stacktrace& from_current_exception();
namespace detail {
#ifdef _MSC_VER
CPPTRACE_EXPORT CPPTRACE_FORCE_NO_INLINE int exception_filter();
#else
class CPPTRACE_EXPORT unwind_interceptor {
public:
virtual ~unwind_interceptor();
};
CPPTRACE_EXPORT void do_prepare_unwind_interceptor();
#ifndef CPPTRACE_DONT_PREPARE_UNWIND_INTERCEPTOR_ON
__attribute__((constructor)) inline void prepare_unwind_interceptor() {
// __attribute__((constructor)) inline functions can be called for every source file they're #included in
// there is still only one copy of the inline function in the final executable, though
// LTO can make the redundant constructs fire only once
// do_prepare_unwind_interceptor prevents against multiple preparations however it makes sense to guard
// against it here too as a fast path, not that this should matter for performance
static bool did_prepare = false;
if(!did_prepare) {
do_prepare_unwind_interceptor();
did_prepare = true;
}
}
#endif
#endif
}
}
#ifdef _MSC_VER
// this awful double-IILE is due to C2713 "You can't use structured exception handling (__try/__except) and C++
// exception handling (try/catch) in the same function."
#define CPPTRACE_TRY \
try { \
[&]() { \
__try { \
[&]() {
#define CPPTRACE_CATCH(param) \
}(); \
} __except(::cpptrace::detail::exception_filter()) {} \
}(); \
} catch(param)
#else
#define CPPTRACE_TRY \
try { \
try {
#define CPPTRACE_CATCH(param) \
} catch(::cpptrace::detail::unwind_interceptor&) {} \
} catch(param)
#endif
#endif

BIN
res/from_current.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -592,6 +592,14 @@ namespace cpptrace {
clear(); clear();
} }
// access // access
const raw_trace& lazy_trace_holder::get_raw_trace() const {
if(resolved) {
throw std::logic_error(
"cpptrace::detail::lazy_trace_holder::get_resolved_trace called on resolved holder"
);
}
return trace;
}
stacktrace& lazy_trace_holder::get_resolved_trace() { stacktrace& lazy_trace_holder::get_resolved_trace() {
if(!resolved) { if(!resolved) {
raw_trace old_trace = std::move(trace); raw_trace old_trace = std::move(trace);
@ -605,7 +613,7 @@ namespace cpptrace {
// TODO: Append to message somehow? // TODO: Append to message somehow?
std::fprintf( std::fprintf(
stderr, stderr,
"Exception occurred while resolving trace in cpptrace::exception object:\n%s\n", "Exception occurred while resolving trace in cpptrace::detail::lazy_trace_holder:\n%s\n",
e.what() e.what()
); );
} }
@ -616,7 +624,7 @@ namespace cpptrace {
const stacktrace& lazy_trace_holder::get_resolved_trace() const { const stacktrace& lazy_trace_holder::get_resolved_trace() const {
if(!resolved) { if(!resolved) {
throw std::logic_error( throw std::logic_error(
"cpptrace::detaillazy_trace_holder::get_resolved_trace called on unresolved const object" "cpptrace::detail::lazy_trace_holder::get_resolved_trace called on unresolved const holder"
); );
} }
return resolved_trace; return resolved_trace;

293
src/from_current.cpp Normal file
View File

@ -0,0 +1,293 @@
#include <cpptrace/cpptrace.hpp>
#define CPPTRACE_DONT_PREPARE_UNWIND_INTERCEPTOR_ON
#include <cpptrace/from_current.hpp>
#include <system_error>
#include <typeinfo>
#include "utils/common.hpp"
#include "utils/microfmt.hpp"
#include "utils/utils.hpp"
#ifndef _MSC_VER
#include <array>
#include <string.h>
#if IS_WINDOWS
#include <windows.h>
#else
#include <sys/mman.h>
#include <unistd.h>
#if IS_APPLE
#include <mach/mach.h>
#include <mach/mach_vm.h>
#else
#include <fstream>
#include <iomanip>
#endif
#endif
#endif
namespace cpptrace {
namespace detail {
thread_local lazy_trace_holder current_exception_trace;
#ifndef _MSC_VER
CPPTRACE_FORCE_NO_INLINE
bool intercept_unwind(const std::type_info*, const std::type_info*, void**, unsigned) {
current_exception_trace = lazy_trace_holder(cpptrace::generate_raw_trace(1));
return false;
}
unwind_interceptor::~unwind_interceptor() = default;
#if defined(__GLIBCXX__) || defined(__GLIBCPP__)
constexpr size_t vtable_size = 11;
#elif defined(_LIBCPP_VERSION)
constexpr size_t vtable_size = 10;
#else
#warning "Cpptrace from_current: Unrecognized C++ standard library, from_current() won't be supported"
constexpr size_t vtable_size = 0;
#endif
#if IS_WINDOWS
int get_page_size() {
SYSTEM_INFO info;
GetSystemInfo(&info);
return info.dwPageSize;
}
constexpr auto memory_readonly = PAGE_READONLY;
constexpr auto memory_readwrite = PAGE_READWRITE;
int mprotect_page_and_return_old_protections(void* page, int page_size, int protections) {
DWORD old_protections;
if(!VirtualProtect(page, page_size, protections, &old_protections)) {
throw std::runtime_error(
microfmt::format(
"VirtualProtect call failed: {}",
std::system_error(GetLastError(), std::system_category()).what()
)
);
}
return old_protections;
}
void mprotect_page(void* page, int page_size, int protections) {
mprotect_page_and_return_old_protections(page, page_size, protections);
}
void* allocate_page(int page_size) {
auto page = VirtualAlloc(nullptr, page_size, MEM_COMMIT | MEM_RESERVE, memory_readwrite);
if(!page) {
throw std::runtime_error(
microfmt::format(
"VirtualAlloc call failed: {}",
std::system_error(GetLastError(), std::system_category()).what()
)
);
}
return page;
}
#else
int get_page_size() {
return getpagesize();
}
constexpr auto memory_readonly = PROT_READ;
constexpr auto memory_readwrite = PROT_READ | PROT_WRITE;
#if IS_APPLE
int get_page_protections(void* page) {
// https://stackoverflow.com/a/12627784/15675011
mach_vm_size_t vmsize;
mach_vm_address_t address = (mach_vm_address_t)page;
vm_region_basic_info_data_t info;
mach_msg_type_number_t info_count =
sizeof(size_t) == 8 ? VM_REGION_BASIC_INFO_COUNT_64 : VM_REGION_BASIC_INFO_COUNT;
memory_object_name_t object;
kern_return_t status = mach_vm_region(
mach_task_self(),
&address,
&vmsize,
VM_REGION_BASIC_INFO,
(vm_region_info_t)&info,
&info_count,
&object
);
if(status == KERN_INVALID_ADDRESS) {
throw std::runtime_error("vm_region failed with KERN_INVALID_ADDRESS");
}
int perms = 0;
if(info.protection & VM_PROT_READ) {
perms |= PROT_READ;
}
if(info.protection & VM_PROT_WRITE) {
perms |= PROT_WRITE;
}
if(info.protection & VM_PROT_EXECUTE) {
perms |= PROT_EXEC;
}
return perms;
}
#else
int get_page_protections(void* page) {
auto page_addr = reinterpret_cast<uintptr_t>(page);
std::ifstream stream("/proc/self/maps");
stream>>std::hex;
while(!stream.eof()) {
uintptr_t start;
uintptr_t stop;
stream>>start;
stream.ignore(1);
stream>>stop;
if(stream.eof()) {
break;
}
if(stream.fail()) {
throw std::runtime_error("Failure reading /proc/self/maps");
}
if(page_addr >= start && page_addr < stop) {
stream.ignore(1);
char r, w, x;
stream>>r>>w>>x;
if(stream.fail() || stream.eof()) {
throw std::runtime_error("Failure reading /proc/self/maps");
}
int perms = 0;
if(r == 'r') {
perms |= PROT_READ;
}
if(w == 'w') {
perms |= PROT_WRITE;
}
if(x == 'x') {
perms |= PROT_EXEC;
}
// std::cerr<<"--parsed: "<<std::hex<<start<<" "<<stop<<" "<<r<<w<<x<<std::endl;
return perms;
}
stream.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
throw std::runtime_error("Failed to find mapping with page in /proc/self/maps");
}
#endif
void mprotect_page(void* page, int page_size, int protections) {
if(mprotect(page, page_size, protections) != 0) {
throw std::runtime_error(microfmt::format("mprotect call failed: {}", strerror(errno)));
}
}
int mprotect_page_and_return_old_protections(void* page, int page_size, int protections) {
auto old_protections = get_page_protections(page);
mprotect_page(page, page_size, protections);
return old_protections;
}
void* allocate_page(int page_size) {
auto page = mmap(nullptr, page_size, memory_readwrite, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if(page == MAP_FAILED) {
throw std::runtime_error(microfmt::format("mmap call failed: {}", strerror(errno)));
}
return page;
}
#endif
// allocated below, cleaned up by OS after exit
void* new_vtable_page = nullptr;
void perform_typeinfo_surgery(const std::type_info& info) {
if(vtable_size == 0) { // set to zero if we don't know what standard library we're working with
return;
}
void* type_info_pointer = const_cast<void*>(static_cast<const void*>(&info));
void* type_info_vtable_pointer = *static_cast<void**>(type_info_pointer);
// the type info vtable pointer points to two pointers inside the vtable, adjust it back
type_info_vtable_pointer = static_cast<void*>(static_cast<void**>(type_info_vtable_pointer) - 2);
// for libstdc++ the class type info vtable looks like
// 0x7ffff7f89d18 <_ZTVN10__cxxabiv117__class_type_infoE>: 0x0000000000000000 0x00007ffff7f89d00
// [offset ][typeinfo pointer ]
// 0x7ffff7f89d28 <_ZTVN10__cxxabiv117__class_type_infoE+16>: 0x00007ffff7dd65a0 0x00007ffff7dd65c0
// [base destructor ][deleting dtor ]
// 0x7ffff7f89d38 <_ZTVN10__cxxabiv117__class_type_infoE+32>: 0x00007ffff7dd8f10 0x00007ffff7dd8f10
// [__is_pointer_p ][__is_function_p ]
// 0x7ffff7f89d48 <_ZTVN10__cxxabiv117__class_type_infoE+48>: 0x00007ffff7dd6640 0x00007ffff7dd6500
// [__do_catch ][__do_upcast ]
// 0x7ffff7f89d58 <_ZTVN10__cxxabiv117__class_type_infoE+64>: 0x00007ffff7dd65e0 0x00007ffff7dd66d0
// [__do_upcast ][__do_dyncast ]
// 0x7ffff7f89d68 <_ZTVN10__cxxabiv117__class_type_infoE+80>: 0x00007ffff7dd6580 0x00007ffff7f8abe8
// [__do_find_public_src][other ]
// In libc++ the layout is
// [offset ][typeinfo pointer ]
// [base destructor ][deleting dtor ]
// [noop1 ][noop2 ]
// [can_catch ][search_above_dst ]
// [search_below_dst ][has_unambiguous_public_base]
// Relevant documentation/implementation:
// https://itanium-cxx-abi.github.io/cxx-abi/abi.html
// libstdc++
// https://github.com/gcc-mirror/gcc/blob/b13e34699c7d27e561fcfe1b66ced1e50e69976f/libstdc%252B%252B-v3/libsupc%252B%252B/typeinfo
// https://github.com/gcc-mirror/gcc/blob/b13e34699c7d27e561fcfe1b66ced1e50e69976f/libstdc%252B%252B-v3/libsupc%252B%252B/class_type_info.cc
// libc++
// https://github.com/llvm/llvm-project/blob/648f4d0658ab00cf1e95330c8811aaea9481a274/libcxx/include/typeinfo
// https://github.com/llvm/llvm-project/blob/648f4d0658ab00cf1e95330c8811aaea9481a274/libcxxabi/src/private_typeinfo.h
// shouldn't be anything other than 4096 but out of an abundance of caution
auto page_size = get_page_size();
if(page_size <= 0 && (page_size & (page_size - 1)) != 0) {
throw std::runtime_error(
microfmt::format("getpagesize() is not a power of 2 greater than zero (was {})", page_size)
);
}
// allocate a page for the new vtable so it can be made read-only later
new_vtable_page = allocate_page(page_size);
// make our own copy of the vtable
memcpy(new_vtable_page, type_info_vtable_pointer, vtable_size * sizeof(void*));
// ninja in the custom __do_catch interceptor
auto new_vtable = static_cast<void**>(new_vtable_page);
new_vtable[6] = reinterpret_cast<void*>(intercept_unwind);
// make the page read-only
mprotect_page(new_vtable_page, page_size, memory_readonly);
// make the vtable pointer for unwind_interceptor's type_info point to the new vtable
auto type_info_addr = reinterpret_cast<uintptr_t>(type_info_pointer);
auto page_addr = type_info_addr & ~(page_size - 1);
// make sure the memory we're going to set is within the page
if(type_info_addr - page_addr + sizeof(void*) > static_cast<unsigned>(page_size)) {
throw std::runtime_error("pointer crosses page boundaries");
}
auto old_protections = mprotect_page_and_return_old_protections(
reinterpret_cast<void*>(page_addr),
page_size,
memory_readwrite
);
*static_cast<void**>(type_info_pointer) = static_cast<void*>(new_vtable + 2);
mprotect_page(reinterpret_cast<void*>(page_addr), page_size, old_protections);
}
void do_prepare_unwind_interceptor() {
static bool did_prepare = false;
if(!did_prepare) {
try {
perform_typeinfo_surgery(typeid(cpptrace::detail::unwind_interceptor));
} catch(std::exception& e) {
std::fprintf(
stderr,
"Cpptrace: Exception occurred while preparing from_current support: %s\n",
e.what()
);
} catch(...) {
std::fprintf(stderr, "Cpptrace: Unknown exception occurred while preparing from_current support\n");
}
did_prepare = true;
}
}
#else
CPPTRACE_FORCE_NO_INLINE int exception_filter() {
current_exception_trace = lazy_trace_holder(cpptrace::generate_raw_trace(1));
return EXCEPTION_CONTINUE_SEARCH;
}
#endif
}
const raw_trace& raw_trace_from_current_exception() {
return detail::current_exception_trace.get_raw_trace();
}
const stacktrace& from_current_exception() {
return detail::current_exception_trace.get_resolved_trace();
}
}

View File

@ -83,6 +83,7 @@ if(NOT CPPTRACE_SKIP_UNIT)
unit/raw_trace.cpp unit/raw_trace.cpp
unit/object_trace.cpp unit/object_trace.cpp
unit/stacktrace.cpp unit/stacktrace.cpp
unit/from_current.cpp
) )
target_compile_features(unittest PRIVATE cxx_std_20) target_compile_features(unittest PRIVATE cxx_std_20)
target_link_libraries(unittest PRIVATE ${target_name} GTest::gtest_main GTest::gmock_main) target_link_libraries(unittest PRIVATE ${target_name} GTest::gtest_main GTest::gmock_main)

143
test/unit/from_current.cpp Normal file
View File

@ -0,0 +1,143 @@
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string_view>
#include <string>
#include <gtest/gtest.h>
#include <gtest/gtest-matchers.h>
#include <gmock/gmock.h>
#include <gmock/gmock-matchers.h>
#include <cpptrace/cpptrace.hpp>
#include <cpptrace/from_current.hpp>
using namespace std::literals;
// NOTE: returning something and then return stacktrace_from_current_3(line_numbers) * 2; later helps prevent the call from
// being optimized to a jmp
CPPTRACE_FORCE_NO_INLINE int stacktrace_from_current_3(std::vector<int>& line_numbers) {
line_numbers.insert(line_numbers.begin(), __LINE__ + 1);
throw std::runtime_error("foobar");
}
CPPTRACE_FORCE_NO_INLINE int stacktrace_from_current_2(std::vector<int>& line_numbers) {
line_numbers.insert(line_numbers.begin(), __LINE__ + 1);
return stacktrace_from_current_3(line_numbers) * 2;
}
CPPTRACE_FORCE_NO_INLINE int stacktrace_from_current_1(std::vector<int>& line_numbers) {
line_numbers.insert(line_numbers.begin(), __LINE__ + 1);
return stacktrace_from_current_2(line_numbers) * 2;
}
TEST(FromCurrent, Basic) {
std::vector<int> line_numbers;
CPPTRACE_TRY {
line_numbers.insert(line_numbers.begin(), __LINE__ + 1);
stacktrace_from_current_1(line_numbers);
} CPPTRACE_CATCH(const std::runtime_error& e) {
EXPECT_EQ(e.what(), "foobar"sv);
const auto& trace = cpptrace::from_current_exception();
ASSERT_GE(trace.frames.size(), 4);
auto it = std::find_if(
trace.frames.begin(),
trace.frames.end(),
[](const cpptrace::stacktrace_frame& frame) {
return frame.filename.find("from_current.cpp") != std::string::npos
&& frame.symbol.find("lambda") == std::string::npos; // due to msvc
}
);
ASSERT_NE(it, trace.frames.end());
size_t i = static_cast<size_t>(it - trace.frames.begin());
int j = 0;
ASSERT_LT(i, trace.frames.size());
ASSERT_LT(j, line_numbers.size());
EXPECT_THAT(trace.frames[i].filename, testing::EndsWith("from_current.cpp"));
EXPECT_EQ(trace.frames[i].line.value(), line_numbers[j]);
EXPECT_THAT(trace.frames[i].symbol, testing::HasSubstr("stacktrace_from_current_3"));
i++;
j++;
ASSERT_LT(i, trace.frames.size());
ASSERT_LT(j, line_numbers.size());
EXPECT_THAT(trace.frames[i].filename, testing::EndsWith("from_current.cpp"));
EXPECT_EQ(trace.frames[i].line.value(), line_numbers[j]);
EXPECT_THAT(trace.frames[i].symbol, testing::HasSubstr("stacktrace_from_current_2"));
i++;
j++;
ASSERT_LT(i, trace.frames.size());
ASSERT_LT(j, line_numbers.size());
EXPECT_THAT(trace.frames[i].filename, testing::EndsWith("from_current.cpp"));
EXPECT_EQ(trace.frames[i].line.value(), line_numbers[j]);
EXPECT_THAT(trace.frames[i].symbol, testing::HasSubstr("stacktrace_from_current_1"));
i++;
j++;
ASSERT_LT(i, trace.frames.size());
ASSERT_LT(j, line_numbers.size());
EXPECT_THAT(trace.frames[i].filename, testing::EndsWith("from_current.cpp"));
EXPECT_EQ(trace.frames[i].line.value(), line_numbers[j]);
EXPECT_THAT(trace.frames[i].symbol, testing::HasSubstr("FromCurrent_Basic_Test::TestBody"));
}
}
TEST(FromCurrent, CorrectHandler) {
std::vector<int> line_numbers;
CPPTRACE_TRY {
CPPTRACE_TRY {
line_numbers.insert(line_numbers.begin(), __LINE__ + 1);
stacktrace_from_current_1(line_numbers);
} CPPTRACE_CATCH(const std::logic_error&) {
FAIL();
}
} CPPTRACE_CATCH(const std::exception& e) {
EXPECT_EQ(e.what(), "foobar"sv);
const auto& trace = cpptrace::from_current_exception();
auto it = std::find_if(
trace.frames.begin(),
trace.frames.end(),
[](const cpptrace::stacktrace_frame& frame) {
return frame.filename.find("from_current.cpp") != std::string::npos
&& frame.symbol.find("lambda") == std::string::npos;
}
);
EXPECT_NE(it, trace.frames.end());
it = std::find_if(
trace.frames.begin(),
trace.frames.end(),
[](const cpptrace::stacktrace_frame& frame) {
return frame.symbol.find("FromCurrent_CorrectHandler_Test::TestBody") != std::string::npos;
}
);
EXPECT_NE(it, trace.frames.end());
}
}
TEST(FromCurrent, RawTrace) {
std::vector<int> line_numbers;
CPPTRACE_TRY {
line_numbers.insert(line_numbers.begin(), __LINE__ + 1);
stacktrace_from_current_1(line_numbers);
} CPPTRACE_CATCH(const std::exception& e) {
EXPECT_EQ(e.what(), "foobar"sv);
const auto& raw_trace = cpptrace::raw_trace_from_current_exception();
auto trace = raw_trace.resolve();
auto it = std::find_if(
trace.frames.begin(),
trace.frames.end(),
[](const cpptrace::stacktrace_frame& frame) {
return frame.filename.find("from_current.cpp") != std::string::npos
&& frame.symbol.find("lambda") == std::string::npos;
}
);
EXPECT_NE(it, trace.frames.end());
it = std::find_if(
trace.frames.begin(),
trace.frames.end(),
[](const cpptrace::stacktrace_frame& frame) {
return frame.symbol.find("FromCurrent_RawTrace_Test::TestBody") != std::string::npos;
}
);
EXPECT_NE(it, trace.frames.end());
}
}