Skip to content

assert: add "c-stdaux-assert.h" with c_more_assert() #16

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
194 changes: 194 additions & 0 deletions src/c-stdaux-assert.h
Original file line number Diff line number Diff line change
@@ -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
26 changes: 6 additions & 20 deletions src/c-stdaux-generic.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ extern "C" {
*/
/**/

#include <assert.h>
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
*
Expand Down
2 changes: 2 additions & 0 deletions src/c-stdaux.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ extern "C" {
# include <c-stdaux-unix.h>
#endif

#include <c-stdaux-assert.h>

#ifdef __cplusplus
}
#endif
2 changes: 1 addition & 1 deletion src/docs/api.rst
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
40 changes: 40 additions & 0 deletions src/test-api.c
Original file line number Diff line number Diff line change
Expand Up @@ -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_* */
{
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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 */
Expand Down
Loading