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

Add fail_fast runtime error policy #210

Merged
merged 3 commits into from
Sep 10, 2024
Merged
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
41 changes: 30 additions & 11 deletions docs/reference/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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
---------------
Expand All @@ -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".

Expand All @@ -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<To>(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<To>(from)` is equivalent to a plain :expr:`static_cast<To>(from)`. This is the default in release builds.
62 changes: 50 additions & 12 deletions include/flux/core/assert.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
#include <stdexcept>
#include <type_traits>

#if defined(__has_builtin)
# if __has_builtin(__builtin_trap)
# define FLUX_HAS_BUILTIN_TRAP 1
# endif
#elif defined(_MSC_VER)
# include <intrin.h>
# define FLUX_HAS_FASTFAIL 1
#endif

namespace flux {

FLUX_EXPORT
Expand All @@ -24,21 +33,51 @@ struct unrecoverable_error : std::logic_error {
namespace detail {

struct runtime_error_fn {
private:
[[noreturn]]
FLUX_ALWAYS_INLINE
static void fail_fast()
{
#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]]
FLUX_ALWAYS_INLINE
void operator()(char const* msg,
std::source_location loc = std::source_location::current()) const
{
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 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);
}
}
};
Expand Down Expand Up @@ -88,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);
}
}
};
Expand Down
8 changes: 6 additions & 2 deletions include/flux/core/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
#include <type_traits>

#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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions include/flux/macros.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down
Loading