Skip to content

Commit

Permalink
Add breakpionts to assertions (#98)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremy-rifkin authored Jun 1, 2024
1 parent ad9baa7 commit 66a89db
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 16 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
cmake .. `
-DCMAKE_BUILD_TYPE=${{matrix.target}} `
-DCMAKE_CXX_COMPILER=${{matrix.compiler}} `
-DLIBASSERT_DESIRED_CXX_STANDARD="${{matrix.cxx_version}}" `
-DLIBASSERT_DESIRED_CXX_STANDARD="${{matrix.cxx_version}}"
msbuild .\libassert.sln
build-mingw:
runs-on: windows-2022
Expand Down
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- [Anatomy of Assertion Information](#anatomy-of-assertion-information)
- [Stringification of Custom Objects](#stringification-of-custom-objects)
- [Custom Failure Handlers](#custom-failure-handlers-1)
- [Breakpoints](#breakpoints)
- [Other Donfigurations](#other-donfigurations)
- [Integration with Test Libraries](#integration-with-test-libraries)
- [Catch2](#catch2)
Expand Down Expand Up @@ -99,12 +100,13 @@ You can enable the lowercase `debug_assert` and `assert` aliases with `-DLIBASSE
- Syntax highlighting
- Stack traces
- `DEBUG_ASSERT_VAL` and `ASSERT_VAL` variants that return a value so they can be integrated seamlessly into code, e.g.
`FILE* f = ASSERT_VAL(fopen(path, "r") != nullptr)`.
`FILE* f = ASSERT_VAL(fopen(path, "r") != nullptr)`
- Smart literal formatting
- Stringification of user-defined types
- Custom failure handlers
- Catch2/Gtest integrations
- {fmt} support
- Programatic breakpoints on assertion failures for more debugger-friendly assertions, more info [below](#breakpoints)
## CMake FetchContent Usage
Expand Down Expand Up @@ -727,6 +729,50 @@ all assertion types instead of aborting.
> [!IMPORTANT]
> Failure handlers must not return for `assert_type::panic` and `assert_type::unreachable`.

## Breakpoints

Libassert supports programatic breakpoints on assertion failure to make assertions more debugger-friendly by breaking on
the assertion line as opposed to several layers deep in a callstack:

![breakpoints](./screenshots/breakpoint.png)

This functionality is currently opt-in and it can be enabled by defining `LIBASSERT_BREAK_ON_FAIL`. This is best done as
a compiler flag: `-DLIBASSERT_BREAK_ON_FAIL` or `/DLIBASSERT_BREAK_ON_FAIL`.

Internally the library checks for the presense of a debugger before executing an instruction to breakpoint the debugger.
By default the check is only performed once on the first assertion failure. In some scenarios it may be desirable to
configure this check to always be performed, e.g. if you're using a custom assertion handler that throws an exception
instead of aborting and you may be able to recover from an assertion failure allowing additional failures later and you
only attach a debugger part-way through the run of your program. You can use `libassert::set_debugger_check_mode` to
control how this check is performed:
```cpp
namespace libassert {
enum class debugger_check_mode {
check_once,
check_every_time,
};
void set_debugger_check_mode(debugger_check_mode mode) noexcept;
}
```
The library also exposes its internal utilities for setting breakpoints and checking if the program is being debugged:
```cpp
namespace libassert {
bool is_debugger_present() noexcept;
}
#define LIBASSERT_BREAKPOINT() <...internals...>
#define LIBASSERT_BREAKPOINT_IF_DEBUGGING() <...internals...>
```
This API mimics the API of [P2514](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2546r3.html), which has
been accepted to C++26.
A note about `constexpr`: For clang and msvc libassert can use compiler intrinsics, however, for gcc inline assembly is
required. Inline assembly isn't allowed in constexpr functions pre-C++20, however, gcc supports it with a warning after
gcc 10 and the library can surpress that warning for gcc 12. <!-- https://godbolt.org/z/ETjePhT3v -->

## Other Donfigurations

**Defines:**
Expand Down
23 changes: 23 additions & 0 deletions include/libassert/assert.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ namespace libassert {

LIBASSERT_ATTR_COLD LIBASSERT_EXPORT bool isatty(int fd);

LIBASSERT_ATTR_COLD LIBASSERT_EXPORT bool is_debugger_present() noexcept;
enum class debugger_check_mode {
check_once,
check_every_time,
};
LIBASSERT_ATTR_COLD LIBASSERT_EXPORT void set_debugger_check_mode(debugger_check_mode mode) noexcept;

// returns the type name of T
template<typename T>
[[nodiscard]] std::string_view type_name() noexcept {
Expand Down Expand Up @@ -689,6 +696,19 @@ namespace libassert {
#define LIBASSERT_IGNORE_UNUSED_VALUE
#endif

#define LIBASSERT_BREAKPOINT_IF_DEBUGGING() \
do \
if(libassert::is_debugger_present()) { \
LIBASSERT_BREAKPOINT(); \
} \
while(0)

#ifdef LIBASSERT_BREAK_ON_FAIL
#define LIBASSERT_BREAKPOINT_IF_DEBUGGING_ON_FAIL() LIBASSERT_BREAKPOINT_IF_DEBUGGING()
#else
#define LIBASSERT_BREAKPOINT_IF_DEBUGGING_ON_FAIL()
#endif

#define LIBASSERT_INVOKE(expr, name, type, failaction, ...) \
/* must push/pop out here due to nasty clang bug https://github.com/llvm/llvm-project/issues/63897 */ \
/* must do awful stuff to workaround differences in where gcc and clang allow these directives to go */ \
Expand All @@ -704,6 +724,7 @@ namespace libassert {
LIBASSERT_WARNING_PRAGMA_POP_GCC \
if(LIBASSERT_STRONG_EXPECT(!static_cast<bool>(libassert_decomposer.get_value()), 0)) { \
libassert::ERROR_ASSERTION_FAILURE_IN_CONSTEXPR_CONTEXT(); \
LIBASSERT_BREAKPOINT_IF_DEBUGGING_ON_FAIL(); \
failaction \
LIBASSERT_STATIC_DATA(name, libassert::assert_type::type, #expr, __VA_ARGS__) \
if constexpr(sizeof libassert_decomposer > 32) { \
Expand All @@ -727,6 +748,7 @@ namespace libassert {
#define LIBASSERT_INVOKE_PANIC(name, type, ...) \
do { \
libassert::ERROR_ASSERTION_FAILURE_IN_CONSTEXPR_CONTEXT(); \
LIBASSERT_BREAKPOINT_IF_DEBUGGING_ON_FAIL(); \
LIBASSERT_STATIC_DATA(name, libassert::assert_type::type, "", __VA_ARGS__) \
libassert::detail::process_panic( \
libassert_params \
Expand Down Expand Up @@ -783,6 +805,7 @@ namespace libassert {
/* https://godbolt.org/z/Kq8Wb6q5j https://godbolt.org/z/nMnqnsMYx */ \
if(LIBASSERT_STRONG_EXPECT(!LIBASSERT_STATIC_CAST_TO_BOOL(libassert_value), 0)) { \
libassert::ERROR_ASSERTION_FAILURE_IN_CONSTEXPR_CONTEXT(); \
LIBASSERT_BREAKPOINT_IF_DEBUGGING_ON_FAIL(); \
failaction \
LIBASSERT_STATIC_DATA(name, libassert::assert_type::type, #expr, __VA_ARGS__) \
if constexpr(sizeof libassert_decomposer > 32) { \
Expand Down
87 changes: 85 additions & 2 deletions include/libassert/platform.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
#define LIBASSERT_CLANG_VERSION 0
#endif


#if (defined(__GNUC__) || defined(__GNUG__)) && !defined(__clang__) && !defined(__INTEL_COMPILER)
#define LIBASSERT_IS_GCC 1
#define LIBASSERT_GCC_VERSION (__GNUC__ * 100 + __GNUC_MINOR__)
Expand All @@ -50,7 +49,6 @@
#define LIBASSERT_GCC_VERSION 0
#endif


#ifdef _MSC_VER
#define LIBASSERT_IS_MSVC 1
#define LIBASSERT_MSVC_VERSION _MSC_VER
Expand All @@ -59,6 +57,18 @@
#define LIBASSERT_MSVC_VERSION 0
#endif

#ifdef __INTEL_COMPILER
#define LIBASSERT_IS_ICC 1
#else
#define LIBASSERT_IS_ICC 0
#endif

#ifdef __INTEL_LLVM_COMPILER
#define LIBASSERT_IS_ICX 1
#else
#define LIBASSERT_IS_ICX 0
#endif

///
/// Detect standard library versions.
///
Expand Down Expand Up @@ -220,4 +230,77 @@ namespace libassert::detail {
}
}

#if LIBASSERT_IS_CLANG || LIBASSERT_IS_GCC
#if LIBASSERT_IS_GCC
#define LIBASSERT_WARNING_PRAGMA_PUSH_GCC _Pragma("GCC diagnostic push")
#define LIBASSERT_WARNING_PRAGMA_POP_GCC _Pragma("GCC diagnostic pop")
#define LIBASSERT_WARNING_PRAGMA_PUSH_CLANG
#define LIBASSERT_WARNING_PRAGMA_POP_CLANG
#else
#define LIBASSERT_WARNING_PRAGMA_PUSH_GCC
#define LIBASSERT_WARNING_PRAGMA_POP_GCC
#define LIBASSERT_WARNING_PRAGMA_PUSH_CLANG _Pragma("GCC diagnostic push")
#define LIBASSERT_WARNING_PRAGMA_POP_CLANG _Pragma("GCC diagnostic pop")
#endif
#else
#define LIBASSERT_WARNING_PRAGMA_PUSH_CLANG
#define LIBASSERT_WARNING_PRAGMA_POP_CLANG
#define LIBASSERT_WARNING_PRAGMA_PUSH_GCC
#define LIBASSERT_WARNING_PRAGMA_POP_GCC
#endif

#if LIBASSERT_IS_CLANG || LIBASSERT_IS_ICX
// clang and icx support this as far back as this library could care
#define LIBASSERT_BREAKPOINT() __builtin_debugtrap()
#elif LIBASSERT_IS_MSVC || LIBASSERT_IS_ICC
// msvc and icc support this as far back as this library could care
#define LIBASSERT_BREAKPOINT() __debugbreak()
#elif LIBASSERT_IS_GCC
#if LIBASSERT_GCC_VERSION >= 1200
#define LIBASSERT_IGNORE_CPP20_EXTENSION_WARNING _Pragma("GCC diagnostic ignored \"-Wc++20-extensions\"")
#else
#define LIBASSERT_IGNORE_CPP20_EXTENSION_WARNING
#endif
#define LIBASSERT_ASM_BREAKPOINT(instruction) \
do { \
LIBASSERT_WARNING_PRAGMA_PUSH_GCC \
LIBASSERT_IGNORE_CPP20_EXTENSION_WARNING \
__asm__ __volatile__(instruction) \
; \
LIBASSERT_WARNING_PRAGMA_POP_GCC \
} while(0)
// precedence for these come from llvm's __builtin_debugtrap() implementation
// arm: https://github.com/llvm/llvm-project/blob/e9954ec087d640809082f46d1c7e5ac1767b798d/llvm/lib/Target/ARM/ARMInstrInfo.td#L2393-L2394
// def : Pat<(debugtrap), (BKPT 0)>, Requires<[IsARM, HasV5T]>;
// def : Pat<(debugtrap), (UDF 254)>, Requires<[IsARM, NoV5T]>;
// thumb: https://github.com/llvm/llvm-project/blob/e9954ec087d640809082f46d1c7e5ac1767b798d/llvm/lib/Target/ARM/ARMInstrThumb.td#L1444-L1445
// def : Pat<(debugtrap), (tBKPT 0)>, Requires<[IsThumb, HasV5T]>;
// def : Pat<(debugtrap), (tUDF 254)>, Requires<[IsThumb, NoV5T]>;
// aarch64: https://github.com/llvm/llvm-project/blob/e9954ec087d640809082f46d1c7e5ac1767b798d/llvm/lib/Target/AArch64/AArch64FastISel.cpp#L3615-L3618
// case Intrinsic::debugtrap:
// BuildMI(*FuncInfo.MBB, FuncInfo.InsertPt, MIMD, TII.get(AArch64::BRK))
// .addImm(0xF000);
// return true;
// x86: https://github.com/llvm/llvm-project/blob/e9954ec087d640809082f46d1c7e5ac1767b798d/llvm/lib/Target/X86/X86InstrSystem.td#L81-L84
// def : Pat<(debugtrap),
// (INT3)>, Requires<[NotPS]>;
#if defined(__i386__) || defined(__x86_64__)
#define LIBASSERT_BREAKPOINT() LIBASSERT_ASM_BREAKPOINT("int3")
#elif defined(__arm__) || defined(__thumb__)
#if __ARM_ARCH >= 5
#define LIBASSERT_BREAKPOINT() LIBASSERT_ASM_BREAKPOINT("bkpt #0")
#else
#define LIBASSERT_BREAKPOINT() LIBASSERT_ASM_BREAKPOINT("udf #0xfe")
#endif
#elif defined(__aarch64__) || defined(_M_ARM64)
#define LIBASSERT_BREAKPOINT() LIBASSERT_ASM_BREAKPOINT("brk #0xf000")
#else
// some architecture we aren't prepared for
#define LIBASSERT_BREAKPOINT()
#endif
#else
// some compiler we aren't prepared for
#define LIBASSERT_BREAKPOINT()
#endif

#endif
10 changes: 10 additions & 0 deletions src/common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,20 @@
#define BASIC_PURPL ESC "35m"

#define IS_WINDOWS 0
#define IS_LINUX 0
#define IS_APPLE 0

#if defined(_WIN32)
#undef IS_WINDOWS
#define IS_WINDOWS 1
#elif defined(__linux)
#undef IS_LINUX
#define IS_LINUX 1
#elif defined(__APPLE__)
#undef IS_APPLE
#define IS_APPLE 1
#else
#error "Libassert doesn't recognize this system, please open an issue at https://github.com/jeremy-rifkin/libassert"
#endif

#endif
Loading

0 comments on commit 66a89db

Please sign in to comment.