summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-07 11:24:56 +0100
committerskal <pascal.massimino@gmail.com>2026-02-07 11:24:56 +0100
commitb84d42f5f9128d5d8f06011c97c5a303b09b00e8 (patch)
tree823b55ccf25ed3ecda2275a7998448a95a75a49a /src
parent13e41ff17ba91c07197e318b3235373aef845023 (diff)
feat(build): Add FINAL_STRIP mode for maximum size optimization
Implemented systematic fatal error checking infrastructure that can be stripped for final builds. This addresses the need to remove all error checking (abort() calls) from the production binary while maintaining safety during development. ## New Infrastructure ### 1. CMake Option: DEMO_FINAL_STRIP - New build mode for absolute minimum binary size - Implies DEMO_STRIP_ALL (stricter superset) - NOT included in DEMO_ALL_OPTIONS (manual opt-in only) - Message printed during configuration ### 2. Header: src/util/fatal_error.h - Systematic macro-based error checking - Zero cost when FINAL_STRIP enabled (compiles to ((void)0)) - Full error messages with file:line info when enabled - Five macros for different use cases: - FATAL_CHECK(cond, msg, ...): Conditional checks (most common) - FATAL_ERROR(msg, ...): Unconditional errors - FATAL_UNREACHABLE(): Unreachable code markers - FATAL_ASSERT(cond): Assertion-style invariants - FATAL_CODE_BEGIN/END: Complex validation blocks ### 3. CMake Target: make final - Convenience target for triggering final build - Reconfigures with FINAL_STRIP and rebuilds demo64k - Only available when NOT in FINAL_STRIP mode (prevents recursion) ### 4. Script: scripts/build_final.sh - Automated final build workflow - Creates build_final/ directory - Shows size comparison with STRIP_ALL build (if available) - Comprehensive warnings about stripped error checking ## Build Mode Hierarchy | Mode | Error Checks | Debug Features | Size Opt | |-------------|--------------|----------------|----------| | Debug | ✅ | ✅ | ❌ | | STRIP_ALL | ✅ | ❌ | ✅ | | FINAL_STRIP | ❌ | ❌ | ✅✅ | ## Design Decisions (All Agreed Upon) 1. **FILE:LINE Info**: ✅ Include (worth 200 bytes for debugging) 2. **ALL_OPTIONS**: ❌ Manual opt-in only (too dangerous for testing) 3. **FATAL_ASSERT**: ✅ Add macro (semantic clarity for invariants) 4. **Strip Hierarchy**: ✅ STRIP_ALL keeps checks, FINAL_STRIP removes all 5. **Naming**: ✅ FATAL_* prefix (clear intent, conventional) ## Size Impact Current: 10 abort() calls in production code - ring_buffer.cc: 7 checks (~350 bytes) - miniaudio_backend.cc: 3 checks (~240 bytes) Estimated savings with FINAL_STRIP: ~500-600 bytes ## Documentation Updated: - doc/HOWTO.md: Added FINAL_STRIP build instructions - doc/CONTRIBUTING.md: Added fatal error checking guidelines - src/util/fatal_error.h: Comprehensive usage documentation ## Next Steps (Not in This Commit) Phase 2: Convert ring_buffer.cc abort() calls to FATAL_CHECK() Phase 3: Convert miniaudio_backend.cc abort() calls to FATAL_CHECK() Phase 4: Systematic scan for remaining abort() calls Phase 5: Verify size reduction with actual measurements ## Usage # Convenience methods make final # From normal build directory ./scripts/build_final.sh # Creates build_final/ # Manual cmake -S . -B build_final -DDEMO_FINAL_STRIP=ON cmake --build build_final ⚠️ WARNING: FINAL_STRIP builds have NO error checking. Use ONLY for final release, never for development/testing. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/util/fatal_error.h180
1 files changed, 180 insertions, 0 deletions
diff --git a/src/util/fatal_error.h b/src/util/fatal_error.h
new file mode 100644
index 0000000..ea4d168
--- /dev/null
+++ b/src/util/fatal_error.h
@@ -0,0 +1,180 @@
+// This file is part of the 64k demo project.
+// Provides fatal error checking macros that can be stripped for final builds.
+// These macros compile to nothing when FINAL_STRIP is enabled.
+
+#pragma once
+
+#include <cstdio>
+#include <cstdlib>
+
+// Build Mode Hierarchy:
+// - Debug: Full error checking with messages and file:line info
+// - STRIP_ALL: Full error checking (same as Debug)
+// - FINAL_STRIP: NO error checking (all macros compile to nothing)
+//
+// FINAL_STRIP implies STRIP_ALL (stricter superset)
+
+#if defined(FINAL_STRIP)
+
+// ==============================================================================
+// FINAL_STRIP: All error checking compiled out (zero runtime cost)
+// ==============================================================================
+
+// Conditional fatal error check - compiles to nothing
+#define FATAL_CHECK(cond, ...) ((void)0)
+
+// Unconditional fatal error - compiles to nothing
+#define FATAL_ERROR(...) ((void)0)
+
+// Unreachable code marker - compiles to nothing
+#define FATAL_UNREACHABLE() ((void)0)
+
+// Assertion-style check (for documenting invariants) - compiles to nothing
+#define FATAL_ASSERT(cond) ((void)0)
+
+// Block-based error checking - code inside is stripped entirely
+#define FATAL_CODE_BEGIN if (0) {
+#define FATAL_CODE_END }
+
+#else
+
+// ==============================================================================
+// Debug / STRIP_ALL: Full error checking enabled
+// ==============================================================================
+
+// Conditional fatal error check with formatted message
+// Usage: FATAL_CHECK(x >= max, "x out of bounds: %d >= %d\n", x, max);
+//
+// If condition is TRUE (error detected):
+// - Prints "FATAL: <message> [file.cc:line]" to stderr
+// - Calls abort()
+//
+// Example output:
+// FATAL: write_pos out of bounds: write=100 >= capacity=50 [ring_buffer.cc:57]
+#define FATAL_CHECK(cond, ...) \
+ do { \
+ if (cond) { \
+ fprintf(stderr, "FATAL: " __VA_ARGS__); \
+ fprintf(stderr, " [%s:%d]\n", __FILE__, __LINE__); \
+ abort(); \
+ } \
+ } while (0)
+
+// Unconditional fatal error (for impossible code paths)
+// Usage: FATAL_ERROR("Should never reach here!");
+//
+// Always prints message and aborts.
+//
+// Example output:
+// FATAL: Should never reach here! [main.cc:123]
+#define FATAL_ERROR(...) \
+ do { \
+ fprintf(stderr, "FATAL: " __VA_ARGS__); \
+ fprintf(stderr, " [%s:%d]\n", __FILE__, __LINE__); \
+ abort(); \
+ } while (0)
+
+// Unreachable code marker (for switch default cases, impossible branches)
+// Usage:
+// switch (type) {
+// case A: return handle_a();
+// case B: return handle_b();
+// default: FATAL_UNREACHABLE();
+// }
+#define FATAL_UNREACHABLE() FATAL_ERROR("Unreachable code executed")
+
+// Assertion-style check (for documenting invariants)
+// Usage: FATAL_ASSERT(buffer != nullptr);
+//
+// If condition is FALSE (assertion failed):
+// - Prints "FATAL: Assertion failed: <condition> [file.cc:line]"
+// - Calls abort()
+//
+// Example output:
+// FATAL: Assertion failed: buffer != nullptr [audio.cc:42]
+#define FATAL_ASSERT(cond) \
+ do { \
+ if (!(cond)) { \
+ fprintf(stderr, "FATAL: Assertion failed: %s [%s:%d]\n", #cond, \
+ __FILE__, __LINE__); \
+ abort(); \
+ } \
+ } while (0)
+
+// Block-based error checking (for complex validation logic)
+// Usage:
+// FATAL_CODE_BEGIN
+// static int callback_depth = 0;
+// ++callback_depth;
+// if (callback_depth > 1) {
+// FATAL_ERROR("Callback re-entered! depth=%d", callback_depth);
+// }
+// FATAL_CODE_END
+//
+// The entire block is stripped when FINAL_STRIP is enabled.
+#define FATAL_CODE_BEGIN
+#define FATAL_CODE_END
+
+#endif /* defined(FINAL_STRIP) */
+
+// ==============================================================================
+// Usage Guidelines
+// ==============================================================================
+//
+// When to use each macro:
+//
+// FATAL_CHECK(cond, msg, ...):
+// - Most common use case (90% of error checks)
+// - Bounds checking: FATAL_CHECK(idx >= size, "Index %d >= %d\n", idx, size)
+// - Null checking: FATAL_CHECK(ptr == nullptr, "Null pointer: %s\n", name)
+// - Range validation: FATAL_CHECK(x < min || x > max, "Out of range\n")
+//
+// FATAL_ERROR(msg, ...):
+// - Unconditional errors (should-never-happen cases)
+// - Example: FATAL_ERROR("Invalid state: %d\n", state)
+//
+// FATAL_UNREACHABLE():
+// - Switch default cases that should never be reached
+// - Impossible code paths after exhaustive checks
+// - Example: switch (enum) { ... default: FATAL_UNREACHABLE(); }
+//
+// FATAL_ASSERT(cond):
+// - Documenting invariants (not error handling)
+// - Pre/post conditions
+// - Example: FATAL_ASSERT(count > 0 && count < MAX_COUNT)
+//
+// FATAL_CODE_BEGIN/END:
+// - Complex validation logic that cannot be expressed as single condition
+// - Temporary state tracking for validation
+// - Example: Callback re-entry detection with static counter
+//
+// ==============================================================================
+// Size Impact
+// ==============================================================================
+//
+// Debug / STRIP_ALL builds:
+// - Each FATAL_CHECK: ~50-80 bytes (depends on message length)
+// - Includes fprintf() call, error message string, file:line info
+//
+// FINAL_STRIP builds:
+// - All macros: 0 bytes (compiled out entirely)
+// - Estimated savings: ~500-600 bytes for typical project
+//
+// ==============================================================================
+// Testing Strategy
+// ==============================================================================
+//
+// Always test both modes before committing:
+//
+// 1. Normal build (error checking enabled):
+// cmake -S . -B build
+// cmake --build build && cd build && ctest
+//
+// 2. FINAL_STRIP build (error checking stripped):
+// cmake -S . -B build_final -DDEMO_FINAL_STRIP=ON
+// cmake --build build_final
+//
+// Or use convenience commands:
+// make final (CMake target)
+// ./scripts/build_final.sh (Script)
+//