Skip to content

Commit cb95bfb

Browse files
committed
assert: add "c-stdaux-assert.h" with c_more_assert()
The standard assert() and c_assert() macros are enabled by default, allowing debugging assertions that can be disabled by defining NDEBUG. Notably, c_assert() always evaluates the condition, whereas assert() expects side-effect-free conditions. However, these macros are often active in most builds, as many projects do not define NDEBUG, even for release builds. While assertions are essential for debugging and testing, they do add runtime overhead. Using different levels of assertion can help manage this. Some assertions check conditions are almost certain to hold, such as function argument validation where code review easily confirms invariants. In such cases, always-on assertions may not be justified, and developers may prefer to avoid assertions that are still active in release builds. This is where `c_more_assert()` fits in. By setting C_MORE_ASSERT to 2 or higher, the user can enable these assertions. Otherwise, they are disabled by default, minimizing runtime cost. `c_more_assert_with()` extends this functionality by allowing selective enablement at specific assertion levels through C_MORE_ASSERT. Compared to assert(), these macros other advantages beside being disabled by default: - The compiler sees the expression regardless of NDEBUG or assertion level, catching potential compile errors in expressions and avoiding certain compiler warnings that otherwise depend on whether the assertion is enabled. - Constant expressions that fail always mark the code path as unreachable, reducing warnings on unreachable paths. Also, as c_assert() evaluates all expressions, unreachable paths are now recognized even with NDEBUG defined. That may allow aggressive optimization as the compiler can reason that the assertion holds. This is what the user asks for with NDEBUG. Similar to <assert.h>, <c-stdaux-assert.h> supports multiple inclusions and reconfiguration through NDEBUG and C_MORE_ASSERT. Assertion failures now print only critical information (e.g., file:line), omitting less useful strings unless C_MORE_ASSERT >= 2, to reduce binary size. Additionally, C_MORE_ASSERT_LEVEL represents the effective assertion level and can be used both as a macro and a runtime constant, allowing conditionally compiled code: if (C_MORE_ASSERT_LEVEL >= 5) { /* Elaborate checking of invariant. The compiler validates this code * while optimizing out the entire block due to the constant expression. */ }
1 parent efcd731 commit cb95bfb

File tree

6 files changed

+235
-21
lines changed

6 files changed

+235
-21
lines changed

