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:

  1. Document preconditions - The code itself declares what must be true
  2. Catch bugs at the source - Not three functions and 1000 lines later
  3. Provide diagnostic information - Where, what, and why
  4. 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:

  1. A header file (assert.h) that defines your assertion macros
  2. An implementation file (assert.c) with the failure handler
  3. Test programs demonstrating assertion failures with clear diagnostics
  4. 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:

  1. Source location information: Captured by preprocessor macros
  2. Expression string: Created by # stringification operator
  3. 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:

  1. 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
  2. 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
  3. The abort() Function
    • What does abort() do differently from exit()?
    • Why does assertion failure call abort() instead of exit(1)?
    • How does abort() interact with debuggers?
    • What signals does abort() raise?
    • Book Reference: “Expert C Programming” by van der Linden - Ch. 9
  4. 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:

  1. 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) and assertf(expr, fmt, ...)?
    • What should the macro expand to when NDEBUG is defined?
  2. 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?
  3. 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?
  4. 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 #e expand to? (Hint: stringification operator)
  • When is Assert_fail called? (Hint: short-circuit evaluation)
  • Why is there a , 0 at the end of the macro?
  • What type does the whole expression have? (Hint: (void) cast)
  • If Assert_fail returns, 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_fail need 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 noreturn for 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:

  1. “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.

  2. “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.

  3. “How would you implement an assertion macro that prints the failed expression and source location?”

    Strong answer should include: Use #e for stringification, __FILE__, __LINE__, __func__. Call a noreturn function. Use short-circuit evaluation: (e) || (fail(), 0). Handle variadic messages with __VA_ARGS__.

  4. “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.

  5. “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)

  1. Create assert.h with header guard
  2. Define basic assert(e) macro using short-circuit evaluation
  3. Implement Assert_fail() function that prints and aborts
  4. Test with simple cases

Phase 2: Enhanced Diagnostics (Day 1 Afternoon)

  1. Add function name (__func__) to output
  2. Format output nicely (aligned, clear)
  3. Add fflush(stderr) before abort
  4. Test with GDB to verify breakpoint capability

Phase 3: Formatted Messages (Day 2 Morning)

  1. Add variadic support to Assert_fail()
  2. Create assertf(e, fmt, ...) or unified macro
  3. Handle the empty __VA_ARGS__ case
  4. Test with various message formats

Phase 4: Polish and NDEBUG (Day 2 Afternoon)

  1. Implement NDEBUG conditional compilation
  2. Add __attribute__((noreturn)) for optimization
  3. Create comprehensive test suite
  4. 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)) evaluates e exactly 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 to Assert_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_fail in GDB
  • Fix: Make sure Assert_fail is 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.c and include/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 #e stringification 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:

  1. 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
  2. Quality
    • No compiler warnings with -Wall -Wextra -Wpedantic
    • Valgrind reports no memory issues
    • Works with both GCC and Clang
  3. Testing
    • Tests for passing assertions
    • Tests for failing assertions (verify output)
    • Tests for single evaluation
    • Tests for NDEBUG behavior
  4. 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.