Skip to content

Commit

Permalink
assert: add "c-stdaux-assert.h" with more assertions
Browse files Browse the repository at this point in the history
- assert() and c_assert() is enabled by default, but the user can
  opt-out with NDEBUG. Add assertion macros c_assert_nse2() and
  c_assert_nse_on(), which are disabled by default, but the user
  can opt-in by defining C_MORE_ASSERTS to 2 or more.

- with GCC, make an effort that a failed assertion marks the code path
  afterwards as unreachable. That only matters, if the assertion is
  disabled, and with the c_assert() and c_assert_not_reached() macros.
  The effect is, that this can avoid compiler warnings about code paths
  that we know are not supposed to be taken. On the other hand, it uses
  __builtin_unreachable(), so the compiler may make far reaching
  optimizations based on that. But that's only done with NDEBUG, where
  you kinda ask for it.

- c_assert() always evaluates the condition (unlike assert()). Add
  c_assert_nse(), c_assert_nse2() and c_assert_nse_on() which don't
  evaluate the condition, if asserts are disabled. Note that unlike
  assert() from glibc, they still make the condition visible to the
  compiler, so we avoid warnings about unused variables. Of course,
  the condition is not executed, if the assertion (level) is disabled.

- hide the function name and the expressions in the assertion messages
  by default. In a project that uses asserts a lot, there is a large
  amount of strings embedded in the binary, only with the assertion texts.
  Assertion messages are mainly useful to the developer, not the end
  user. Of course, we still print the file and line number. With
  C_MORE_ASSERTS > 1, we also embed the message and the function name.

- like <assert.h>, <c-stdaux-assert.h> can be included multiple times
  and reconfigured by defining NDEBUG and C_MORE_ASSERTS.

- C_MORE_ASSERTS_LEVEL will be defined (as a number). You can both
  use it with the preprocessor ("#if") or C (if()). You can use it
  for more elaborate, opt-in assertions, like

    if (C_MORE_ASSERTS_LEVEL >= 5) {
         /* elaborate calculation. */
    }

  There is little need for "#if", since this is a constant and the
  compiler can optimize it out. It's better to let the compiler see
  all code paths, and not use "#if".
  • Loading branch information
thom311 committed Jul 12, 2024
1 parent efcd731 commit 738a223
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 21 deletions.
136 changes: 136 additions & 0 deletions src/c-stdaux-assert.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/* No include guard. Just like <assert.h>, we can include the header multiple
* times to update the macros for NDEBUG/C_MORE_ASSERTS changes.
*
* The user can define NDEBUG to disable all asserts.
*
* The user can define C_MORE_ASSERTS to a non-negative number.
* - defined(NDEBUG) implies C_MORE_ASSERTS 0
* - C_MORE_ASSERTS 0 means asserts are disabled (like NDEBUG)
* - C_MORE_ASSERTS 1 is the default, and assert() and c_assert() is enabled.
* - C_MORE_ASSERTS > 1 means that c_more_assert() is enabled, based on the level. */

#include <c-stdaux-generic.h>
#include <assert.h>

/**
* C_MORE_ASSERTS_LEVEL: the detected assertion level. Depends on NDEBUG and
* C_MORE_ASSERTS define. */
#undef C_MORE_ASSERTS_LEVEL
#ifdef NDEBUG
# define C_MORE_ASSERTS_LEVEL 0
#elif !defined(C_MORE_ASSERTS)
# define C_MORE_ASSERTS_LEVEL 1
#else
# define C_MORE_ASSERTS_LEVEL (C_MORE_ASSERTS)
#endif

/*****************************************************************************/

#undef _c_assert_fail
#if !defined(C_COMPILER_GNUC)
#define _c_assert_fail(drop_msg, msg) assert(false && msg)
#elif C_MORE_ASSERTS_LEVEL <= 0
#define _c_assert_fail(drop_msg, msg) _c_unreachable_code()
#elif defined(__GNU_LIBRARY__)
/* __assert_fail() also exists on musl, but we don't detect that.
*
* Depending on "drop_msg", we hide the "msg" unless we build with
* "C_MORE_ASSERTS > 1". The reason is that an assertion failure is not useful
* for the end user, and for the developer the __FILE__:__LINE__ is
* sufficient. The __func__ is dropped unless "C_MORE_ASSERTS > 1".
* The point is to not embed many debugging strings in the binary. */
#define _c_assert_fail(drop_msg, msg) \
__assert_fail((drop_msg) && C_MORE_ASSERTS_LEVEL < 1 \
? "<dropped>" \
: ""msg"", \
__FILE__, \
__LINE__, \
C_MORE_ASSERTS_LEVEL < 1 \
? "<unknown-fcn>" \
: __func__)
#else
#define _c_assert_fail(drop_msg, msg) ((void) assert(false && msg), _c_unreachable_code())
#endif

/*****************************************************************************/

/* The remainder we only define 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 { } while (1)

#endif /* defined(C_COMPILER_GNUC) */

#define c_assert_nse_on(_level, _cond) \
do { \
/* c_assert_nse_on() 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_ASSERTS_LEVEL) { \
if (__builtin_constant_p(_cond) && !(_cond)) { \
/* Constant expressions are still evaluated and result \
* in unreachable code too. \
* \
* This can avoid compiler warnings about unreachable \
* code with c_assert_nse(false). \
*/ \
_c_unreachable_code(); \
} \
/* pass */ \
} else if (_c_likely_(_cond)) { \
/* pass */ \
} else { \
_c_assert_fail(true, #_cond); \
} \
} while (0)

#define c_assert_nse(_cond) c_assert_nse_on(1, _cond)
#define c_assert_nse2(_cond) c_assert_nse_on(2, _cond)

/**
* 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.
*
* The macro is async-signal-safe, if @_x is and the assertion doesn't fail.
*/
#define c_assert(_cond) \
do { \
if (!_c_likely_(_cond)) { \
_c_assert_fail(true, #_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).
*/
#define c_assert_not_reached() _c_assert_fail(false, "unreachable")

#endif /* !defined(C_HAS_STDAUX_ASSERT) */

#ifdef __cplusplus
}
#endif
20 changes: 0 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 @@ -316,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
37 changes: 37 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,41 @@ 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 assert_level;
int v;

v = 0;
c_assert((v = 1));
c_assert(v == 1);

/* depending on the assert_level, the condition is evaluate
* or not ("nse" == no-side-effect). */
for (assert_level = 0; assert_level < 10; assert_level++) {
v = 5;
c_assert_nse_on(assert_level, (v = 6));
if (assert_level < C_MORE_ASSERTS_LEVEL) {
c_assert(v == 5);
} else {
c_assert(v == 6);
}
}
}
}

#else /* C_MODULE_GENERIC */
Expand Down

0 comments on commit 738a223

Please sign in to comment.