src/c-stdaux-assert.h

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/* There is no include guard. Just like <assert.h>, we can include this header
2+
* multiple times to update the macros for NDEBUG/C_MORE_ASSERT changes.
3+
*
4+
* The user can define NDEBUG to disable all asserts.
5+
*
6+
* The user can define C_MORE_ASSERT to a non-negative number to control
7+
* which assertions are enabled.
8+
*/
9+
10+
#include <assert.h>
11+
#include <c-stdaux-generic.h>
12+
13+
/**
14+
* C_MORE_ASSERT: user define to configure assertion levels (similar to NDEBUG).
15+
*
16+
* If NDEBUG is defined, then assert() is a nop. This also implies
17+
* C_MORE_ASSERT_LEVEL of zero, which means that c_more_assert() does not
18+
* evaluate the condition at runtime.
19+
*
20+
* Otherwise, if C_MORE_ASSERT is defined it determines the
21+
* C_MORE_ASSERT_LEVEL. If C_MORE_ASSERT, is undefined, C_MORE_ASSERT_LEVEL
22+
* defaults to 1.
23+
*
24+
* The effective C_MORE_ASSERT_LEVEL affects whether c_more_assert() and
25+
* c_more_assert_with() evaluates the condition at runtime. The purpose is that
26+
* more assertions are disabled by default (and in release builds). For
27+
* debugging and testing, define C_MORE_ASSERT to a number larger than 1.
28+
*/
29+
#undef C_MORE_ASSERT_LEVEL
30+
#ifdef NDEBUG
31+
#define C_MORE_ASSERT_LEVEL 0
32+
#elif !defined(C_MORE_ASSERT)
33+
#define C_MORE_ASSERT_LEVEL 1
34+
#else
35+
#define C_MORE_ASSERT_LEVEL (C_MORE_ASSERT)
36+
#endif
37+
38+
#undef _c_assert_fail
39+
#if C_MORE_ASSERT_LEVEL > 0 && defined(__GNU_LIBRARY__)
40+
/* Depending on "_with_msg", we hide the "msg" string unless we build with
41+
* "C_MORE_ASSERT > 1". The point is to avoid embedding debugging strings in
42+
* the binary with release builds.
43+
*
44+
* The assertion failure messages are often not very useful for the end user
45+
* and for the developer __FILE__:__LINE__ is sufficient.
46+
*
47+
* __assert_fail() also exists on musl, but we don't have a separate detection
48+
* for musl.
49+
*/
50+
#define _c_assert_fail(_with_msg, msg) \
51+
__assert_fail( \
52+
C_MORE_ASSERT_LEVEL > 1 || (_with_msg) ? "" msg "" : "<dropped>", \
53+
__FILE__, __LINE__, \
54+
C_MORE_ASSERT_LEVEL > 1 || (_with_msg) ? "<unknown-fcn>" : __func__)
55+
#else
56+
#define _c_assert_fail(_with_msg, msg) \
57+
((void)assert(0 && msg), _c_unreachable_code())
58+
#endif
59+
60+
/* There is an include guard. The remainder of this header is only evaluated
61+
* once upon multiple inclusions. */
62+
#if !defined(C_HAS_STDAUX_ASSERT)
63+
#define C_HAS_STDAUX_ASSERT
64+
65+
#if defined(C_COMPILER_GNUC)
66+
#define _c_unreachable_code() __builtin_unreachable()
67+
#else /* defined(C_COMPILER_GNUC) */
68+
#define _c_unreachable_code() \
69+
do { \
70+
/* Infinite loop without side effects is undefined behavior and marks \
71+
* unreachable code. */ \
72+
} while (1)
73+
#endif /* defined(C_COMPILER_GNUC) */
74+
75+
#if defined(C_COMPILER_GNUC)
76+
#define _c_assert_constant(_cond) \
77+
do { \
78+
if (__builtin_constant_p(_cond) && !(_cond)) { \
79+
/* With gcc, constant expressions are still evaluated and result \
80+
* in unreachable code too. \
81+
* \
82+
* The point is to avoid compiler warnings with \
83+
* c_more_assert(false) and NDEBUG. \
84+
*/ \
85+
_c_unreachable_code(); \
86+
} \
87+
} while (0)
88+
#else /* defined(C_COMPILER_GNUC) */
89+
#define _c_assert_constant(_cond) \
90+
do { \
91+
/* This does nothing. */ \
92+
} while (0)
93+
#endif /* defined(C_COMPILER_GNUC) */
94+
95+
/**
96+
* c_more_assert_with() - Conditional runtime assertion.
97+
* @_level: Assertion level that determines whether the assertion is evaluated,
98+
* based on comparison with C_MORE_ASSERT_LEVEL.
99+
* @_cond: Condition or expression to validate.
100+
*
101+
* This macro performs an assertion based on the specified _level in comparison
102+
* to the compile-time constant C_MORE_ASSERT_LEVEL. C_MORE_ASSERT_LEVEL
103+
* typically defaults to 1 but can be modified by defining NDEBUG or
104+
* C_MORE_ASSERT.
105+
*
106+
* - If _level is less than C_MORE_ASSERT_LEVEL, the condition is ignored and
107+
* the assertion code is excluded from the final build, allowing for performance
108+
* optimizations.
109+
*
110+
* - If _cond is a constant expression that fails, the compiler will mark the code
111+
* path as unreachable, regardless of NDEBUG or the configured C_MORE_ASSERT_LEVEL.
112+
*
113+
* Unlike c_assert(), which always evaluates the condition,
114+
* `c_more_assert_with()` * only evaluates the condition if the specified _level *
115+
* meets the configured assertion threshold. This conditional behavior requires *
116+
* that _cond has no side effects, as it may not be evaluated in all cases.
117+
*
118+
* Note: This macro is usually excluded from regular builds unless explicitly
119+
* enabled by defining C_MORE_ASSERT, making it particularly useful for debugging
120+
* and testing without incurring runtime costs in production builds.
121+
*
122+
* The macro is async-signal-safe, if @_cond is and the assertion doesn't fail.
123+
*/
124+
#define c_more_assert_with(_level, _cond) \
125+
do { \
126+
/* c_more_assert_with() must do *nothing* of effect, \
127+
* except evaluating @_cond (0 or 1 times). \
128+
* \
129+
* As such, it is async-signal-safe (provided @_cond and \
130+
* @_level is, and the assertion does not fail). */ \
131+
if ((_level) < C_MORE_ASSERT_LEVEL) { \
132+
_c_assert_constant(_cond); \
133+
} else if (_c_likely_(_cond)) { \
134+
/* pass */ \
135+
} else { \
136+
_c_assert_fail(0, #_cond); \
137+
} \
138+
} while (0)
139+
140+
/**
141+
* c_more_assert() - Conditional runtime assertion.
142+
* @_cond: Condition or expression to validate.
143+
*
144+
* This is the same as c_more_assert_with(2, _cond). This means that
145+
* the assertion is usually disabled in regular builds unless the user
146+
* opts in by setting C_MORE_ASSERT to 2 or larger.
147+
*
148+
* The macro is async-signal-safe, if @_cond is and the assertion doesn't fail.
149+
*/
150+
#define c_more_assert(_cond) c_more_assert_with(2, _cond)
151+
152+
/**
153+
* c_assert() - Runtime assertions
154+
* @_cond: Result of an expression
155+
*
156+
* This function behaves like the standard ``assert(3)`` macro. That is, if
157+
* ``NDEBUG`` is defined, it is a no-op. In all other cases it will assert that
158+
* the result of the passed expression is true.
159+
*
160+
* Unlike the standard ``assert(3)`` macro, this function always evaluates its
161+
* argument. This means side-effects will always be evaluated! However, if the
162+
* macro is used with constant expressions, the compiler will be able to
163+
* optimize it away.
164+
*
165+
* The macro is async-signal-safe, if @_cond is and the assertion doesn't fail.
166+
*/
167+
#define c_assert(_cond) \
168+
do { \
169+
if (!_c_likely_(_cond)) { \
170+
_c_assert_fail(0, #_cond); \
171+
} \
172+
} while (0)
173+
174+
/**
175+
* c_assert_not_reached() - Fail assertion when called.
176+
*
177+
* With C_COMPILER_GNUC, the macro calls assert(false) and marks the code
178+
* path as __builtin_unreachable(). The benefit is that also with NDEBUG the
179+
* compiler considers the path unreachable.
180+
*
181+
* Otherwise, just calls assert(false).
182+
*
183+
* The macro is async-signal-safe.
184+
*/
185+
#define c_assert_not_reached() _c_assert_fail(1, "unreachable")
186+
187+
#endif /* !defined(C_HAS_STDAUX_ASSERT) */
188+
189+
#ifdef __cplusplus
190+
}
191+
#endif

src/c-stdaux-generic.h

-20
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ extern "C" {
8181
*/
8282
/**/
8383

84-
#include <assert.h>
8584
#include <errno.h>
8685
#include <inttypes.h>
8786
#include <limits.h>
@@ -316,25 +315,6 @@ extern "C" {
316315
# define c_internal_assume_aligned(_ptr, _alignment, _offset) ((void)(_alignment), (void)(_offset), (_ptr))
317316
#endif
318317

319-
/**
320-
* c_assert() - Runtime assertions
321-
* @_x: Result of an expression
322-
*
323-
* This function behaves like the standard ``assert(3)`` macro. That is, if
324-
* ``NDEBUG`` is defined, it is a no-op. In all other cases it will assert that
325-
* the result of the passed expression is true.
326-
*
327-
* Unlike the standard ``assert(3)`` macro, this function always evaluates its
328-
* argument. This means side-effects will always be evaluated! However, if the
329-
* macro is used with constant expressions, the compiler will be able to
330-
* optimize it away.
331-
*/
332-
#define c_assert(_x) ( \
333-
_c_likely_(_x) \
334-
? assert(true && #_x) \
335-
: assert(false && #_x) \
336-
)
337-
338318
/**
339319
* c_errno() - Return valid errno
340320
*

src/c-stdaux.h

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ extern "C" {
5050
# include <c-stdaux-unix.h>
5151
#endif
5252

53+
#include <c-stdaux-assert.h>
54+
5355
#ifdef __cplusplus
5456
}
5557
#endif

src/docs/api.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
API
22
===
33

4-
.. c:autodoc:: c-stdaux.h c-stdaux-generic.h c-stdaux-gnuc.h c-stdaux-unix.h
4+
.. c:autodoc:: c-stdaux.h c-stdaux-generic.h c-stdaux-gnuc.h c-stdaux-unix.h c-stdaux-assert.h
55
:transform: kerneldoc

src/meson.build

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ libcstdaux_dep = declare_dependency(
2020
if not meson.is_subproject()
2121
install_headers(
2222
'c-stdaux.h',
23+
'c-stdaux-assert.h',
2324
'c-stdaux-generic.h',
2425
'c-stdaux-gnuc.h',
2526
'c-stdaux-unix.h',

src/test-api.c

+40
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ static void direct_cleanup_fn(int p) { (void)p; }
2020
C_DEFINE_CLEANUP(int, cleanup_fn);
2121
C_DEFINE_DIRECT_CLEANUP(int, direct_cleanup_fn);
2222

23+
int global_int_0;
24+
2325
static void test_api_generic(void) {
2426
/* C_COMPILER_* */
2527
{
@@ -155,6 +157,34 @@ static void test_api_generic(void) {
155157
for (i = 0; i < sizeof(fns) / sizeof(*fns); ++i)
156158
c_assert(!!fns[i]);
157159
}
160+
161+
if (false)
162+
c_assert_not_reached();
163+
164+
switch (global_int_0) {
165+
default:
166+
/* Test that we don't get a -Wimplicit-fallthrough warning and
167+
* the compiler detect that the function doesn't return. */
168+
c_assert_not_reached();
169+
case 1:
170+
case 0:
171+
c_assert(global_int_0 == 0);
172+
break;
173+
}
174+
175+
{
176+
int v;
177+
178+
v = 0;
179+
c_assert((v = 1));
180+
c_assert(v == 1);
181+
182+
v = 5;
183+
c_more_assert_with(C_MORE_ASSERT_LEVEL - 1, (++v == -1));
184+
c_more_assert_with(C_MORE_ASSERT_LEVEL, (++v == 6));
185+
c_more_assert_with(C_MORE_ASSERT_LEVEL + 1, (++v == 7));
186+
c_assert(v == 7);
187+
}
158188
}
159189

160190
#else /* C_MODULE_GENERIC */
@@ -280,6 +310,16 @@ static void test_api_gnuc(void) {
280310
{
281311
c_assert(c_align_to(0, 0) == 0);
282312
}
313+
314+
/* Check that assertions can be nested without compiler warnings. That easily
315+
* happens when asserting on macros that contain expression statements and
316+
* themselves assertions. */
317+
{
318+
c_assert(__extension__({ c_assert(true); true; }));
319+
c_assert(__extension__({ c_more_assert(true); true; }));
320+
c_more_assert(__extension__({ c_assert(true); true; }));
321+
c_more_assert(__extension__({ c_more_assert(true); true; }));
322+
}
283323
}
284324

285325
#else /* C_MODULE_GNUC */

0 commit comments

Comments
 (0)