diff options
| -rw-r--r-- | CMakeLists.txt | 34 | ||||
| -rw-r--r-- | doc/CONTRIBUTING.md | 76 | ||||
| -rw-r--r-- | doc/HOWTO.md | 24 | ||||
| -rwxr-xr-x | scripts/build_final.sh | 71 | ||||
| -rw-r--r-- | src/util/fatal_error.h | 180 |
5 files changed, 384 insertions, 1 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index ef072ec..7000d0a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,9 +4,10 @@ project(demo64k LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -#-- - Configuration Options -- - +#-- - Configuration Options -- - option(DEMO_SIZE_OPT "Enable size optimization flags" OFF) option(DEMO_STRIP_ALL "Strip all unnecessary code for final build" OFF) +option(DEMO_FINAL_STRIP "Strip ALL error checking for final-final build" OFF) option(DEMO_BUILD_TESTS "Build tests" OFF) option(DEMO_BUILD_TOOLS "Build tools" OFF) option(DEMO_ENABLE_COVERAGE "Enable code coverage generation (macOS only)" OFF) @@ -18,6 +19,15 @@ if (DEMO_ALL_OPTIONS) set(DEMO_STRIP_ALL ON) set(DEMO_BUILD_TESTS ON) set(DEMO_BUILD_TOOLS ON) + # NOTE: DEMO_FINAL_STRIP is NOT included here (too dangerous for testing) +endif() + +# FINAL_STRIP: Most aggressive stripping (removes ALL error checking) +# Implies STRIP_ALL (stricter superset) +if (DEMO_FINAL_STRIP) + add_definitions(-DFINAL_STRIP) + set(DEMO_STRIP_ALL ON) + message(STATUS "FINAL_STRIP enabled - all error checking will be removed") endif() if (DEMO_STRIP_ALL) @@ -308,6 +318,28 @@ if (DEMO_SIZE_OPT) endif() # ============================================================================== +# Final Build Target (make final) +# ============================================================================== +# Convenience target for final production build with all stripping enabled +# Usage: make final +# This reconfigures CMake with FINAL_STRIP and rebuilds demo64k +# Note: Only create this target if we're NOT already in FINAL_STRIP mode + +if (NOT DEMO_FINAL_STRIP) + add_custom_target(final + COMMAND ${CMAKE_COMMAND} -E echo "Building FINAL production binary..." + COMMAND ${CMAKE_COMMAND} ${CMAKE_SOURCE_DIR} -DDEMO_FINAL_STRIP=ON -DDEMO_SIZE_OPT=ON + COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target demo64k -j8 + COMMAND ${CMAKE_COMMAND} -E echo "" + COMMAND ${CMAKE_COMMAND} -E echo "Final build complete!" + COMMAND ${CMAKE_COMMAND} -E echo "Binary: ${CMAKE_BINARY_DIR}/demo64k" + COMMAND ls -lh ${CMAKE_BINARY_DIR}/demo64k + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Building final production binary with FINAL_STRIP" + ) +endif() + +# ============================================================================== # Test Demo (Audio/Visual Sync Tool) # ============================================================================== diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md index ae8c35a..d7ea448 100644 --- a/doc/CONTRIBUTING.md +++ b/doc/CONTRIBUTING.md @@ -340,6 +340,82 @@ For tests that only need synth or tracker (not both), you can still call `synth_ These are performance-critical APIs and should be called directly, not wrapped by AudioEngine. +### Fatal Error Checking + +The project uses fatal error checking macros that can be stripped for final builds. These are defined in `src/util/fatal_error.h`. + +**When to use fatal error checks:** + +Use fatal error checking for conditions that indicate programming errors or corrupt state that cannot be recovered from: +- Bounds checking (array/buffer overflows) +- Null pointer checks +- Invariant violations +- Invalid state transitions + +**DO NOT use for:** +- Expected runtime errors (file not found, network errors) +- Recoverable conditions +- Input validation (use return codes instead) + +**Macro Reference:** + +```cpp +#include "util/fatal_error.h" + +// Conditional check (most common - 90% of cases) +FATAL_CHECK(write_pos >= capacity, + "write_pos out of bounds: %d >= %d\n", + write_pos, capacity); + +// Unconditional error (should-never-happen) +FATAL_ERROR("Invalid state: %d\n", state); + +// Unreachable code marker (switch defaults) +switch (type) { + case TYPE_A: return handle_a(); + case TYPE_B: return handle_b(); + default: FATAL_UNREACHABLE(); +} + +// Assertion-style (documenting invariants) +FATAL_ASSERT(buffer != nullptr); +FATAL_ASSERT(count > 0 && count < MAX_COUNT); + +// Complex validation blocks +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 +``` + +**Build modes:** +- **Debug/STRIP_ALL**: All checks enabled, full error messages with file:line info +- **FINAL_STRIP**: All checks compiled out (zero runtime cost, ~500-600 bytes saved) + +**Testing requirement:** + +Before committing code with fatal error checks, verify both modes compile: + +```bash +# Normal build (checks enabled) +cmake -S . -B build +cmake --build build && cd build && ctest + +# FINAL_STRIP build (checks stripped) +cmake -S . -B build_final -DDEMO_FINAL_STRIP=ON +cmake --build build_final +``` + +Or use the convenience script: +```bash +./scripts/build_final.sh +``` + +**Important:** Never use `FATAL_CHECK` for conditions that can legitimately occur during normal operation. These are for programming errors only. + ### Script Maintenance After Hierarchy Changes After any major source hierarchy change (moving, renaming, or reorganizing files), you **must** review and update all scripts in the `scripts/` directory to ensure they remain functional. diff --git a/doc/HOWTO.md b/doc/HOWTO.md index 36216fa..3a1aee2 100644 --- a/doc/HOWTO.md +++ b/doc/HOWTO.md @@ -46,6 +46,30 @@ cmake --build build ``` In this mode, the demo will always start in fullscreen. +### Final-Final Build (Maximum Stripping) + +For the absolute smallest binary (removing ALL error checking), use the `DEMO_FINAL_STRIP` option: + +```bash +# Method 1: CMake target (from normal build directory) +cd build +make final + +# Method 2: Convenience script (creates build_final/ directory) +./scripts/build_final.sh + +# Method 3: Manual CMake configuration +cmake -S . -B build_final -DDEMO_FINAL_STRIP=ON +cmake --build build_final +``` + +**⚠️ WARNING:** FINAL_STRIP removes ALL error checking (bounds checks, null checks, assertions). This saves ~500-600 bytes but means crashes will have no error messages. Use ONLY for final release builds, never for development or testing. + +**Build Mode Hierarchy:** +- **Debug**: Full error checking + debug features +- **STRIP_ALL**: Full error checking, no debug features (~64k target) +- **FINAL_STRIP**: No error checking, no debug features (absolute minimum size) + ### Developer Build (All Options) To enable all features at once (tests, tools, size optimizations, and stripping) for a comprehensive check: diff --git a/scripts/build_final.sh b/scripts/build_final.sh new file mode 100755 index 0000000..ae85030 --- /dev/null +++ b/scripts/build_final.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# This file is part of the 64k demo project. +# Builds the final production binary with all stripping enabled. +# Usage: ./scripts/build_final.sh + +set -e # Exit on error + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +BUILD_DIR="$PROJECT_ROOT/build_final" + +echo "========================================" +echo " Building FINAL Production Binary" +echo "========================================" +echo "" +echo "This build uses:" +echo " - DEMO_FINAL_STRIP=ON (removes ALL error checking)" +echo " - DEMO_STRIP_ALL=ON (removes debug features)" +echo " - DEMO_SIZE_OPT=ON (maximum size optimization)" +echo "" + +# Clean previous final build +if [ -d "$BUILD_DIR" ]; then + echo "Cleaning previous final build..." + rm -rf "$BUILD_DIR" +fi + +# Create fresh build directory +mkdir -p "$BUILD_DIR" +cd "$BUILD_DIR" + +# Configure with FINAL_STRIP +echo "Configuring with FINAL_STRIP..." +cmake "$PROJECT_ROOT" \ + -DDEMO_FINAL_STRIP=ON \ + -DDEMO_SIZE_OPT=ON \ + -DCMAKE_BUILD_TYPE=Release + +# Build demo64k +echo "" +echo "Building demo64k..." +cmake --build . --target demo64k -j8 + +# Show result +echo "" +echo "========================================" +echo " Final Build Complete!" +echo "========================================" +echo "" +echo "Binary location:" +ls -lh "$BUILD_DIR/demo64k" +echo "" + +# Optional: Compare with stripped build (if exists) +STRIP_BUILD="$PROJECT_ROOT/build_strip" +if [ -f "$STRIP_BUILD/demo64k" ]; then + FINAL_SIZE=$(stat -f%z "$BUILD_DIR/demo64k" 2>/dev/null || stat -c%s "$BUILD_DIR/demo64k") + STRIP_SIZE=$(stat -f%z "$STRIP_BUILD/demo64k" 2>/dev/null || stat -c%s "$STRIP_BUILD/demo64k") + SAVED=$((STRIP_SIZE - FINAL_SIZE)) + + echo "Size comparison:" + echo " STRIP_ALL build: $STRIP_SIZE bytes" + echo " FINAL_STRIP build: $FINAL_SIZE bytes" + echo " Saved: $SAVED bytes" + echo "" +fi + +echo "To run: $BUILD_DIR/demo64k" +echo "" +echo "⚠️ WARNING: This build has NO error checking!" +echo " Use only for final release, not for development/testing." 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) +// |
