diff --git a/src/c-stdaux-assert.h b/src/c-stdaux-assert.h new file mode 100644 index 0000000..a878e80 --- /dev/null +++ b/src/c-stdaux-assert.h @@ -0,0 +1,194 @@ +/* There is no include guard. Just like <assert.h>, we can include this header + * multiple times to update the macros for NDEBUG/C_MORE_ASSERT changes. + * + * The user can define NDEBUG to disable all asserts. + * + * The user can define C_MORE_ASSERT to a non-negative number to control + * which assertions are enabled. + */ + +#include <assert.h> +#include <c-stdaux-generic.h> + +/** + * C_MORE_ASSERT: user define to configure assertion levels (similar to NDEBUG). + * + * If NDEBUG is defined, then assert() is a nop. This also implies + * C_MORE_ASSERT_LEVEL of zero, which means that c_more_assert() does not + * evaluate the condition at runtime. + * + * Otherwise, if C_MORE_ASSERT is defined it determines the + * C_MORE_ASSERT_LEVEL. If C_MORE_ASSERT, is undefined, C_MORE_ASSERT_LEVEL + * defaults to 1. + * + * The effective C_MORE_ASSERT_LEVEL affects whether c_more_assert() and + * c_more_assert_with() evaluates the condition at runtime. The purpose is that + * more assertions are disabled by default (and in release builds). For + * debugging and testing, define C_MORE_ASSERT to a number larger than 1. + */ +#undef C_MORE_ASSERT_LEVEL +#ifdef NDEBUG +#define C_MORE_ASSERT_LEVEL 0 +#elif !defined(C_MORE_ASSERT) +#define C_MORE_ASSERT_LEVEL 1 +#else +#define C_MORE_ASSERT_LEVEL (C_MORE_ASSERT) +#endif + +#undef _c_assert_fail +#if C_MORE_ASSERT_LEVEL > 0 && defined(__GNU_LIBRARY__) +/* Depending on "_with_msg", we hide the "msg" string unless we build with + * "C_MORE_ASSERT > 1". The point is to avoid embedding debugging strings in + * the binary with release builds. + * + * The assertion failure messages are often not very useful for the end user + * and for the developer __FILE__:__LINE__ is sufficient. + * + * __assert_fail() also exists on musl, but we don't have a separate detection + * for musl. + */ +#define _c_assert_fail(_with_msg, msg) \ + __assert_fail( \ + C_MORE_ASSERT_LEVEL > 1 || (_with_msg) ? "" msg "" : "<dropped>", \ + __FILE__, __LINE__, \ + C_MORE_ASSERT_LEVEL > 1 || (_with_msg) ? "<unknown-fcn>" : __func__) +#else +#define _c_assert_fail(_with_msg, msg) \ + do { \ + assert(false && msg); \ + _c_unreachable_code(); \ + } while (0) +#endif + +/* There is an include guard. The remainder of this header is only evaluated + * once upon multiple inclusions. */ +#if !defined(C_HAS_STDAUX_ASSERT) +#define C_HAS_STDAUX_ASSERT + +#if defined(C_COMPILER_GNUC) +#define _c_unreachable_code() __builtin_unreachable() +#else /* defined(C_COMPILER_GNUC) */ +#define _c_unreachable_code() \ + do { \ + /* Infinite loop without side effects is undefined behavior and marks \ + * unreachable code. */ \ + } while (1) +#endif /* defined(C_COMPILER_GNUC) */ + +#if defined(C_COMPILER_GNUC) +#define _c_assert_constant(_cond) \ + do { \ + if (__builtin_constant_p(_cond) && !(_cond)) { \ + /* With gcc, constant expressions are still evaluated and result \ + * in unreachable code too. \ + * \ + * The point is to avoid compiler warnings with \ + * c_more_assert(false) and NDEBUG. \ + */ \ + _c_unreachable_code(); \ + } \ + } while (0) +#else /* defined(C_COMPILER_GNUC) */ +#define _c_assert_constant(_cond) \ + do { \ + /* This does nothing. */ \ + } while (0) +#endif /* defined(C_COMPILER_GNUC) */ + +/** + * c_more_assert_with() - Conditional runtime assertion. + * @_level: Assertion level that determines whether the assertion is evaluated, + * based on comparison with C_MORE_ASSERT_LEVEL. + * @_cond: Condition or expression to validate. + * + * This macro performs an assertion based on the specified _level in comparison + * to the compile-time constant C_MORE_ASSERT_LEVEL. C_MORE_ASSERT_LEVEL + * typically defaults to 1 but can be modified by defining NDEBUG or + * C_MORE_ASSERT. + * + * - If _level is less than C_MORE_ASSERT_LEVEL, the condition is ignored and + * the assertion code is excluded from the final build, allowing for performance + * optimizations. + * + * - If _cond is a constant expression that fails, the compiler will mark the code + * path as unreachable, regardless of NDEBUG or the configured C_MORE_ASSERT_LEVEL. + * + * Unlike c_assert(), which always evaluates the condition, + * `c_more_assert_with()` * only evaluates the condition if the specified _level * + * meets the configured assertion threshold. This conditional behavior requires * + * that _cond has no side effects, as it may not be evaluated in all cases. + * + * Note: This macro is usually excluded from regular builds unless explicitly + * enabled by defining C_MORE_ASSERT, making it particularly useful for debugging + * and testing without incurring runtime costs in production builds. + * + * The macro is async-signal-safe, if @_cond is and the assertion doesn't fail. + */ +#define c_more_assert_with(_level, _cond) \ + do { \ + /* c_more_assert_with() must do *nothing* of effect, \ + * except evaluating @_cond (0 or 1 times). \ + * \ + * As such, it is async-signal-safe (provided @_cond and \ + * @_level is, and the assertion does not fail). */ \ + if ((_level) < C_MORE_ASSERT_LEVEL) { \ + _c_assert_constant(_cond); \ + } else if (_c_likely_(_cond)) { \ + /* pass */ \ + } else { \ + _c_assert_fail(false, #_cond); \ + } \ + } while (0) + +/** + * c_more_assert() - Conditional runtime assertion. + * @_cond: Condition or expression to validate. + * + * This is the same as c_more_assert_with(2, _cond). This means that + * the assertion is usually disabled in regular builds unless the user + * opts in by setting C_MORE_ASSERT to 2 or larger. + * + * The macro is async-signal-safe, if @_cond is and the assertion doesn't fail. + */ +#define c_more_assert(_cond) c_more_assert_with(2, _cond) + +/** + * c_assert() - Runtime assertions + * @_cond: Result of an expression + * + * This function behaves like the standard ``assert(3)`` macro. That is, if + * ``NDEBUG`` is defined, it is a no-op. In all other cases it will assert that + * the result of the passed expression is true. + * + * Unlike the standard ``assert(3)`` macro, this function always evaluates its + * argument. This means side-effects will always be evaluated! However, if the + * macro is used with constant expressions, the compiler will be able to + * optimize it away. + * + * The macro is async-signal-safe, if @_cond is and the assertion doesn't fail. + */ +#define c_assert(_cond) \ + do { \ + if (!_c_likely_(_cond)) { \ + _c_assert_fail(false, #_cond); \ + } \ + } while (0) + +/** + * c_assert_not_reached() - Fail assertion when called. + * + * With C_COMPILER_GNUC, the macro calls assert(false) and marks the code + * path as __builtin_unreachable(). The benefit is that also with NDEBUG the + * compiler considers the path unreachable. + * + * Otherwise, just calls assert(false). + * + * The macro is async-signal-safe. + */ +#define c_assert_not_reached() _c_assert_fail(true, "unreachable") + +#endif /* !defined(C_HAS_STDAUX_ASSERT) */ + +#ifdef __cplusplus +} +#endif diff --git a/src/c-stdaux-generic.h b/src/c-stdaux-generic.h index 6f87fd4..3800b8c 100644 --- a/src/c-stdaux-generic.h +++ b/src/c-stdaux-generic.h @@ -81,7 +81,6 @@ extern "C" { */ /**/ -#include <assert.h> #include <errno.h> #include <inttypes.h> #include <limits.h> @@ -129,6 +128,8 @@ extern "C" { * * Outside of macros, this has no added value. * + * This macro is async-signal-safe, provided that the condition is. + * * Return: Evaluates to the value of ``!!_x``. */ #define _c_boolean_expr_(_x) _c_internal_boolean_expr_(__COUNTER__, _x) @@ -164,6 +165,8 @@ extern "C" { * * Alias for ``__builtin_expect(!!(_x), 1)``. * + * This macro is async-signal-safe, provided that the condition is. + * * Return: The expression ``!!_x`` is evaluated and returned. */ #define _c_likely_(_x) _c_internal_likely_(_x) @@ -200,6 +203,8 @@ extern "C" { * * Alias for ``__builtin_expect(!!(_x), 0)``. * + * This macro is async-signal-safe, provided that the condition is. + * * Return: The expression ``!!_x`` is evaluated and returned. */ #define _c_unlikely_(_x) _c_internal_unlikely_(_x) @@ -310,25 +315,6 @@ extern "C" { # define c_internal_assume_aligned(_ptr, _alignment, _offset) ((void)(_alignment), (void)(_offset), (_ptr)) #endif -/** - * c_assert() - Runtime assertions - * @_x: Result of an expression - * - * This function behaves like the standard ``assert(3)`` macro. That is, if - * ``NDEBUG`` is defined, it is a no-op. In all other cases it will assert that - * the result of the passed expression is true. - * - * Unlike the standard ``assert(3)`` macro, this function always evaluates its - * argument. This means side-effects will always be evaluated! However, if the - * macro is used with constant expressions, the compiler will be able to - * optimize it away. - */ -#define c_assert(_x) ( \ - _c_likely_(_x) \ - ? assert(true && #_x) \ - : assert(false && #_x) \ - ) - /** * c_errno() - Return valid errno * diff --git a/src/c-stdaux.h b/src/c-stdaux.h index 0eccb20..23be069 100644 --- a/src/c-stdaux.h +++ b/src/c-stdaux.h @@ -50,6 +50,8 @@ extern "C" { # include <c-stdaux-unix.h> #endif +#include <c-stdaux-assert.h> + #ifdef __cplusplus } #endif diff --git a/src/docs/api.rst b/src/docs/api.rst index a25e9fa..be55c53 100644 --- a/src/docs/api.rst +++ b/src/docs/api.rst @@ -1,5 +1,5 @@ API === -.. c:autodoc:: c-stdaux.h c-stdaux-generic.h c-stdaux-gnuc.h c-stdaux-unix.h +.. c:autodoc:: c-stdaux.h c-stdaux-generic.h c-stdaux-gnuc.h c-stdaux-unix.h c-stdaux-assert.h :transform: kerneldoc diff --git a/src/meson.build b/src/meson.build index 0e958af..16dbd96 100644 --- a/src/meson.build +++ b/src/meson.build @@ -20,6 +20,7 @@ libcstdaux_dep = declare_dependency( if not meson.is_subproject() install_headers( 'c-stdaux.h', + 'c-stdaux-assert.h', 'c-stdaux-generic.h', 'c-stdaux-gnuc.h', 'c-stdaux-unix.h', diff --git a/src/test-api.c b/src/test-api.c index e52d42c..1dab1a5 100644 --- a/src/test-api.c +++ b/src/test-api.c @@ -20,6 +20,8 @@ static void direct_cleanup_fn(int p) { (void)p; } C_DEFINE_CLEANUP(int, cleanup_fn); C_DEFINE_DIRECT_CLEANUP(int, direct_cleanup_fn); +int global_int_0; + static void test_api_generic(void) { /* C_COMPILER_* */ { @@ -155,6 +157,34 @@ static void test_api_generic(void) { for (i = 0; i < sizeof(fns) / sizeof(*fns); ++i) c_assert(!!fns[i]); } + + if (false) + c_assert_not_reached(); + + switch (global_int_0) { + default: + /* Test that we don't get a -Wimplicit-fallthrough warning and + * the compiler detect that the function doesn't return. */ + c_assert_not_reached(); + case 1: + case 0: + c_assert(global_int_0 == 0); + break; + } + + { + int v; + + v = 0; + c_assert((v = 1)); + c_assert(v == 1); + + v = 5; + c_more_assert_with(C_MORE_ASSERT_LEVEL - 1, (++v == -1)); + c_more_assert_with(C_MORE_ASSERT_LEVEL, (++v == 6)); + c_more_assert_with(C_MORE_ASSERT_LEVEL + 1, (++v == 7)); + c_assert(v == 7); + } } #else /* C_MODULE_GENERIC */ @@ -280,6 +310,16 @@ static void test_api_gnuc(void) { { c_assert(c_align_to(0, 0) == 0); } + + /* Check that assertions can be nested without compiler warnings. That easily + * happens when asserting on macros that contain expression statements and + * themselves assertions. */ + { + c_assert(__extension__({ c_assert(true); true; })); + c_assert(__extension__({ c_more_assert(true); true; })); + c_more_assert(__extension__({ c_assert(true); true; })); + c_more_assert(__extension__({ c_more_assert(true); true; })); + } } #else /* C_MODULE_GNUC */