Skip to content

Commit 05f796a

Browse files
committed
assert: add "c-stdaux-assert.h" with more assertions
- 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".
1 parent efcd731 commit 05f796a

File tree

6 files changed

+183
-21
lines changed

6 files changed

+183
-21
lines changed

src/c-stdaux-assert.h

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/* No include guard. Just like <assert.h>, we can include the header multiple
2+
* times to update the macros for NDEBUG/C_MORE_ASSERTS changes.
3+
*
4+
* The user can define NDEBUG to disable all asserts.
5+
*
6+
* The user can define C_MORE_ASSERTS to a non-negative number.
7+
* - defined(NDEBUG) implies C_MORE_ASSERTS 0
8+
* - C_MORE_ASSERTS 0 means asserts are disabled (like NDEBUG)
9+
* - C_MORE_ASSERTS 1 is the default, and assert() and c_assert() is enabled.
10+
* - C_MORE_ASSERTS > 1 means that c_more_assert() is enabled, based on the
11+
* level. */
12+
13+
#include <assert.h>
14+
#include <c-stdaux-generic.h>
15+
16+
/**
17+
* C_MORE_ASSERTS_LEVEL: the detected assertion level. Depends on NDEBUG and
18+
* C_MORE_ASSERTS define. */
19+
#undef C_MORE_ASSERTS_LEVEL
20+
#ifdef NDEBUG
21+
#define C_MORE_ASSERTS_LEVEL 0
22+
#elif !defined(C_MORE_ASSERTS)
23+
#define C_MORE_ASSERTS_LEVEL 1
24+
#else
25+
#define C_MORE_ASSERTS_LEVEL (C_MORE_ASSERTS)
26+
#endif
27+
28+
#undef _c_assert_fail
29+
#if !defined(C_COMPILER_GNUC)
30+
#define _c_assert_fail(drop_msg, msg) assert(false && msg)
31+
#elif C_MORE_ASSERTS_LEVEL <= 0
32+
#define _c_assert_fail(drop_msg, msg) _c_unreachable_code()
33+
#elif defined(__GNU_LIBRARY__)
34+
/* __assert_fail() also exists on musl, but we don't detect that.
35+
*
36+
* Depending on "drop_msg", we hide the "msg" unless we build with
37+
* "C_MORE_ASSERTS > 1". The reason is that an assertion failure is not useful
38+
* for the end user, and for the developer the __FILE__:__LINE__ is
39+
* sufficient. The __func__ is dropped unless "C_MORE_ASSERTS > 1".
40+
* The point is to not embed many debugging strings in the binary. */
41+
#define _c_assert_fail(drop_msg, msg) \
42+
__assert_fail( \
43+
(drop_msg) && C_MORE_ASSERTS_LEVEL < 1 ? "<dropped>" : "" msg "", \
44+
__FILE__, __LINE__, \
45+
C_MORE_ASSERTS_LEVEL < 1 ? "<unknown-fcn>" : __func__)
46+
#else
47+
#define _c_assert_fail(drop_msg, msg) \
48+
((void)assert(false && msg), _c_unreachable_code())
49+
#endif
50+
51+
/* The remainder we only define once (upon multiple inclusions) */
52+
#if !defined(C_HAS_STDAUX_ASSERT)
53+
#define C_HAS_STDAUX_ASSERT
54+
55+
#if defined(C_COMPILER_GNUC)
56+
57+
#define _c_unreachable_code() __builtin_unreachable()
58+
59+
#define _c_assert_nse_on_disabled(cond) \
60+
do { \
61+
if (__builtin_constant_p(cond) && !(cond)) { \
62+
/* Constant expressions are still evaluated and result \
63+
* in unreachable code too. \
64+
* \
65+
* This can avoid compiler warnings about unreachable \
66+
* code with c_assert_nse(false). \
67+
*/ \
68+
_c_unreachable_code(); \
69+
} \
70+
} while (0)
71+
72+
#else /* defined(C_COMPILER_GNUC) */
73+
74+
#define _c_unreachable_code() \
75+
do { \
76+
/* Infinite loop for unreachable. */ \
77+
} while (1)
78+
79+
#define _c_assert_nse_on_disabled(cond) \
80+
do { \
81+
} while (0)
82+
83+
#endif /* defined(C_COMPILER_GNUC) */
84+
85+
#define c_assert_nse_on(_level, _cond) \
86+
do { \
87+
/* c_assert_nse_on() must do *nothing* of effect, \
88+
* except evaluating @_cond (0 or 1 times). \
89+
* \
90+
* As such, it is async-signal-safe (provided @_cond and \
91+
* @_level is, and the assertion does not fail). */ \
92+
if ((_level) < C_MORE_ASSERTS_LEVEL) { \
93+
_c_assert_nse_on_disabled(_cond); \
94+
/* pass */ \
95+
} else if (_c_likely_(_cond)) { \
96+
/* pass */ \
97+
} else { \
98+
_c_assert_fail(true, #_cond); \
99+
} \
100+
} while (0)
101+
102+
#define c_assert_nse(_cond) c_assert_nse_on(1, _cond)
103+
#define c_assert_nse2(_cond) c_assert_nse_on(2, _cond)
104+
105+
/**
106+
* c_assert() - Runtime assertions
107+
* @_x: Result of an expression
108+
*
109+
* This function behaves like the standard ``assert(3)`` macro. That is, if
110+
* ``NDEBUG`` is defined, it is a no-op. In all other cases it will assert that
111+
* the result of the passed expression is true.
112+
*
113+
* Unlike the standard ``assert(3)`` macro, this function always evaluates its
114+
* argument. This means side-effects will always be evaluated! However, if the
115+
* macro is used with constant expressions, the compiler will be able to
116+
* optimize it away.
117+
*
118+
* The macro is async-signal-safe, if @_x is and the assertion doesn't fail.
119+
*/
120+
#define c_assert(_cond) \
121+
do { \
122+
if (!_c_likely_(_cond)) { \
123+
_c_assert_fail(true, #_cond); \
124+
} \
125+
} while (0)
126+
127+
/**
128+
* c_assert_not_reached() - Fail assertion when called.
129+
*
130+
* With C_COMPILER_GNUC, the macro calls assert(false) and marks the code
131+
* path as __builtin_unreachable(). The benefit is that also with NDEBUG the
132+
* compiler considers the path unreachable.
133+
*
134+
* Otherwise, just calls assert(false).
135+
*/
136+
#define c_assert_not_reached() _c_assert_fail(false, "unreachable")
137+
138+
#endif /* !defined(C_HAS_STDAUX_ASSERT) */
139+
140+
#ifdef __cplusplus
141+
}
142+
#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

+37
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,41 @@ 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 assert_level;
177+
int v;
178+
179+
v = 0;
180+
c_assert((v = 1));
181+
c_assert(v == 1);
182+
183+
/* depending on the assert_level, the condition is evaluate
184+
* or not ("nse" == no-side-effect). */
185+
for (assert_level = 0; assert_level < 10; assert_level++) {
186+
v = 5;
187+
c_assert_nse_on(assert_level, (v = 6));
188+
if (assert_level < C_MORE_ASSERTS_LEVEL) {
189+
c_assert(v == 5);
190+
} else {
191+
c_assert(v == 6);
192+
}
193+
}
194+
}
158195
}
159196

160197
#else /* C_MODULE_GENERIC */

0 commit comments

Comments
 (0)