Project 1: Assert Module - Runtime Checking That Actually Helps
Build a custom assertion module that provides informative failure messages, includes file/line/expression information, integrates with debugging tools, and supports both simple checks and formatted diagnostic messages.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 2 - Intermediate |
| Time Estimate | 1-2 days |
| Language | C |
| Prerequisites | C fundamentals, pointers, preprocessor macros, header files |
| Key Topics | Fail-fast philosophy, variadic macros, preprocessor, debugging |
1. Learning Objectives
What you’ll build: A custom assertion module that provides informative failure messages, includes file/line/expression information, integrates with debugging tools, and supports both simple checks and formatted diagnostic messages.
Why it teaches the topic: The standard C assert() macro is bare-bones - it tells you an assertion failed but provides minimal context for debugging. By building your own, you’ll understand variadic macros, the preprocessor’s role in debugging, and why assertions are the first line of defense against bugs. Hanson’s assert establishes the pattern used by all other modules.
By the end of this project, you will:
- Understand the difference between assertions (bug detection) and error handling
- Master variadic macro design with
__VA_ARGS__and__VA_OPT__ - Know how to capture source location with
__FILE__,__LINE__, and__func__ - Implement conditional compilation with NDEBUG
- Create debugging infrastructure that integrates with GDB/LLDB
2. Theoretical Foundation
2.1 Core Concepts
The Fail-Fast Philosophy
The philosophy behind assertions is simple but powerful: “If a precondition is violated, the program is ALREADY broken. Crashing immediately is a FAVOR to the developer.” - David Hanson (paraphrased)
┌─────────────────────────────────────────────────────────────────────────────┐
│ DEFENSIVE PROGRAMMING WITH ASSERTIONS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ THE PHILOSOPHY: │
│ ────────────── │
│ "If a precondition is violated, the program is ALREADY broken. │
│ Crashing immediately is a FAVOR to the developer." │
│ │
│ — David Hanson (paraphrased) │
│ │
│ STANDARD assert() IS WEAK: │
│ ────────────────────────── │
│ │
│ assert(ptr != NULL); // Output: "Assertion failed: ptr != NULL" │
│ // Where? Which function? Which call? │
│ │
│ HANSON'S Assert MODULE: │
│ ─────────────────────── │
│ │
│ #include "assert.h" │
│ │
│ void Table_put(Table_T table, void *key, void *value) { │
│ assert(table); // Precondition: table must be valid │
│ assert(key); // Precondition: key must not be NULL │
│ // ... implementation │
│ } │
│ │
│ Hanson's assert does: │
│ 1. Raises SIGABRT with useful message │
│ 2. Includes file:line:function context │
│ 3. Can be disabled with NDEBUG (like standard assert) │
│ 4. Sets foundation for Except module (exceptions via setjmp/longjmp) │
│ │
│ TYPES OF CHECKED ERRORS: │
│ ──────────────────────── │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Error Type │ Example │ Response │ │
│ ├───────────────────┼──────────────────────────┼───────────────────────┤ │
│ │ Precondition │ NULL pointer argument │ assert() → crash │ │
│ │ Postcondition │ Invalid return state │ assert() → crash │ │
│ │ Invariant │ Corrupt internal data │ assert() → crash │ │
│ │ Resource error │ malloc returns NULL │ RAISE exception │ │
│ │ External failure │ File not found │ Return error code │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ THE FAIL-FAST PRINCIPLE: │
│ ──────────────────────── │
│ │
│ Bug occurs ──► Data corrupted ──► Crash later ──► Debug nightmare │
│ ▲ │
│ │ STOP IT HERE │
│ │ │
│ Bug occurs ──► assert() fires ──► Immediate crash with context │
│ "table.c:47: Table_put: table is NULL" │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
2.2 Why This Matters
The Assert module is the foundation of Hanson’s entire library. Every other module uses assertions to:
- Document preconditions - The code itself declares what must be true
- Catch bugs at the source - Not three functions and 1000 lines later
- Provide diagnostic information - Where, what, and why
- Create a safety net - No silent corruption, no mysterious crashes
The standard library assert() is intentionally minimal. Hanson’s version adds:
- Formatted messages (like printf in your assertion)
- Better integration with debuggers
- Foundation for the Except module (exception handling)
2.3 Historical Context
The assert pattern appears throughout production software:
SQLite: Uses extensive assertion checking in debug builds. The SQLite documentation states they have over 50,000 assert statements in the codebase.
Linux Kernel: Uses BUG_ON() and WARN_ON() macros that provide similar functionality with kernel-specific handling.
Redis: Uses redisAssert() which provides detailed output before aborting.
Professional Practice: Steve Maguire’s “Writing Solid Code” from Microsoft Press (1993) dedicates entire chapters to assertion-based programming, arguing that assertions should remain enabled in shipping code.
2.4 Common Misconceptions
Misconception 1: “Assertions are the same as error handling”
- Reality: Assertions detect bugs (programmer errors). Error handling manages expected failures (file not found, network timeout).
- An assertion should NEVER fail if the program is correct.
Misconception 2: “I should disable assertions in production for performance”
- Reality: This is debatable. If assertions guard against undefined behavior, disabling them may cause worse problems than a controlled crash.
- Some teams keep assertions in production (“crash early” philosophy).
Misconception 3: “Assertions with side effects are fine”
- Reality: NEVER put side effects in assertions.
assert(ptr = malloc(100))will not allocate in release builds!
3. Project Specification
3.1 What You Will Build
A complete assertion module consisting of:
- A header file (
assert.h) that defines your assertion macros - An implementation file (
assert.c) with the failure handler - Test programs demonstrating assertion failures with clear diagnostics
- Integration tests showing how assertions catch bugs immediately
3.2 Functional Requirements
| Requirement | Description |
|---|---|
| Basic assertion | assert(expr) - check boolean expression |
| Formatted assertion | assertf(expr, fmt, ...) - check with printf-style message |
| Source location | Include file name, line number, function name |
| Expression stringification | Print the failed expression as text |
| Debugger integration | Easy to set breakpoints on failure |
| NDEBUG support | Disable assertions when NDEBUG is defined |
3.3 Non-Functional Requirements
| Requirement | Description |
|---|---|
| Portability | Work with GCC, Clang, and standard C11 |
| Performance | Zero overhead when disabled (NDEBUG) |
| Macro hygiene | No variable name conflicts with user code |
| Single evaluation | Expression evaluated exactly once |
3.4 Example Usage / Output
# 1. Compile a test program with assertions enabled (default)
$ gcc -std=c11 -g -o test_assert test_assert.c assert.c
# 2. Run a program that violates a simple assertion
$ ./test_assert null_ptr
Assertion failed: ptr != NULL
Expression: ptr != NULL
File: test_assert.c
Line: 42
Function: process_data
Aborted (core dumped)
# 3. Run with a formatted message assertion
$ ./test_assert bounds
Assertion failed: index < size
Expression: index < size
File: test_assert.c
Line: 67
Function: get_element
Message: Array index 100 out of bounds (array size is 50)
Aborted (core dumped)
# 4. The stack trace in GDB shows exactly where the bug is
$ gdb ./test_assert core
(gdb) bt
#0 __GI_abort () at abort.c:79
#1 0x0000555555555234 in Assert_fail (expr=0x555555556012 "ptr != NULL",
file=0x555555556004 "test_assert.c", line=42,
func=0x555555556020 "process_data", ...) at assert.c:28
#2 0x00005555555552a1 in process_data (data=0x0, n=10)
at test_assert.c:42
#3 0x0000555555555312 in main () at test_assert.c:58
# 5. Compile with NDEBUG to disable assertions for release
$ gcc -std=c11 -DNDEBUG -O3 -o test_release test_assert.c assert.c
$ ./test_release null_ptr
# (runs without assertion check - undefined behavior ensues)
# This is why NDEBUG is dangerous if assertions guard against UB!
# 6. Demonstrate the assert vs assert with message
$ ./test_assert demo
Testing simple assert: assert(x > 0)
Testing message assert: assert(x > 0, "x must be positive, got %d", x)
# 7. Compare with standard assert (less informative)
$ gcc -std=c11 -o test_standard test_standard_assert.c
$ ./test_standard
test_standard_assert.c:42: process_data: Assertion `ptr != NULL' failed.
Aborted (core dumped)
# Notice: No custom message, no function context, harder to debug
# 8. Show assertion preventing silent corruption
$ ./memory_test without_assert
# Program corrupts memory, crashes 1000 lines later in unrelated code
$ ./memory_test with_assert
Assertion failed: nbytes > 0
Expression: nbytes > 0
File: memory_test.c
Line: 15
Function: safe_alloc
Message: Cannot allocate non-positive bytes: -42
Aborted (core dumped)
# Bug caught at source, not at symptom
4. Solution Architecture
4.1 High-Level Design
┌─────────────────────────────────────────────────────────────────────────────┐
│ ASSERT MODULE ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ assert.h (Interface) │
│ ──────────────────── │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ #ifndef ASSERT_INCLUDED │ │
│ │ #define ASSERT_INCLUDED │ │
│ │ │ │
│ │ #undef assert /* Override standard assert if included */ │ │
│ │ │ │
│ │ #ifdef NDEBUG │ │
│ │ #define assert(e) ((void)0) │ │
│ │ #else │ │
│ │ extern void Assert_fail(const char *expr, │ │
│ │ const char *file, │ │
│ │ int line, │ │
│ │ const char *func, │ │
│ │ ...); │ │
│ │ │ │
│ │ #define assert(e) ((void)((e) || \ │ │
│ │ (Assert_fail(#e, __FILE__, __LINE__, __func__), 0))) │ │
│ │ #endif │ │
│ │ │ │
│ │ #endif │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ assert.c (Implementation) │
│ ───────────────────────── │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ #include <stdarg.h> │ │
│ │ #include <stdio.h> │ │
│ │ #include <stdlib.h> │ │
│ │ #include "assert.h" │ │
│ │ │ │
│ │ void Assert_fail(const char *expr, const char *file, │ │
│ │ int line, const char *func, ...) { │ │
│ │ // Print diagnostic information │ │
│ │ // Handle optional format string + args │ │
│ │ // Flush output │ │
│ │ // Call abort() │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Macro Expansion Flow: │
│ ──────────────────── │
│ │
│ User code: assert(ptr != NULL) │
│ │ │
│ ▼ │
│ After expansion: ((void)((ptr != NULL) || │
│ (Assert_fail("ptr != NULL", │
│ "user.c", 42, "my_func"), 0))) │
│ │ │
│ ▼ │
│ If ptr is NULL: Assert_fail is called → prints info → abort() │
│ If ptr is valid: Right side never evaluated (short-circuit) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
4.2 Key Components
| Component | File | Purpose |
|---|---|---|
| Header guard | assert.h | Prevent multiple inclusion |
| assert macro | assert.h | Main assertion macro |
| assertf macro (optional) | assert.h | Assertion with formatted message |
| Assert_fail function | assert.c | Print diagnostics and abort |
| NDEBUG handling | assert.h | Conditional compilation for release |
4.3 Data Structures
This module doesn’t require complex data structures. The key “data” is:
- Source location information: Captured by preprocessor macros
- Expression string: Created by
#stringification operator - Optional message: Variadic arguments for printf-style formatting
4.4 Algorithm Overview
Macro Evaluation (using short-circuit OR):
assert(e) → ((void)((e) || (Assert_fail(#e, __FILE__, __LINE__, __func__), 0)))
Step 1: Evaluate (e)
Step 2: If (e) is true (non-zero), short-circuit - done
Step 3: If (e) is false (zero), evaluate right side
Step 4: Assert_fail is called (prints and aborts)
Step 5: The ", 0" ensures the expression has type int
Step 6: The (void) cast silences "unused value" warnings
Assert_fail Function:
1. Print "Assertion failed: <expression>"
2. Print " File: <file>"
3. Print " Line: <line>"
4. Print " Function: <function>"
5. If additional arguments provided:
a. Treat first arg as format string
b. Print " Message: " followed by formatted output
6. Flush stderr (ensure message visible before abort)
7. Call abort()
5. Implementation Guide
5.1 Development Environment Setup
Required Tools:
□ C Compiler: GCC (gcc) or Clang (clang)
- Verify: gcc --version OR clang --version
- Need: C11 support minimum (-std=c11)
□ Debugger: GDB (Linux) or LLDB (macOS)
- Essential for verifying assertion behavior
□ Text Editor or IDE with C support
Recommended Compiler Flags:
CFLAGS = -Wall -Wextra -Wpedantic -std=c11 -g
# For development with extra checks:
CFLAGS += -fsanitize=address -fsanitize=undefined
# For release (assertions disabled):
CFLAGS_RELEASE = -O3 -DNDEBUG
5.2 Project Structure
assert_project/
├── include/
│ └── assert.h # Interface (macros and declarations)
├── src/
│ └── assert.c # Implementation (Assert_fail function)
├── tests/
│ ├── test_basic.c # Test basic assertions
│ ├── test_format.c # Test formatted messages
│ └── test_ndebug.c # Test NDEBUG behavior
├── Makefile
└── README.md
5.3 The Core Question You’re Answering
“How can I catch bugs at the moment they occur, with enough context to fix them immediately?”
Before you write any code, understand that assertions are not error handling - they’re bug detection. An assertion failure means the program has a bug that needs to be fixed, not an error condition that needs to be handled at runtime.
The distinction is crucial:
- Errors are expected: File not found, network timeout, user input invalid. These should be handled gracefully - return error codes, throw exceptions, prompt for retry.
- Bugs are not expected: Null pointer dereference, negative array size, violated invariant. These indicate a programmer mistake. The correct response is to crash immediately with maximum diagnostic information.
When you see assert(table != NULL) at the start of Table_put(), it means “if table is NULL here, my program is already broken and continuing would only make things worse.”
5.4 Concepts You Must Understand First
Stop and research these before coding:
- Variadic Macros
- What is
__VA_ARGS__and how does it work? - What is
__VA_OPT__(C23) and why was it added? - How do you write a macro that takes a variable number of arguments?
- What is the problem with empty
__VA_ARGS__and trailing commas? - Book Reference: “21st Century C” by Klemens - Ch. 10
- What is
- Preprocessor Source Location Macros
- What does
__FILE__expand to? - What does
__LINE__expand to? - What does
__func__expand to? (Note: it’s not technically a macro!) - Why must these be captured in the macro, not the function?
- What happens if you pass
__LINE__to a function? - Book Reference: “C Interfaces and Implementations” by Hanson - Ch. 4
- What does
- The abort() Function
- What does
abort()do differently fromexit()? - Why does assertion failure call
abort()instead ofexit(1)? - How does
abort()interact with debuggers? - What signals does
abort()raise? - Book Reference: “Expert C Programming” by van der Linden - Ch. 9
- What does
- NDEBUG Convention
- What is the NDEBUG macro and who defines it?
- Why do release builds typically disable assertions?
- What are the risks of disabled assertions?
- Should assertions check for conditions that could occur in valid usage?
- Book Reference: “C Interfaces and Implementations” by Hanson - Ch. 4
5.5 Questions to Guide Your Design
Before implementing, think through these:
- Macro Interface
- Should your assert take just an expression, or also a message?
- How will you handle assertions with formatted messages (like printf)?
- Should you have two macros:
assert(expr)andassertf(expr, fmt, ...)? - What should the macro expand to when NDEBUG is defined?
- Failure Behavior
- What information should be printed on failure?
- Should output go to stdout or stderr?
- Should you flush output before aborting?
- Should you call a user-definable hook before aborting?
- Integration with Tools
- How can you make it easier to set breakpoints on assertion failures?
- Should you support a “soft assert” that logs but doesn’t abort?
- How will your assert work with sanitizers (ASan, UBSan)?
- Should assertions be able to raise Except_T exceptions?
- Defensive Considerations
- What if the format string in an assertion message is NULL?
- What if an assertion is in a signal handler?
- What about assertions in library code vs application code?
5.6 Thinking Exercise
Trace Through an Assertion Failure
Before coding, trace what happens when this code runs:
#define assert(e) \
((void)((e) || (Assert_fail(#e, __FILE__, __LINE__, __func__), 0)))
void process_data(int *data, int n) {
assert(data != NULL);
assert(n > 0);
// ... process data ...
}
int main(void) {
process_data(NULL, 10); // Bug: passing NULL
return 0;
}
Questions while tracing:
- What does
#eexpand to? (Hint: stringification operator) - When is
Assert_failcalled? (Hint: short-circuit evaluation) - Why is there a
, 0at the end of the macro? - What type does the whole expression have? (Hint:
(void)cast) - If
Assert_failreturns, what happens? (It shouldn’t, but what if?)
Now add a formatted message:
#define assertf(e, ...) \
((void)((e) || (Assert_fail(#e, __FILE__, __LINE__, __func__, __VA_ARGS__), 0)))
void get_element(int *array, int size, int index) {
assertf(index >= 0 && index < size,
"Index %d out of bounds [0, %d)", index, size);
return array[index];
}
Additional questions:
- What does
Assert_failneed to do with__VA_ARGS__? - What if someone calls
assertf(x > 0)with no message? - How does
__VA_OPT__solve the trailing comma problem?
5.7 Hints in Layers
Hint 1: Start with the Simplest Version
Don’t worry about formatted messages at first. Implement a basic assert that just prints the expression and aborts:
// Pseudocode - not actual implementation
define assert(expression)
if expression is false
print "Assertion failed:", stringified expression
print "File:", current file, "Line:", current line
call abort()
// The key insight: use short-circuit evaluation
// (expr) || (fail(), 0)
// If expr is true, right side never evaluated
// If expr is false, fail() is called, then 0 returned (but fail never returns)
Hint 2: Add the Failure Handler Function
Separating the failure handling into a function has benefits:
- Keeps the macro small (better debugging)
- Allows setting breakpoints on the function
- Enables variadic arguments for messages
- Can be marked
noreturnfor better optimization
// Pseudocode structure
function Assert_fail(expression_string, file, line, function_name)
print formatted failure message to stderr
flush stderr (ensure message is visible before abort)
call abort()
// Mark as noreturn for compiler optimization
__attribute__((noreturn))
void Assert_fail(...);
// Macro calls the function
define assert(expression)
if not expression
call Assert_fail with stringified expression and location
Hint 3: Add Formatted Messages with Variadic Macros
The tricky part is handling the optional message. Consider two approaches:
// Approach 1: Two macros
#define assert(e) assert_impl(e, #e, __FILE__, __LINE__, __func__, NULL)
#define assertf(e, fmt, ...) assert_impl(e, #e, __FILE__, __LINE__, __func__, fmt, __VA_ARGS__)
// Approach 2: Single macro with __VA_OPT__ (C23)
#define assert(e, ...) \
((void)((e) || (Assert_fail(#e, __FILE__, __LINE__, __func__ \
__VA_OPT__(,) __VA_ARGS__), 0)))
// Approach 3: GNU extension ##__VA_ARGS__
#define assert(e, ...) \
((void)((e) || (Assert_fail(#e, __FILE__, __LINE__, __func__, ##__VA_ARGS__), 0)))
Hint 4: Handle NDEBUG Correctly
// Pseudocode for conditional compilation
#ifdef NDEBUG
// Option A: Expand to nothing (expression not evaluated)
#define assert(e, ...) ((void)0)
// Option B: Evaluate but don't check (for side effects)
#define assert(e, ...) ((void)(e))
#else
#define assert(e, ...) /* full version */
#endif
// IMPORTANT DESIGN DECISION:
// If the expression has side effects, Option A changes program behavior!
// Example: assert(ptr = malloc(100))
// With Option A: malloc never called in release
// With Option B: malloc called, but result not checked
// Best practice: NEVER put side effects in assertions
5.8 The Interview Questions They’ll Ask
Prepare to answer these:
-
“What’s the difference between assertions and error handling? Give an example of when you’d use each.”
Strong answer should include: Assertions are for programmer errors (bugs), not runtime errors. An assertion should never fail if the program is correct.
assert(ptr != NULL)catches bugs;if (file == NULL) return ERROR;handles expected failures. -
“Why do release builds typically disable assertions? What are the tradeoffs?”
Strong answer should include: Performance (no check overhead), binary size, but risks: if assertions guard against UB, disabling them is dangerous. Some teams keep assertions in production (“crash early” philosophy). Microsoft’s “Writing Solid Code” recommends this.
-
“How would you implement an assertion macro that prints the failed expression and source location?”
Strong answer should include: Use
#efor stringification,__FILE__,__LINE__,__func__. Call a noreturn function. Use short-circuit evaluation:(e) || (fail(), 0). Handle variadic messages with__VA_ARGS__. -
“Explain a bug you caught using assertions. How did the assertion help you find it quickly?”
Strong answer should include: Specific example showing how early detection (at the assertion) saved debugging time vs finding the symptom later. Emphasize the value of the diagnostic information.
-
“Some teams use assertions in production. What are the arguments for and against this?”
Strong answer should include: For: crash early vs corrupt data, security (some UB is exploitable), debuggability. Against: performance, denial-of-service if assertions can be triggered by input, user experience.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Assert design | “C Interfaces and Implementations” by Hanson | Ch. 4 |
| Macro techniques | “21st Century C” by Klemens | Ch. 10 |
| Defensive programming | “Writing Solid Code” by Maguire | Ch. 2-3 |
| Debugging philosophy | “The Practice of Programming” by Kernighan & Pike | Ch. 5 |
| Preprocessor deep dive | “Expert C Programming” by van der Linden | Ch. 8 |
5.10 Implementation Phases
Phase 1: Basic Assert (Day 1 Morning)
- Create
assert.hwith header guard - Define basic
assert(e)macro using short-circuit evaluation - Implement
Assert_fail()function that prints and aborts - Test with simple cases
Phase 2: Enhanced Diagnostics (Day 1 Afternoon)
- Add function name (
__func__) to output - Format output nicely (aligned, clear)
- Add
fflush(stderr)before abort - Test with GDB to verify breakpoint capability
Phase 3: Formatted Messages (Day 2 Morning)
- Add variadic support to
Assert_fail() - Create
assertf(e, fmt, ...)or unified macro - Handle the empty
__VA_ARGS__case - Test with various message formats
Phase 4: Polish and NDEBUG (Day 2 Afternoon)
- Implement NDEBUG conditional compilation
- Add
__attribute__((noreturn))for optimization - Create comprehensive test suite
- Document the module
5.11 Key Implementation Decisions
| Decision | Options | Recommendation |
|---|---|---|
| Macro style | Two macros vs unified | Start with two (assert + assertf) for clarity |
| NDEBUG behavior | ((void)0) vs ((void)(e)) |
((void)0) - never evaluate expression |
| Output destination | stdout vs stderr | stderr (unbuffered, appropriate for errors) |
| Function attribute | noreturn | Yes, helps compiler optimize |
| Override standard | #undef assert |
Yes, for drop-in replacement |
6. Testing Strategy
6.1 Unit Tests
Test 1: Basic assertion passes
void test_passing_assert(void) {
int x = 5;
assert(x > 0); // Should not abort
printf("PASS: Basic assertion passed\n");
}
Test 2: Basic assertion fails
void test_failing_assert(void) {
int x = -1;
assert(x > 0); // Should abort with message
// Never reached
}
Test 3: Formatted assertion
void test_formatted_assert(void) {
int index = 100, size = 50;
assertf(index < size, "Index %d out of bounds (size=%d)", index, size);
}
Test 4: Expression evaluated once
int counter = 0;
int increment(void) { return ++counter; }
void test_single_evaluation(void) {
assert(increment() == 1);
assert(counter == 1); // Would be 2 if evaluated twice
}
6.2 Integration Tests
Test with GDB:
$ gdb ./test_failing
(gdb) break Assert_fail
(gdb) run
# Should break at Assert_fail with full context
(gdb) bt
# Verify backtrace shows calling location
Test NDEBUG behavior:
$ gcc -DNDEBUG -o test_ndebug test_ndebug.c assert.c
$ ./test_ndebug
# Assertions should not execute
6.3 Edge Cases
- Empty expression:
assert() - Expression with commas:
assert((a, b)) - Expression with string literals:
assert(strcmp(s, "test") == 0) - Assertion in macro:
#define CHECK(x) do { assert(x); use(x); } while(0)
7. Common Pitfalls & Debugging
Problem 1: “My assert doesn’t show the right line number”
- Why: You’re calling a function that then uses
__LINE__, which gives the function’s line, not the caller’s - Debug:
__LINE__is expanded at the point the macro is used. If you pass it to another macro, it expands there. - Fix: The macro must expand
__LINE__and pass the value to the failure function
Problem 2: “The expression is evaluated twice”
- Why: Your macro uses the expression in both the condition and the message
- Debug:
assert(x++)increments twice - Fix: Use short-circuit evaluation:
((e) || (fail(#e, ...), 0))evaluateseexactly once
Problem 3: “My formatted message doesn’t work with no arguments”
- Why:
__VA_ARGS__might be empty, leaving a trailing comma - Debug:
assert(x > 0)might expand toAssert_fail(..., ) - Fix: Use
__VA_OPT__(,) __VA_ARGS__in C23, or GNU’s##__VA_ARGS__, or provide two macro versions
Problem 4: “The debugger doesn’t stop at my assertion”
- Why: The debugger needs a breakable function, not just a macro
- Debug: Set breakpoint with
break Assert_failin GDB - Fix: Make sure
Assert_failis a real function (not inlined) in debug builds
Problem 5: “Assertion fires in library code but I want to handle it”
- Why: Assertions should not be used for recoverable errors
- Debug: Consider if the condition is truly a bug or a handleable error
- Fix: If it’s handleable, use the Except module instead (see Project 5)
8. Extensions & Challenges
After completing the basic implementation, try these extensions:
8.1 Soft Assertions
Create a soft_assert that logs but doesn’t abort - useful for non-critical invariants:
soft_assert(cache_valid); // Log warning if false, but continue
8.2 Assertion Hooks
Allow users to register a callback before abort:
void my_handler(const char *expr, const char *file, int line) {
log_to_file(expr, file, line);
}
Assert_set_handler(my_handler);
8.3 Compile-Time Assertions
Implement static_assert for C99/C11:
#define static_assert(e, msg) \
typedef char _static_assert_##__LINE__[(e) ? 1 : -1]
8.4 Rich Context
Capture additional context like thread ID, timestamp, or stack trace:
Assertion failed: ptr != NULL
Expression: ptr != NULL
File: main.c:42
Function: process()
Thread: 0x7f8a4000
Time: 2024-01-15 10:23:45.123
9. Real-World Connections
SQLite
SQLite has over 50,000 assertions in their codebase. They use assert() extensively for:
- Checking database invariants
- Validating internal state
- Ensuring correct API usage The SQLite documentation discusses their assertion philosophy in detail.
Linux Kernel
The kernel uses BUG_ON() and WARN_ON() macros:
BUG_ON(ptr == NULL); // Kernel panic if condition is true
WARN_ON(count > MAX); // Warning but continues
Redis
Redis implements redisAssert() that provides detailed output:
void redisAssert(char *estr, char *file, int line) {
bugReportStart();
serverLog(LL_WARNING,"=== ASSERTION FAILED ===");
serverLog(LL_WARNING,"==> %s:%d '%s' is not true",file,line,estr);
// ... additional diagnostics ...
}
10. Resources
Official CII Source Code
- GitHub: https://github.com/drh/cii
- File:
src/assert.candinclude/assert.h
Books
- “C Interfaces and Implementations” by David Hanson - Chapter 4
- “Writing Solid Code” by Steve Maguire - Chapters 2-3
- “21st Century C” by Ben Klemens - Chapter 10 (Macros)
- “The Practice of Programming” by Kernighan & Pike - Chapter 5
Online Resources
- Hanson’s Princeton page: https://www.cs.princeton.edu/~drh/
- GCC Preprocessor documentation
- C11 standard section on
assert.h
11. Self-Assessment Checklist
Before moving on, verify you can:
- Explain why assertions crash rather than return error codes
- Implement a macro that evaluates its argument exactly once
- Use
#estringification to print the failed expression - Capture source location with
__FILE__,__LINE__,__func__ - Handle variadic arguments with
__VA_ARGS__ - Implement NDEBUG conditional compilation
- Set a breakpoint on assertion failure in GDB/LLDB
- Distinguish between assertions (bugs) and error handling (expected failures)
- Explain the risks of disabling assertions in production
- Design assertions that provide useful diagnostic information
12. Submission / Completion Criteria
Your Assert module is complete when:
- Functionality
assert(e)works for simple expressions- Formatted messages work with
assertf(e, fmt, ...) - Output includes file, line, function, and expression
- NDEBUG disables assertions completely
- Quality
- No compiler warnings with
-Wall -Wextra -Wpedantic - Valgrind reports no memory issues
- Works with both GCC and Clang
- No compiler warnings with
- Testing
- Tests for passing assertions
- Tests for failing assertions (verify output)
- Tests for single evaluation
- Tests for NDEBUG behavior
- Documentation
- Header file documents usage
- Comments explain key design decisions
- README explains how to build and test
Congratulations! You’ve built the foundation that all other CII modules depend on. The Assert module establishes the fail-fast philosophy that makes debugging tractable.