From 0b6810aca783d2fb08e11929672757d73f1b9a7e Mon Sep 17 00:00:00 2001 From: Tristan Brindle Date: Mon, 9 Sep 2024 19:18:59 +0100 Subject: [PATCH 1/3] Add fail_fast runtime error policy This adds a new fail_fast runtime error policy to go with our existing terminate and unwind policies. As the name suggests, fail_fast is intended to tear down the process in the fastest way possible should a runtime error occur. It uses `__builtin_trap()` on GCC and Clang, the `__fastfail()` intrinsic on MSVC, and otherwise falls back to `std::abort()`. Unlike the terminate policy, it never attempts to print a message to stderr. --- include/flux/core/assert.hpp | 61 +++++++++++++++++++++++++++++------- include/flux/core/config.hpp | 8 +++-- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/include/flux/core/assert.hpp b/include/flux/core/assert.hpp index 5c1416be..76a44982 100644 --- a/include/flux/core/assert.hpp +++ b/include/flux/core/assert.hpp @@ -14,6 +14,15 @@ #include #include +#if defined(__has_builtin) +# if __has_builtin(__builtin_trap) +# define FLUX_HAS_BUILTIN_TRAP 1 +# endif +#elif defined(_MSC_VER) +# include +# define FLUX_HAS_FASTFAIL 1 +#endif + namespace flux { FLUX_EXPORT @@ -24,21 +33,49 @@ struct unrecoverable_error : std::logic_error { namespace detail { struct runtime_error_fn { +private: [[noreturn]] - void operator()(char const* msg, - std::source_location loc = std::source_location::current()) const + static inline void fail_fast() { - if constexpr (config::on_error == error_policy::unwind) { - char buf[1024]; - std::snprintf(buf, 1024, "%s:%u: Fatal error: %s", - loc.file_name(), loc.line(), msg); - throw unrecoverable_error(buf); +#if FLUX_HAS_BUILTIN_TRAP + __builtin_trap(); +#elif FLUX_HAS_FASTFAIL + __fastfail(7); // FAST_FAIL_FATAL_APP_EXIT +#else + std::abort(); +#endif + } + + [[noreturn]] + static void unwind(const char* msg, std::source_location loc) + { + char buf[1024]; + std::snprintf(buf, 1024, "%s:%u: Fatal error: %s", + loc.file_name(), loc.line(), msg); + throw unrecoverable_error(buf); + } + + [[noreturn]] + static void terminate(const char* msg, std::source_location loc) + { + if constexpr (config::print_error_on_terminate) { + std::fprintf(stderr, "%s:%u: Fatal error: %s\n", + loc.file_name(), loc.line(), msg); + } + std::terminate(); + } + +public: + [[noreturn]] + inline void operator()(char const* msg, + std::source_location loc = std::source_location::current()) const + { + if constexpr (config::on_error == error_policy::fail_fast) { + fail_fast(); + } else if constexpr (config::on_error == error_policy::unwind) { + unwind(msg, loc); } else { - if constexpr (config::print_error_on_terminate) { - std::fprintf(stderr, "%s:%u: Fatal error: %s\n", - loc.file_name(), loc.line(), msg); - } - std::terminate(); + terminate(msg, loc); } } }; diff --git a/include/flux/core/config.hpp b/include/flux/core/config.hpp index 322b663e..92d7af72 100644 --- a/include/flux/core/config.hpp +++ b/include/flux/core/config.hpp @@ -13,7 +13,8 @@ #include #define FLUX_ERROR_POLICY_TERMINATE 1 -#define FLUX_ERROR_POLICY_UNWIND 2 +#define FLUX_ERROR_POLICY_UNWIND 2 +#define FLUX_ERROR_POLICY_FAIL_FAST 3 #define FLUX_OVERFLOW_POLICY_ERROR 10 #define FLUX_OVERFLOW_POLICY_WRAP 11 @@ -47,6 +48,8 @@ # define FLUX_ERROR_POLICY FLUX_ERROR_POLICY_TERMINATE #elif defined(FLUX_UNWIND_ON_ERROR) # define FLUX_ERROR_POLICY FLUX_ERROR_POLICY_UNWIND +#elif defined(FLUX_FAIL_FAST_ON_ERROR) +# define FLUX_ERROR_POLICY FLUX_ERROR_POLICY_FAIL_FAST #else # define FLUX_ERROR_POLICY FLUX_DEFAULT_ERROR_POLICY #endif // FLUX_TERMINATE_ON_ERROR @@ -123,7 +126,8 @@ namespace flux { FLUX_EXPORT enum class error_policy { terminate = FLUX_ERROR_POLICY_TERMINATE, - unwind = FLUX_ERROR_POLICY_UNWIND + unwind = FLUX_ERROR_POLICY_UNWIND, + fail_fast = FLUX_ERROR_POLICY_FAIL_FAST }; FLUX_EXPORT From ffa75ca40872e599a0eb78c069536e28afbfb1b7 Mon Sep 17 00:00:00 2001 From: Tristan Brindle Date: Mon, 9 Sep 2024 19:36:01 +0100 Subject: [PATCH 2/3] Force inline fast_fail to improve MSVC codegen --- include/flux/core/assert.hpp | 11 ++++++----- include/flux/macros.hpp | 8 +++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/include/flux/core/assert.hpp b/include/flux/core/assert.hpp index 76a44982..88310f5a 100644 --- a/include/flux/core/assert.hpp +++ b/include/flux/core/assert.hpp @@ -35,7 +35,8 @@ namespace detail { struct runtime_error_fn { private: [[noreturn]] - static inline void fail_fast() + FLUX_ALWAYS_INLINE + static void fail_fast() { #if FLUX_HAS_BUILTIN_TRAP __builtin_trap(); @@ -67,8 +68,9 @@ struct runtime_error_fn { public: [[noreturn]] - inline void operator()(char const* msg, - std::source_location loc = std::source_location::current()) const + FLUX_ALWAYS_INLINE + void operator()(char const* msg, + std::source_location loc = std::source_location::current()) const { if constexpr (config::on_error == error_policy::fail_fast) { fail_fast(); @@ -125,8 +127,7 @@ struct indexed_bounds_check_fn { } } #endif - assert_fn{}(idx >= T{0}, "index cannot be negative", loc); - assert_fn{}(idx < limit, "out-of-bounds sequence access", loc); + assert_fn{}(idx >= T{0} && idx < limit, "out-of-bounds sequence access", loc); } } }; diff --git a/include/flux/macros.hpp b/include/flux/macros.hpp index 38faae19..72c6865b 100644 --- a/include/flux/macros.hpp +++ b/include/flux/macros.hpp @@ -23,10 +23,12 @@ #define FLUX_DECLVAL(...) ((static_cast<__VA_ARGS__(*)()noexcept>(nullptr))()) -#ifdef __GNUC__ -#define FLUX_ALWAYS_INLINE [[gnu::always_inline]] +#if defined(__GNUC__) +# define FLUX_ALWAYS_INLINE [[gnu::always_inline]] inline +#elif defined(_MSC_VER) +# define FLUX_ALWAYS_INLINE __forceinline #else -#define FLUX_ALWAYS_INLINE +# define FLUX_ALWAYS_INLINE inline #endif #define FLUX_NO_UNIQUE_ADDRESS [[no_unique_address]] From c08e6d6e8554012009d260f7bba52b1f3812a392 Mon Sep 17 00:00:00 2001 From: Tristan Brindle Date: Tue, 10 Sep 2024 16:28:46 +0100 Subject: [PATCH 3/3] Update library config docs --- docs/reference/config.rst | 41 ++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/docs/reference/config.rst b/docs/reference/config.rst index ab8e7ebb..d75653eb 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -17,21 +17,30 @@ These must be set before ``#include`` -ing any Flux headers. Runtime Error Policy ===================== -When normal execution of a program cannot continue, Flux will raise a *runtime error*. Typically this happens because the library has detected a situation that would otherwise lead to undefined behaviour -- for example, an out-of-bounds read of a sequence or a dereference of an empty :type:`flux::optional`. The library can be configured to handle runtime errors in one of two ways: either by terminating, or by unwinding. +When normal execution of a program cannot continue, Flux will raise a *runtime error*. Typically this happens because the library has detected a situation that would otherwise lead to undefined behaviour -- for example, an out-of-bounds read of a sequence or a dereference of an empty :type:`flux::optional`. A runtime error will be handled according to the configured error policy: one of *terminate*, *fail fast* or *unwind*. The default error policy is *terminate*. -Termination ------------ +Terminate +--------- .. c:macro:: FLUX_TERMINATE_ON_ERROR .. c:macro:: FLUX_PRINT_ERROR_ON_TERMINATE -If :c:macro:`FLUX_TERMINATE_ON_ERROR` is defined, a Flux runtime error will result in a call to :func:`std::terminate`. +If :c:macro:`FLUX_TERMINATE_ON_ERROR` is defined, a Flux runtime error will result in a call to :func:`std::terminate`. This will in turn run the currently set terminate handler before halting the process. By default, the library will attempt to print a short message to ``stdout`` describing the error before terminating. This can be disabled by setting :c:macro:`FLUX_PRINT_ERROR_ON_TERMINATE` to ``0``. -Unwinding +Fail Fast --------- +.. c:macro:: FLUX_FAIL_FAST_ON_ERROR + +Alternatively :c:macro:`FLUX_FAIL_FAST_ON_ERROR` is defined, the library will attempt to halt the running process in the fastest way possible, typically by executing an illegal CPU instruction. No cleanup will occur, no debug info will be printed to the console, and no exit handlers will be called. + +Using the fail fast policy typically results in the smallest binary code size. + +Unwind +------ + .. c:macro:: FLUX_UNWIND_ON_ERROR .. struct:: unrecoverable_error : std::logic_error @@ -46,7 +55,7 @@ If :c:macro:`FLUX_UNWIND_ON_ERROR` is defined, a runtime error will result in an .. note:: - According to the C++ standard, it is unspecified whether stack unwinding will occur if an exception is not caught -- an implementation may choose to immediately call :func:`std::terminate` without performing unwinding. + According to the C++ standard, it is unspecified whether stack unwinding occurs if an exception is not caught -- an implementation may choose to immediately call :func:`std::terminate` without performing unwinding if there is no matching catch clause anywhere in the call stack. If using the "unwind" policy, you may also wish to wrap your :func:`main` in an appropriate try-catch block to ensure unwinding occurs on all platforms. @@ -86,7 +95,7 @@ A custom :c:macro:`FLUX_INT_TYPE` must be a built-in signed integer type at leas Numeric Error Policies ====================== -Flux provides a selection of checked integer functions, which are used internally by the library when performing operations on signed ints. The behaviour of these functions can be customised by setting the overflow policy and divide by zero policies as desired. +Flux provides a selection of checked integer functions, which are used internally by the library when performing operations on ints. The behaviour of these functions can be customised by setting the overflow, divide by zero and integer cast policies as desired. Overflow policy --------------- @@ -95,9 +104,9 @@ Overflow policy .. c:macro:: FLUX_WRAP_ON_OVERFLOW .. c:macro:: FLUX_IGNORE_OVERFLOW -If :c:macro:`FLUX_ERROR_ON_OVERFLOW` is set, a signed integer operation which would overflow will instead raise a runtime error. This is the default in debug builds (i.e. ``NDEBUG`` is not set). +If :c:macro:`FLUX_ERROR_ON_OVERFLOW` is set, an integer operation which would overflow will instead raise a runtime error. This is the default in debug builds (i.e. ``NDEBUG`` is not set). -Alternatively, if :c:macro:`FLUX_WRAP_ON_OVERFLOW` is set, signed integer operations are performed as if by casting to the equivalent unsigned type, performing the operation, and then casting back to the original signed type. This avoids undefined behaviour (since overflow is well defined on unsigned ints) and avoids needing to generate error handing code, at the cost of giving numerically incorrect answers if overflow occurs. This is the default in release builds (i.e. ``NDEBUG`` is set). +Alternatively, if :c:macro:`FLUX_WRAP_ON_OVERFLOW` is set, integer operations are performed as if by casting to the equivalent unsigned type, performing the operation, and then casting back to the original type. This avoids undefined behaviour (since overflow is well defined on unsigned ints) and avoids needing to generate error handing code, at the cost of giving numerically incorrect answers if overflow occurs. This is the default in release builds (i.e. ``NDEBUG`` is set). Finally, if :c:macro:`FLUX_IGNORE_OVERFLOW` is set, the standard built-in integer operations will be used. This means that an operation which overflows will result in undefined behaviour. Use this setting if you are already handling signed integer UB by some other means (for example compiling with ``-ftrapv`` or using UB Sanitizer) and wish to avoid "double checking". @@ -107,6 +116,16 @@ Divide by zero policy .. c:macro:: FLUX_ERROR_ON_DIVIDE_BY_ZERO .. c:macro:: FLUX_IGNORE_DIVIDE_BY_ZERO -If :c:macro:`FLUX_ERROR_ON_DIVIDE_BY_ZERO` is set then a runtime error will be raised if zero is passed as the second argument to :func:`flux::checked_div` or :func:`flux::checked_mod`. This is the default in debug builds. +If :c:macro:`FLUX_ERROR_ON_DIVIDE_BY_ZERO` is set then a runtime error will be raised if zero is passed as the second argument to :func:`flux::num::div` or :func:`flux::num::mod`. This is the default in debug builds. + +Alternatively, if :c:macro:`FLUX_IGNORE_DIVIDE_BY_ZERO` is set then no extra zero check will be used in :func:`flux::num::div` or :func:`flux::num::mod`. This is the default for release builds. + +Integer cast policy +------------------- + +.. c:macro:: FLUX_INTEGER_CAST_POLICY_CHECKED +.. c:macro:: FLUX_INTEGER_CAST_POLICY_UNCHECKED + +If :c:macro:`FLUX_INTEGER_CAST_POLICY_CHECKED` is defined, then :expr:`flux::num::cast(from)` will (if necessary) perform a runtime check to ensure that the source value is within the bounds of the destination type -- that is, that the cast is not lossy. This is the default for debug builds. -Alternatively, if :c:macro:`FLUX_IGNORE_DIVIDE_BY_ZERO` is set then no extra zero check will be used in :func:`flux::checked_div` or :func:`flux::checked_mod`. This is the default for release builds. +Alternatively :c:macro:`FLUX_INTEGER_CAST_POLICY_UNCHECKED` is defined then no runtime check will occur, and :expr:`flux::num::cast(from)` is equivalent to a plain :expr:`static_cast(from)`. This is the default in release builds.