Skip to content

feat: S11.01 CircularBuffer pool migration (E11 pilot)#394

Merged
DavidCozens merged 31 commits into
mainfrom
feat/s11-01-circular-buffer-pool
May 18, 2026
Merged

feat: S11.01 CircularBuffer pool migration (E11 pilot)#394
DavidCozens merged 31 commits into
mainfrom
feat/s11-01-circular-buffer-pool

Conversation

@DavidCozens

@DavidCozens DavidCozens commented May 17, 2026

Copy link
Copy Markdown
Owner

Purpose

Closes #392. First story of E11 (#29) — migrates SolidSyslogCircularBuffer away from caller-supplied instance storage and onto a library-owned static pool. The resulting *Static.c is the reference shape for every other E11 class (BlockStore, FileBlockDevice, the platform Mutex / Stream / Datagram / File / Atomic / TLS / FatFs adapters); getting it right here saves churn later. PR #393 (already merged) landed the SolidSyslog_SetConfigLock synchronisation seam this PR consumes.

Change Description

Public API change

SolidSyslogCircularBuffer_Create(struct SolidSyslogMutex* mutex, uint8_t* ring, size_t ringBytes) returns struct SolidSyslogBuffer*. The instance bookkeeping struct is now library-pooled; the integrator still supplies the ring memory. _Destroy(struct SolidSyslogBuffer* base) takes the handle the integrator was given.

Three-TU split per E11 class

  • Core/Source/SolidSyslogCircularBuffer.c — vtable + ring logic + private Initialise / Cleanup.
  • Core/Source/SolidSyslogCircularBufferPrivate.h — struct + private signatures (TU-internal; tests do not include).
  • Core/Source/SolidSyslogCircularBufferStatic.c — pool, public _Create / _Destroy, Fallback singleton, slot-walk synchronisation.

Locked-in shape (the bits other E11 classes copy)

  • Tunable SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE (default 1U), #ifndef-guarded, floor #error.
  • SolidSyslog_LockConfig() wraps every slot probe. Per-iteration locking, not lock-once-around-loop. Initialise runs outside the lock; Cleanup runs inside its per-iteration critical section. The asymmetry is intentional — releasing the lock around Cleanup would let a concurrent Create grab the slot and race Initialise vs Cleanup on the same memory.
  • Fallback singleton of type struct SolidSyslogBuffer with a no-op vtable. Pool exhaustion returns &Fallback (Log/Service become no-ops; never NULL deref). One ERROR severity report on the transition.
  • Uniform _Destroy contract: any handle not currently issued by Create (unknown stranger / pool-issued but already destroyed / Fallback) reports SOLIDSYSLOG_SEVERITY_WARNING. Same message; no per-case carve-outs.
  • Symmetric Initialise / Cleanup — both take struct SolidSyslogBuffer* base and recover the concrete struct via CircularBuffer_SelfFromBase. The 11.3 cast lives in one TU (already deviated). *Static.c never casts.
  • Scan-then-release in _Destroy: address-match walk is lock-free (pool addresses are file-scope statics that never move); the lock only wraps the InUse check + Cleanup + MarkFree. Lock count drops to 0 on unknown handles, 1 on matched handles, regardless of pool size.

Reference structure of *Static.c

_Create and _Destroy are four lines of pure intent each. The helper tree splits at clean boundaries:

  • Composition: AcquireFirstFree, AcquireIfFree, IndexFromHandle, FreeIfInUse, Acquire
  • Predicates (subject-IS-adjective): PoolItemIsFree chains through PoolItemIsInUse; HandleIsValid; PoolIndexIsValid
  • Atomic field ops: MarkInUse, MarkFree, HandleFromIndex, PoolItemIsInUse

The .InUse field is touched in three places total: one read (PoolItemIsInUse), one set-true (MarkInUse), one set-false (MarkFree). &Fallback is referenced in two places (static initialiser + HandleIsValid). &Pool[poolIndex].Object.Base is computed in exactly one place (HandleFromIndex).

MISRA / suppression posture

  • Zero new cppcheck-suppress / NOLINT lines added on this branch — the E11 invariant.
  • The existing line-pinned 11.3 suppression for CircularBuffer.c's SelfFromBase cast shifted two lines as Initialise and Cleanup signatures changed; both shifts updated the existing entry, never added a new one.
  • cppcheck-misra count: 87 (down 1 from main's 88, gained by _Destroy becoming single-exit and then by the scan-then-release split).

Tests

  • New TEST_GROUP SolidSyslogCircularBufferPool pins the pool-exhaustion Fallback contract, the ERROR report on exhaustion, lock counts on Create (per-iteration) and Destroy (zero on unknown, one on matched), and WARNING-on-unknown-or-stale.
  • Basic SolidSyslogCircularBuffer and SolidSyslogCircularBufferMutex test groups restructured onto a shared CircularBufferFixture TEST_BASE with Write(...) / Read() helpers and a CHECK_LAST_READ_RECORD macro. Test bodies now read as setup → act → check with no SolidSyslogBuffer_*(buffer, ..., sizeof(...), &readSize) boilerplate.

Test Evidence

All gates green on the host containers:

Gate Result
cmake --preset debug + SolidSyslogTests 1127 ran, 0 failures
cmake --preset sanitize (ASan + UBSan) + tests 1127 ran, 0 failures
cmake --preset clang-debug (Clang container) builds clean
cmake --preset tidy (clang-tidy errors-as-warnings) clean
cmake --preset cppcheck clean
cmake --preset coverage ≥99% project-wide
clang-format --dry-run --Werror clean
cppcheck-misra (warning-mode) 87 (was 88 on main)

SolidSyslogCircularBufferStatic.c specifically: 100% line (68/68), 100% function (16/16), 100% branch (18/18).

Windows / BDD / FreeRTOS / TLS-integration jobs run in CI.

Areas Affected

  • Core/Interface/SolidSyslogCircularBuffer.h — public _Create / _Destroy API change (caller-supplied instance storage gone; ring still caller-supplied).
  • Core/Interface/SolidSyslogTunablesDefaults.h — new SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE tunable.
  • Core/Source/SolidSyslogCircularBuffer.c / Private.h — vtable + ring logic split; Initialise and Cleanup now take base.
  • Core/Source/SolidSyslogCircularBufferStatic.cnew: pool + public API + Fallback.
  • Core/Source/SolidSyslogErrorMessages.h — two new error message strings.
  • Tests/SolidSyslogCircularBufferTest.cpp — restructured onto TEST_BASE; pool tests added.
  • misra_suppressions.txt — line-pin updates for the existing 11.3 deviation (no new entries).
  • DEVLOG.md — pool migration entry covering the locked-in design.

Downstream callers in this repo (Bdd/Targets/, Tests/Bdd/Targets) were updated to the new _Create signature in earlier commits on the branch.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor

    • Buffer API updated to take a caller-provided ring buffer and mutex with a revised argument order.
    • Internal storage model moved from embedded storage blobs to externally supplied ring memory and a library-managed static pool with fallback behavior.
  • New Features

    • Compile-time tunable added to configure the static pool size of buffer instances.
  • Documentation

    • Architecture and naming docs updated to describe the ring-buffer model and pool behavior.

Review Change Stack

DavidCozens and others added 30 commits May 17, 2026 17:33
…r-supplied

Splits the previously-conflated CircularBuffer storage. The instance
bookkeeping struct (vtable, mutex pointer, ring pointer, head/tail/wrap)
now lives in a library-internal static pool; the caller continues to
supply the ring memory as plain uint8_t* + size. This addresses the
original E11 driver — eliminating the void* storage cast on the
instance — without conflating it with a tunable for ring capacity
(which would not work: different call sites need different ring sizes
inside the same build).

Public API change:
- SolidSyslogCircularBuffer_Create(mutex, ring, ringBytes) replaces
  Create(storage, storageBytes, mutex).
- SolidSyslogCircularBufferStorage typedef, OVERHEAD constant, and
  STORAGE_SIZE / STORAGE_SIZE_BYTES macros removed.
- New convenience macro SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(maxMessages)
  for sizing the caller's ring.
- HEADER_BYTES stays public — needed by the new macro.

Three-TU split (E11 canonical pattern):
- SolidSyslogCircularBuffer.c: vtable + ring logic. Exposes
  CircularBuffer_Initialise(self, mutex, ring, ringBytes) and
  CircularBuffer_Cleanup(self) via SolidSyslogCircularBufferPrivate.h.
- SolidSyslogCircularBufferPrivate.h: struct definition + helper
  signatures. TU-local; tests must not include it.
- SolidSyslogCircularBufferStatic.c: 1-slot static pool, public
  Create/Destroy. Next commit grows pool size, adds the fallback
  object, wraps slot walks in SolidSyslog_LockConfig/UnlockConfig,
  and handles unknown-pointer Destroy.

CMake gate (E11):
- SOLIDSYSLOG_ALLOCATION_STRATEGY cache var (static|dynamic, default
  static). dynamic errors with FATAL_ERROR — no dynamic TUs ship yet.

MISRA / static analysis:
- Storage-cast suppression (CircularBuffer.c:72 SelfFromStorage) deleted
  — function gone.
- 18.7 FAM suppression (CircularBuffer.c:26) deleted — struct no longer
  has a flexible array member; ring is now an external pointer.
- 2.5 STORAGE_SIZE macro suppression deleted — macro gone, RING_BYTES
  takes the remaining slot.
- 5.7 + 2.4 + 11.3 (SelfFromBase) + 2.4 + 5.7 (anonymous enums)
  suppression line numbers updated.
- One new constParameter inline suppression in Static.c for the
  transitional 1-slot Destroy that only compares the base pointer;
  next commit's pool walk + Cleanup wiring mutates through it and the
  suppression goes away.
- Net unsuppressed cppcheck-misra findings 93 -> 87 across the
  scanned Core+Platform tree.

Doc updates:
- CLAUDE.md public-header audience table: row rewritten for new API.
- README.md public-headers list: bullet updated to call out caller
  supplies ring memory only, instance lives in static pool.
- docs/NAMING.md: SelfFromStorage example switched to BlockStore (still
  on the caller-storage pattern); added a note that E11-migrated
  classes (currently only CircularBuffer) no longer use the cast.
- docs/misra-deviations.md: D.002 caller-storage list drops
  CircularBuffer with a note pointing at E11; D.005 FAM scope reduces
  to Formatter only; D.012 macro inventory updated to RING_BYTES.
- docs/iec62443.md: "caller-allocated storage" -> "caller-allocated
  ring memory" in the RTOS-integration prose.
- SKILL.md: matching prose update.

Tests + BDD targets updated:
- HandleEqualsStorageAddress test removed (caller no longer addresses
  the instance struct; only the ring is at a known address).
- TEST_MAX_MESSAGES / BDD_TARGET_BUFFER_MESSAGES / etc. constants kept
  unchanged at every call site — only the API mechanics changed.

Gates all green locally:
- debug, clang-debug, sanitize: 1118/1118 tests pass.
- tidy, cppcheck, cppcheck-misra: clean.
- coverage: 99.6% lines / 99.0% functions.
- cmake -DSOLIDSYSLOG_ALLOCATION_STRATEGY=dynamic errors as designed.
…ithm disabled project-wide

Adds the second half of the pool mechanics now that there is a real
consumer of POOL_SIZE: pool walks (claim + release) wrap in
SolidSyslog_LockConfig / _UnlockConfig, exhausted Create returns a
class-private fallback singleton with no-op vtable, and emits
SolidSyslog_Error(ERROR, "...pool exhausted...").

Tunable:
- SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE (default 1U) added to
  SolidSyslogTunablesDefaults.h with the standard #ifndef / floor
  guard pattern + NOLINTNEXTLINE matching SOLIDSYSLOG_MAX_MESSAGE_SIZE.

CMake / cppcheck:
- Global --suppress=useStlAlgorithm added to CMAKE_CXX_CPPCHECK.
  The rule only ever fires on C++ test code (production is C, no STL),
  and replacing test loops with std::generate masks the side-effecting
  Create per iteration rather than expressing it. Replaces the
  per-loop inline suppression that the earlier attempt added.

Tests:
- New TEST_GROUP(SolidSyslogCircularBufferPool) covering: filling the
  pool returns distinct buffers + a distinct fallback on overflow,
  exhausted Create reports ERROR, fallback Read/Write are no-ops,
  Create and Destroy each wrap in LockConfig / UnlockConfig.
- Tests are parameterised on SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE
  (loop POOL_SIZE Creates into a fixture array, then one more for
  the overflow) so they work at any pool-size tunable.

Outstanding hygiene (next commit):
- Cleanup currently takes struct SolidSyslogCircularBuffer*; will
  change to take struct SolidSyslogBuffer* and do SelfFromBase
  internally so Static.c no longer needs the constParameter suppression.
- Extract Pool_TryClaim / Pool_Release / Slot_OwnsBase static-inlines
  in Static.c to express intent and DRY the two pool walks.
- Improve test diagnostic messages: bare CHECK(...) replaced with
  CHECK_TEXT(...) or a CHECK_IS_FALLBACK(buf, pool) macro for the
  central pool-test assertion.
Pure formatting pass on CircularBuffer.h / .c / Private.h.
The earlier S11.01 commits left aligned-column declarations
that the project's clang-format rejects (PointerAlignment: Left,
AlignConsecutiveDeclarations: None). No code changes.
SolidSyslogCircularBuffer_Destroy now emits SolidSyslog_Error
(SOLIDSYSLOG_SEVERITY_WARNING) when given a handle the pool never
issued. Handles legitimately returned by _Create — pool-slot bases
and the pool-exhaustion Fallback — remain silent. Drives the
acceptance criterion from #392.

Two tests in TEST_GROUP(SolidSyslogCircularBufferPool):
- DestroyOfUnknownHandleReportsWarning pins the WARNING severity
  and message text via the existing ErrorHandlerFake.
- DestroyOfFallbackHandleIsSilent pins the Fallback exception
  (otherwise every integrator who Destroys the buffer they were
  given would get a spurious warning when the pool was exhausted).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cleanup now takes struct SolidSyslogBuffer* and casts internally,
matching the symmetry of every other vtable method on the class.
*Static.c can hand it the integrator's handle directly, which removes
the cppcheck constParameter complaint that previously needed an
inline suppression on _Destroy.

This is the pattern the rest of E11 will follow: Initialise takes
the concrete pool slot (known shape inside *Static.c), Cleanup takes
the public handle (unknown provenance from the caller's side).

Adjusts the existing 11.3 line-pinned suppression for the SelfFromBase
cast — the cast moved one line down inside the new Cleanup body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_Create and _Destroy now read as the lifecycle they describe:

  _Create   -> LockConfig; claimed = TryClaim(...); UnlockConfig;
              if (!claimed) Error+Fallback; return claimed;

  _Destroy  -> if (base != &Fallback) {
                  LockConfig; released = Release(base); UnlockConfig;
                  if (!released) Error(WARNING, unknown handle);
              }

Three new file-scope static-inline helpers carry the intent:
- CircularBuffer_TryClaim     -> walk pool, claim first free slot
- CircularBuffer_Release      -> walk pool, release matching slot
- CircularBuffer_SlotOwnsBase -> named predicate for the slot-walk match

_Destroy is now single-exit fall-through, retiring a pre-existing
MISRA 15.5 violation. cppcheck-misra count drops 88 -> 87.

The lock/unlock pair still wraps every slot walk; the error reporting
stays outside the critical section, matching the existing pattern.
Helpers are forward-declared at the top and defined immediately
beneath their first caller, matching SolidSyslogFileBlockDevice.c.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous shape held the LockConfig critical section around the
whole slot walk, including the CircularBuffer_Initialise call. That
becomes a problem the moment another E11 class has a non-trivial
Initialise (FatFs file open, TLS context setup, ...) or the integrator
maps LockConfig to taskENTER_CRITICAL on FreeRTOS — interrupts would
be disabled for the full Initialise.

TryClaim now locks per slot probe: lock, check, claim (mark InUse),
unlock. Initialise runs once, outside any critical section. The slot
is safe to initialise lock-free because the InUse=true flag has
already reserved it against parallel Creates, and no other task can
yet hold the handle (we haven't returned from _Create).

Release / _Destroy uses the same per-iteration locking but Cleanup
stays inside the critical section: releasing the lock between match
and Cleanup would let a concurrent Destroy of the same handle race,
and releasing it after marking InUse=false would let a concurrent
Create grab the slot and race Initialise vs Cleanup on the same
memory. The asymmetry is intentional and worth documenting for the
rest of E11: Create can offload Initialise outside the lock; Destroy
cannot offload Cleanup without a third "Pending" slot state.

For CircularBuffer specifically Cleanup is 8 stores; on classes where
Cleanup is expensive, integrators should map LockConfig to a sleeping
mutex (pthread_mutex_t, FreeRTOS Semaphore) rather than a critical
section.

The existing CreateAcquiresAndReleasesConfigLock / DestroyAcquires...
tests still pass: at default SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE=1
"once per probed slot" equals "once total". A tunable-override preset
with pool size > 1 would pin per-iteration locking observably; not
adding one yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The TryClaim / Release / SlotOwnsBase static-inline split was making
the file harder to read, not easier. Reverting the extraction so both
public functions hold the whole walk inline; intent is to factor it
differently from a clean starting point.

Behaviour is unchanged: per-iteration LockConfig/UnlockConfig in both
walks, Initialise runs outside any lock, Cleanup stays inside its
per-iteration critical section, Fallback short-circuits Destroy,
unknown handles report a WARNING.

Tests still pass (1127 ran, 0 failures); cppcheck-misra count stays
at 87, no new suppressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pool group:
- New MakeBuffer() fixture method removes the
  (mutex, ring, sizeof(ring)) repetition at every Create call site.
- New CHECK_IS_FALLBACK(buf, pool) macro turns the bare-CHECK trio
  in FillingPoolThenOverflowReturnsDistinctFallback into three named
  CHECK_TEXT calls that say which constituent failed and why.
- Two new tests pin the per-iteration locking property added in
  4a34777 -- they assert
    LockCallCount == SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE
  for both Create-when-full and Destroy-of-last-issued. At default
  POOL_SIZE=1 these reduce to ONCE (the existing on-first-match
  tests already cover that); at POOL_SIZE > 1 they observe the
  per-iteration shape.
- Existing CreateAcquiresAndReleasesConfigLock / Destroy... renamed
  to "...OnFirstFreeSlot" / "...OnFirstSlotMatch" to make their
  shape explicit alongside the new tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eliminates claimedIndex from _Create. The slot-walk now tracks a
struct SolidSyslogBuffer* initialised to &Fallback; a successful
claim swings it to the slot's base pointer, and the loop breaks on
the first non-Fallback observation. The single pointer carries both
"have we claimed" and "which handle do we return".

To make this work without a parallel index, CircularBuffer_Initialise
gains the same base-pointer signature that _Cleanup already has --
both vtable lifecycle hooks now take struct SolidSyslogBuffer* and
recover the concrete struct via CircularBuffer_SelfFromBase. The
existing 11.3 line-pinned suppression follows the cast one line
down inside the updated Initialise body; no new suppression sites.

The comparisons against &Fallback are repeated -- next step is
probably an IsFallback(p) helper to wrap them. Behaviour unchanged;
1127 tests pass, cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lifts the lock/check/claim/unlock sequence into a per-slot helper.
TryClaimSlot returns the slot's base pointer when the slot was free
and is now claimed, or &Fallback when the slot was already in use.
_Create's loop body collapses to a single assignment and the break.

The handle returned is exactly what _Create wants to either pass to
Initialise or hand back to the caller, so no second walk or index
recovery is needed. Sits immediately above _Create -- defined before
first use, no forward declaration.

1127 tests pass; cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "Try" in TryClaimSlot was misleading -- the function never fails,
it always returns a usable handle, just sometimes the Fallback.
"AcquireIfFree" reads honestly: it acquires the entry if free,
otherwise hands back the Fallback. Drops the "Slot" terminology that
was leaking the file-internal struct Slot name into the helper.

The local "claimed" similarly read past-tense; the variable starts as
&Fallback, which is the un-claimed state. "handle" matches the term
the public API already uses for the buffer pointers it issues, and
reads cleanly at both call sites:
   handle = &Fallback;                     (haven't found anything yet)
   handle = CircularBuffer_AcquireIfFree(i);

1127 tests pass; cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per CLAUDE.md "Function Ordering": _Create first, _Destroy second,
helpers defined directly below their first caller with forward
declarations grouped at the top. CircularBuffer_AcquireIfFree moves
from above _Create to immediately below it, joined by a forward
declaration alongside the Fallback_Read / Fallback_Write decls.

No behaviour change; 1127 tests pass, cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The three "handle != &Fallback" / "base != &Fallback" sites in
_Create and _Destroy now read as a named predicate. The Fallback
sentinel address is now mentioned in exactly one place in the file
-- the helper body -- so a future change of the sentinel mechanism
(e.g. a per-class null-object table or a different signalling
strategy) is a one-line edit.

Skipping the symmetric HandleIsInvalid for now: no production site
uses the negative form, and tests don't see &Fallback today. Easy
follow-up when a test (or a future production guard-clause) wants
positive-logic against "this is the fallback".

1127 tests pass; cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_Create now reads top-to-bottom as:
   handle = AcquireFirstFree();
   if HandleIsValid -> Initialise
   else             -> Error
   return handle;

The pool walk lives one level down in AcquireFirstFree, where the
loop bound and the per-iteration AcquireIfFree call are the only
shape. _Create itself is six executable lines of pure intent.

Function order: AcquireFirstFree is defined directly below _Create;
AcquireIfFree directly below AcquireFirstFree (its first caller);
HandleIsValid directly below that. _Destroy follows.

Naming kept parallel with AcquireIfFree -- both are verb + free-
condition with no "Handle" suffix; the `handle` variable at the call
site documents the return shape. Trivial to bulk-rename both to
*Handle later if the file gets more such helpers.

1127 tests pass; cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pool[poolIndex].InUse reads as a unit at every site; the name ties
the index to the only collection it can index in the file, matching
FileBlockDevice.c's blockIndex pattern.

Applied to AcquireIfFree's parameter, AcquireFirstFree's loop, and
_Destroy's loop -- all three are indices into Pool.

1127 tests pass; cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AcquireIfFree's guard now reads as a near-quote of the function
name: "if PoolItemIsFree, claim it". The !InUse negation is hidden
behind the predicate, leaving the function body in pure positive
logic.

Left _Destroy's "Pool[poolIndex].InUse && ..." check alone -- it
wants the positive in-use sense, and PoolItemIsFree's inverse would
be negative logic at the call site. If the symmetry matters later,
PoolItemIsInUse is the natural pair to add.

1127 tests pass; cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two lines that actually do the claim -- mark the pool item
in-use and hand back its base pointer -- now live in a dedicated
Acquire helper. The vocabulary nests:

   AcquireFirstFree -> walk pool, return first acquired
   AcquireIfFree    -> probe one slot, lock-check-Acquire-unlock
   Acquire          -> unconditional mark-and-return for one slot

AcquireIfFree's body now reads as a sentence:
   if PoolItemIsFree, Acquire it.

1127 tests pass; cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_Destroy no longer treats &Fallback as a privileged input. The
Fallback singleton is never installed in Pool, so the slot-walk
simply doesn't match it and falls through to the existing WARNING
path. The contract changes from

   Destroy(&Fallback) -> silent
   Destroy(other unknown handle) -> WARNING

to a single uniform rule:

   Destroy(handle not currently issued by Create) -> WARNING

regardless of whether that handle was once issued by Create
(pool-exhaustion Fallback) or never was (stack-fabricated stranger,
double-Destroy, etc).

The function body drops one indent level and the asymmetric "we
silently swallow the Fallback we issued you" special case. The
existing DestroyOfUnknownHandleReportsWarning test already covers
the WARNING path; the now-incorrect DestroyOfFallbackHandleIsSilent
test is deleted (its assertion no longer holds).

HandleIsValid stays -- still used by _Create and AcquireFirstFree.

1126 tests pass (-1 from the deletion); cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_Destroy now has two phases at the top level:
1. Find the matching poolIndex by scanning constant slot addresses
   (no lock needed -- &Pool[i].Object.Base never moves).
2. If a match was found, acquire the lock once, check InUse,
   conditionally Cleanup + free + release. Single lock acquisition
   per Destroy at most.

Lock-count contract changes accordingly:
- Pool-issued, currently-in-use handle: 1 lock (was 1..N).
- Pool-issued, already-destroyed handle: 1 lock (was 1..N).
- Unknown handle (no address match):    0 locks (was N).

Externally-visible behaviour for the integrator is unchanged: live
handles release cleanly, stale or stranger handles get the WARNING.
The "stale-pool-address" case (double-Destroy or use-after-Destroy)
already went down the WARNING path before -- the compound condition
in the old loop just failed -- so this isn't a new failure mode,
just made explicit.

Tests updated:
- DestroyAcquiresAndReleasesConfigLockOnFirstSlotMatch ->
  DestroyOfPooledHandleLocksOnce. The "OnFirstSlotMatch" qualifier
  is now obsolete; any pool-issued handle locks exactly once.
- DestroyLocksOncePerSlotProbedUntilMatch (N locks) replaced by
  DestroyOfUnknownHandleDoesNotLock (NEVER). The N-locks property
  no longer holds and the zero-locks property is new and worth
  pinning.

1126 tests pass; cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
&Pool[poolIndex].Object.Base appeared at two unrelated sites --
Acquire's return and _Destroy's address-match scan -- with the same
"convert pool index to the handle the integrator sees" meaning.
Lifted into HandleFromIndex(poolIndex) so each site reads as
intent, not field navigation.

1126 tests pass; cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pool[poolIndex].InUse mutations now go through named verbs --
MarkInUse(poolIndex) from Acquire, MarkFree(poolIndex) from
_Destroy's release block. The field-write is hidden the same way
PoolItemIsFree hides the field-read, so the file reads as a
consistent vocabulary at one level of abstraction.

Pair-of-helpers, not single-helper-with-bool-arg, so call sites
don't read "set in-use to false" and instead read "mark free" --
intent at the verb, no boolean to translate.

1126 tests pass; cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_Destroy's head reduces to one line of intent:
   poolIndex = IndexFromHandle(base);

The find loop now lives in its own function -- the natural inverse
of HandleFromIndex(i). Together the pair forms a closed conversion:

   HandleFromIndex(i)         -> base pointer for slot i
   IndexFromHandle(base)      -> slot index for that pointer, or
                                 POOL_SIZE if base wasn't issued

_Destroy now reads at one level of abstraction: get the index,
release-if-in-use under lock, warn-if-not-released.

1126 tests pass; cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE" check was
shorthand for "IndexFromHandle returned a real index, not the
sentinel". Naming it makes the condition self-documenting:

    if (CircularBuffer_PoolIndexIsValid(poolIndex))

Subject-IS-adjective form matches the other predicates in the file
(PoolItemIsFree, HandleIsValid).

1126 tests pass; cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Raw Pool[poolIndex].InUse reads collapse to a single site. The
field's full surface in the file is now:

    PoolItemIsInUse(poolIndex)  -- the only read
    MarkInUse(poolIndex)        -- the only set-to-true
    MarkFree(poolIndex)         -- the only set-to-false

Everything else expresses intent. PoolItemIsFree becomes the
negation of PoolItemIsInUse, so a future representation change
(atomic flag, packed bitset, separate free-list, etc) touches just
the three accessor functions.

1126 tests pass; cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symmetric counterpart to AcquireIfFree: lock, check IsInUse, do
the work (Cleanup + MarkFree), unlock, return whether anything was
freed. _Destroy collapses to the lifecycle skeleton:

    poolIndex = IndexFromHandle(base);
    if PoolIndexIsValid -> released = FreeIfInUse(poolIndex);
    if not released -> warn;

FreeIfInUse takes only the poolIndex, mirroring AcquireIfFree's
single-arg signature. The handle for Cleanup is recomputed
internally via HandleFromIndex -- no risk of a base argument
that diverges from the index.

1126 tests pass; cppcheck-misra stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The loop now iterates poolIndex consistently with AcquireFirstFree;
the function's return becomes `result`, naming its role rather than
its type. The previous shape used the inconsistent `i` to avoid
shadowing -- this is the cleaner resolution.

Final state of CircularBufferStatic.c on this branch:

- Public API: _Create and _Destroy each read as 4-6 lines of pure
  intent against named helpers.
- Helpers organised three levels deep:
  * Composition  -- AcquireFirstFree, AcquireIfFree, IndexFromHandle,
                    FreeIfInUse, Acquire
  * Predicates   -- HandleIsValid, PoolIndexIsValid,
                    PoolItemIsFree (-> chains through PoolItemIsInUse)
  * Atomic ops   -- MarkInUse, MarkFree, HandleFromIndex,
                    PoolItemIsInUse
- Pool[poolIndex].InUse touched in exactly three places:
  one read (PoolItemIsInUse), one set-true (MarkInUse),
  one set-false (MarkFree).
- &Fallback referenced in exactly two places: the static initialiser
  and HandleIsValid.
- &Pool[poolIndex].Object.Base computed in exactly one place
  (HandleFromIndex).
- Zero MISRA warnings in this TU; zero new cppcheck suppressions
  or NOLINT comments anywhere on the branch since main.
- Lock duration: per-iteration around Acquire (so Initialise runs
  unlocked); single acquire on _Destroy only when a pool address
  matches.
- 1126 tests pass; cppcheck-misra count 87 (was 88 on main).

This is the reference shape for the rest of E11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds DestroyOfStaleHandleReportsWarning. Exercises the
FreeIfInUse path where IndexFromHandle DOES find a matching slot
(so PoolIndexIsValid is true and the lock is taken) but
PoolItemIsInUse is false because the slot was already released --
the double-Destroy / use-after-Destroy case introduced by the
scan-then-release split.

Brings CircularBufferStatic.c to 100% line, 100% function, and
100% branch coverage (68/68, 16/16, 18/18). The whole-codebase
line + function coverage stays above the CI floor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tests now read as a 1-2 line setup followed by an act and a check,
with the noise lifted out into shared fixture members and macros.

- TEST_BASE CircularBufferFixture holds buffer, readData, readSize
  and the Write(text) / Write(data, size) / Read() helpers. Groups
  1, 2, 3 derive via TEST_GROUP_BASE; each declares only its own
  ring storage and chooses its mutex source in setup/teardown.
  Eliminates ~30 lines of duplicated declarations across the
  three groups.

- CHECK_LAST_READ_RECORD(expected, size) macro at file scope
  replaces the LONGS_EQUAL(size, readSize) + MEMCMP_EQUAL(...)
  pair that appeared in 8+ tests. Wraps with NOLINTBEGIN/END so the
  macro preserves __FILE__/__LINE__ at the test's call site.

- The Pool group teardown now guards against destroying nullptr
  pool slots / nullptr overflow -- those previously emitted a
  WARNING through the still-installed ErrorHandlerFake after the
  body's assertions, which was harmless but noisy.

- Small cleanups:
    OverflowingWriteIsDropped's local `overflow` -> `extra` (the
      Pool group has an `overflow` field; the name clash was only
      visual but it confused the reader).
    WrapsAroundEndOfStorage drops the inline `enum {CYCLES,...}`
      in favour of constexpr locals.
    WriteExceedingMaxMessageSizeIsDropped drops the bespoke
      destination buffer -- the fixture's readData is fine.

1127 tests pass; static.c coverage stays 100% line / 100% function /
100% branch; cppcheck-misra count stays at 87; clang-tidy, sanitize,
clang-format, plain cppcheck all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the locked-in shape, the reference structure of *Static.c
(this is the E11 reference for every other class), the test pinning
strategy, the mid-flight course correction on helper extraction,
the gate posture, and the open questions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR migrates SolidSyslogCircularBuffer from a caller-provided storage-blob model to a static-pool architecture using caller-allocated uint8_t ring buffers. The public API changes to SolidSyslogCircularBuffer_Create(mutex, ring, ringBytes); private lifecycle, static-pool Create/Destroy, tests, consumers, build options, and documentation are updated accordingly.

Changes

Circular Buffer Pool Migration

Layer / File(s) Summary
Public API contract and sizing
Core/Interface/SolidSyslogCircularBuffer.h
Ring-based Create(mutex, ring, ringBytes) replaces storage-blob form and introduces SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES sizing macro.
Build options and tunables
Core/Interface/SolidSyslogTunablesDefaults.h, CMakeLists.txt, Core/Source/CMakeLists.txt
Adds SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE tunable (default 1U) and CMake SOLIDSYSLOG_ALLOCATION_STRATEGY option (enforces static-only), and adds cppcheck suppression and Static TU to Core sources.
Private struct, lifecycle, and ring I/O
Core/Source/SolidSyslogCircularBufferPrivate.h, Core/Source/SolidSyslogCircularBuffer.c
Introduces private SolidSyslogCircularBuffer with Ring pointer and indices; CircularBuffer_Initialise/CircularBuffer_Cleanup wire/unwire vtable and state; read/write now use caller Ring with 2-byte little-endian length header.
Static pool Create/Destroy
Core/Source/SolidSyslogCircularBufferStatic.c, Core/Source/SolidSyslogErrorMessages.h
Implements pooled SolidSyslogCircularBuffer_Create/Destroy with config-lock-protected slot allocation, fallback buffer behavior when pool exhausted, and new error message macros for pool exhaustion/unknown destroy.
Consumer migration and test refactor
Bdd/Targets/*, Tests/*
Consumers (FreeRTOS, Windows, integration tests) now allocate uint8_t ring buffers sized via SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES and call Create(mutex, ring, ringBytes). Tests refactored with CircularBufferFixture, CHECK_LAST_READ_RECORD, TEST_GROUP_BASE inheritance, and new pool/fallback tests.
Docs, devlog, and MISRA deviations
CLAUDE.md, README.md, SKILL.md, docs/NAMING.md, docs/iec62443.md, docs/misra-deviations.md, DEVLOG.md
Docs updated to describe ring-memory model and static-pool bookkeeping; MISRA deviations revised to reflect E11 migration and single RING_BYTES macro.
MISRA suppression updates
misra_suppressions.txt
Line-number adjustments and removal/addition of suppressions for circular-buffer headers/sources across several deviation groups.
Tools: IWYU filter
scripts/iwyu_filter.py
Filters IWYU addition suggestions that match CppUTest macro-generated forward-declaration patterns and updates filtering logic/documentation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 A tiny ring the caller brings,

Static slots where syslog sings,
Create with mutex, ring, and size,
Pool or fallback, steady wise—
Rabbits cheer the tidy things.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately describes the main change: CircularBuffer pool migration, follows conventional commits format, and is concise.
Description check ✅ Passed Description fully addresses all template sections: Purpose (closes #392, E11 pilot), Change Description (public API change, three-TU split, locked-in shape, reference structure, MISRA posture, tests), and Areas Affected with comprehensive file listing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/s11-01-circular-buffer-pool

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   build-linux-gcc: 100% successful (✔️ 1133 passed, 🙈 2 skipped)
   🚦   build-freertos-host-tdd: 100% successful (✔️ 1253 passed, 🙈 2 skipped)
   🚦   build-linux-clang: 100% successful (✔️ 1085 passed, 🙈 2 skipped)
   🚦   sanitize-linux-gcc: 100% successful (✔️ 1085 passed, 🙈 2 skipped)
   🚦   integration-linux-openssl: 100% successful (✔️ 9 passed)
   🚦   integration-windows-openssl: 100% successful (✔️ 9 passed)
   🚦   bdd-linux-syslog-ng: 94% successful (✔️ 46 passed, 🙈 3 skipped)
   🚦   bdd-windows-otel: 90% successful (✔️ 44 passed, 🙈 5 skipped)
   🚦   bdd-freertos-qemu: 73% successful (✔️ 36 passed, 🙈 13 skipped)
   🚦   build-windows-msvc: 100% successful (✔️ 989 passed, 🙈 1 skipped)
   🚦   build-linux-tunable-override: 100% successful (✔️ 1085 passed, 🙈 2 skipped)
   ⚠️   Clang-Tidy: 10 warnings (normal: 10)
   ⚠️   CPPCheck: No warnings


Created by Quality Monitor v1.14.0 (#f3859fd). More details are shown in the GitHub Checks Result.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
Core/Source/SolidSyslogCircularBufferStatic.c (1)

59-114: ⚡ Quick win

Place _Destroy immediately after _Create in this source file.

Public _Destroy is currently below helper definitions. Reorder so _Destroy is the second function, then keep helpers below their first caller.

As per coding guidelines: "Within a source file, functions are ordered top-down: (1) _Create first, (2) _Destroy second, (3) other public functions in call order, (4) helper functions forward-declared at top ...".

Also applies to: 116-128

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Source/SolidSyslogCircularBufferStatic.c` around lines 59 - 114, Move
the public _Destroy function to immediately follow the public _Create function
in this source file so _Create is first and _Destroy is second; then keep the
other public functions (e.g., CircularBuffer_AcquireFirstFree,
CircularBuffer_AcquireIfFree) and helper functions (e.g.,
CircularBuffer_PoolItemIsFree, CircularBuffer_PoolItemIsInUse,
CircularBuffer_Acquire, CircularBuffer_MarkInUse,
CircularBuffer_HandleFromIndex, CircularBuffer_HandleIsValid) below their first
caller, adding forward declarations for any helper moved below its caller to
preserve build order and visibility per the project ordering guideline.
Tests/SolidSyslogCircularBufferTest.cpp (1)

13-13: 💤 Low value

Remove unused include SolidSyslogPrival.h.

The #include "SolidSyslogPrival.h" at line 13 is not used anywhere in this test file. Remove it to keep the include list clean.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/SolidSyslogCircularBufferTest.cpp` at line 13, Remove the unused
include by deleting the line that includes "SolidSyslogPrival.h" from
Tests/SolidSyslogCircularBufferTest.cpp; ensure no references to symbols from
SolidSyslogPrival remain in functions or tests (e.g., any use of prival-related
identifiers) and run the test compile to confirm no missing dependencies after
removing the include.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/misra-deviations.md`:
- Line 835: The phrase "caller-supplied ring memory in messages" is ambiguous
about units; update the text around the ring sizing macro (the sentence
containing "caller-supplied ring memory in messages") to explicitly state bytes
and show how to compute it from a maximum message count — e.g., reword to "size
caller-supplied ring memory in bytes from a max-message count" or similar, and
ensure any reference to the ring sizing macro mentions "bytes" (not just
"messages") so readers know the macro takes a byte size.

In `@misra_suppressions.txt`:
- Line 27: The suppression for misra-c2012-11.3 attributed to D.002 referring to
SolidSyslogCircularBuffer must be removed or rejustified: either delete the line
"misra-c2012-11.3:Core/Source/SolidSyslogCircularBuffer.c:91" from
misra_suppressions.txt and refactor CircularBuffer_SelfFromBase to avoid the
base-to-derived cast (replace the cast with a safe accessor or container/lookup
that returns a SolidSyslogCircularBuffer* without casting), or keep a
suppression but update its justification to a correct deviation entry (not
D.002) and add a matching explanation in docs/misra-deviations.md referencing
SolidSyslogCircularBuffer and the reason for the base-to-derived cast; ensure
the change references the function CircularBuffer_SelfFromBase and the
SolidSyslogCircularBuffer type so reviewers can locate the fix.

---

Nitpick comments:
In `@Core/Source/SolidSyslogCircularBufferStatic.c`:
- Around line 59-114: Move the public _Destroy function to immediately follow
the public _Create function in this source file so _Create is first and _Destroy
is second; then keep the other public functions (e.g.,
CircularBuffer_AcquireFirstFree, CircularBuffer_AcquireIfFree) and helper
functions (e.g., CircularBuffer_PoolItemIsFree, CircularBuffer_PoolItemIsInUse,
CircularBuffer_Acquire, CircularBuffer_MarkInUse,
CircularBuffer_HandleFromIndex, CircularBuffer_HandleIsValid) below their first
caller, adding forward declarations for any helper moved below its caller to
preserve build order and visibility per the project ordering guideline.

In `@Tests/SolidSyslogCircularBufferTest.cpp`:
- Line 13: Remove the unused include by deleting the line that includes
"SolidSyslogPrival.h" from Tests/SolidSyslogCircularBufferTest.cpp; ensure no
references to symbols from SolidSyslogPrival remain in functions or tests (e.g.,
any use of prival-related identifiers) and run the test compile to confirm no
missing dependencies after removing the include.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 368a0d05-9ea9-4e10-a795-654bd598867f

📥 Commits

Reviewing files that changed from the base of the PR and between 168632b and 286194b.

📒 Files selected for processing (21)
  • Bdd/Targets/FreeRtos/main.c
  • Bdd/Targets/Windows/BddTargetWindows.c
  • CLAUDE.md
  • CMakeLists.txt
  • Core/Interface/SolidSyslogCircularBuffer.h
  • Core/Interface/SolidSyslogTunablesDefaults.h
  • Core/Source/CMakeLists.txt
  • Core/Source/SolidSyslogCircularBuffer.c
  • Core/Source/SolidSyslogCircularBufferPrivate.h
  • Core/Source/SolidSyslogCircularBufferStatic.c
  • Core/Source/SolidSyslogErrorMessages.h
  • DEVLOG.md
  • README.md
  • SKILL.md
  • Tests/SolidSyslogBlockStoreDrainOrderingTest.cpp
  • Tests/SolidSyslogCircularBufferTest.cpp
  • Tests/SolidSyslogTest.cpp
  • docs/NAMING.md
  • docs/iec62443.md
  • docs/misra-deviations.md
  • misra_suppressions.txt

Comment on lines +116 to +127
void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base)
{
size_t poolIndex = CircularBuffer_IndexFromHandle(base);
bool released = false;
if (CircularBuffer_PoolIndexIsValid(poolIndex))
{
released = CircularBuffer_FreeIfInUse(poolIndex);
}
if (!released)
{
SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_WARNING, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY);
}

@coderabbitai coderabbitai Bot May 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Destroy warns on fallback handles returned by Create.

Line 61 allows Create to return &Fallback, but Lines 120–127 can never mark that handle as released, so Destroy emits SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY for a valid CreateDestroy flow under pool exhaustion.

Proposed fix
 void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base)
 {
     size_t poolIndex = CircularBuffer_IndexFromHandle(base);
     bool released = false;
-    if (CircularBuffer_PoolIndexIsValid(poolIndex))
+    if (base == &Fallback)
+    {
+        released = true;
+    }
+    else if (CircularBuffer_PoolIndexIsValid(poolIndex))
     {
         released = CircularBuffer_FreeIfInUse(poolIndex);
     }
     if (!released)
     {
         SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_WARNING, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY);
     }
 }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the designed behaviour, not a bug — declining the suggestion.

Earlier in this branch (commit 6500440, "feat: S11.01 drop Fallback special-case from _Destroy") we deliberately removed the if (base == &Fallback) return; short-circuit from _Destroy and unified the contract to:

any handle that is not currently issued by _Create reports SOLIDSYSLOG_SEVERITY_WARNING — regardless of whether it was once issued (pool-exhaustion Fallback, double-Destroy) or never was (stack-fabricated stranger).

The rationale, captured in that commit message and in the DEVLOG entry:

  1. Simpler contract for the integrator: one rule covers all "didn't come from a live _Create" cases. The Fallback exception was load-bearing only for the silent-on-Fallback property — which is itself questionable: a Fallback handle means the system hit pool exhaustion at some point, and the integrator destroying it is a useful place to surface that information one more time.
  2. _Destroy reads at one level of abstraction without the special case — that one-indent-flatter shape is the reference for every other E11 class.
  3. The Fallback is a no-op handle that never accepts Writes or returns data on Reads, so the WARNING does not signal any data loss at Destroy time — it only flags the prior exhaustion.

The error handler is null-object by default, so an integrator who has not installed a handler sees nothing at all; one who has, sees the same uniform message for any unknown handle. Pinned in tests DestroyOfUnknownHandleReportsWarning and DestroyOfStaleHandleReportsWarning.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DavidCozens, understood — withdrawing the suggestion. The unified "any handle not currently issued by _Create emits WARNING" contract is well-reasoned: it removes a Fallback special-case that would have diverged from the reference E11 shape, surfaces the prior pool-exhaustion event at destroy time without implying data loss, and keeps the null-object default handler harmless for integrators who haven't installed one. The test coverage in DestroyOfUnknownHandleReportsWarning and DestroyOfStaleHandleReportsWarning makes the contract explicit.

(´• ω •`) nods and hops away


✏️ Learnings added
Learnt from: DavidCozens
Repo: DavidCozens/solid-syslog PR: 394
File: Core/Source/SolidSyslogCircularBufferStatic.c:116-127
Timestamp: 2026-05-18T05:42:33.698Z
Learning: In DavidCozens/solid-syslog, `SolidSyslogCircularBuffer_Destroy` (Core/Source/SolidSyslogCircularBufferStatic.c) intentionally emits `SOLIDSYSLOG_SEVERITY_WARNING` / `SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY` for ALL handles not currently issued by `_Create`, including pool-exhaustion Fallback handles. This is the designed E11 reference contract: one uniform rule covers Fallback handles, double-Destroy, and stranger handles — no Fallback short-circuit. Do not flag this as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: DavidCozens
Repo: DavidCozens/solid-syslog PR: 283
File: Platform/OpenSsl/Source/SolidSyslogTlsStream.c:363-363
Timestamp: 2026-05-07T08:22:59.856Z
Learning: In this solid-syslog codebase, follow the documented contract: do not add NULL/defensive checks inside library/internal code paths (e.g., `_Create` functions) when the NULL case cannot occur under normal internal usage and is covered by framework or internal wiring guarantees. Only add validation at true system boundaries such as user input and external APIs. Where setup-time wiring contracts between the integrator and the library require specific function-pointer fields (e.g., `SolidSyslogTlsStreamConfig.sleep`), assume those required fields are present and intentionally omit NULL guards in internal creation functions.

Learnt from: DavidCozens
Repo: DavidCozens/solid-syslog PR: 291
File: Example/FreeRtos/HelloWorld/CmsdkUart.c:17-42
Timestamp: 2026-05-08T17:19:38.382Z
Learning: In this codebase (DavidCozens/solid-syslog), internal driver/config/access structs may intentionally store raw pointers without NULL checks, `isInitialized` flags, or defensive struct copies. Treat this as an explicit project policy: do not flag store-by-pointer usage or missing NULL/initialization guards for these internal config/access structs when the pointer is provided by production callers and is guaranteed to point to `static const` objects with program lifetime (per CLAUDE.md). Only flag NULL/initialization issues when the pointer originates from potentially invalid/external/untrusted sources or when the code does not establish lifetime/guarantees.

Learnt from: DavidCozens
Repo: DavidCozens/solid-syslog PR: 303
File: Example/Common/ExampleInteractive.h:14-17
Timestamp: 2026-05-09T15:56:13.853Z
Learning: During code review, ensure solid-syslog Tier 1/2 library callbacks in Core/ and Platform/ follow the CLAUDE.md “Callback Conventions”: the callback API must take a `void*` context parameter and the implementation must use the paired configuration field associated with that callback context. Do not apply this requirement to Tier 3 example handlers (e.g., Example/Common/, Example/FreeRtos/, such as `ExampleInteractiveSwitchHandler`/`ExampleInteractiveSetHandler`), which are allowed to use file-static globals per the documented example-tier pattern. If a multi-instance use-case arises, migrate Switch and Set together in a single cohesive change rather than piecemeal updates.

Learnt from: DavidCozens
Repo: DavidCozens/solid-syslog PR: 372
File: Platform/FreeRtos/Source/SolidSyslogFreeRtosDatagram.c:0-0
Timestamp: 2026-05-15T13:09:11.811Z
Learning: When reviewing PRs that bulk-rename identifiers (especially “static function rename” changes) in this repo, check for unintended renames inside FreeRTOS macro usages. A too-broad regex can incorrectly prefix macro tokens used in `static const` initializers (e.g., `pdMS_TO_TICKS` from FreeRTOS). Look for signs like a renamed macro name being incorrectly prefixed (e.g., `FreeRtosDatagram_pdMS_TO_TICKS`), and if present, treat it as evidence the identifier-extraction regex was overly broad and requires correction/revert of the affected rename changes.

Comment thread docs/misra-deviations.md Outdated
Comment thread misra_suppressions.txt
misra-c2012-11.3:Core/Source/SolidSyslogBlockStore.c:165
misra-c2012-11.3:Core/Source/SolidSyslogCircularBuffer.c:72
misra-c2012-11.3:Core/Source/SolidSyslogCircularBuffer.c:89
misra-c2012-11.3:Core/Source/SolidSyslogCircularBuffer.c:91

@coderabbitai coderabbitai Bot May 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate CircularBuffer implementation =="
fd -a 'SolidSyslogCircularBuffer.c$'

echo
echo "== Show the suppressed line and nearby context (expect line 91 to contain the cast/findings site) =="
nl -ba Core/Source/SolidSyslogCircularBuffer.c | sed -n '78,110p'

echo
echo "== Show cast helper patterns in CircularBuffer implementation =="
rg -n -C2 --type=c 'SelfFromBase|SelfFromStorage|\(struct SolidSyslogCircularBuffer\*\)' Core/Source/SolidSyslogCircularBuffer.c

echo
echo "== Show D.002 text for scope comparison =="
nl -ba docs/misra-deviations.md | sed -n '140,205p'

Repository: DavidCozens/solid-syslog

Length of output: 333


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Show the suppressed line and nearby context (lines 78-110) =="
cat -n Core/Source/SolidSyslogCircularBuffer.c | sed -n '78,110p'

echo
echo "== Show cast patterns in CircularBuffer (SelfFromBase|SelfFromStorage|cast to struct SolidSyslogCircularBuffer*) =="
rg -n -C2 --type=c 'SelfFromBase|SelfFromStorage|\(struct SolidSyslogCircularBuffer\*\)' Core/Source/SolidSyslogCircularBuffer.c

echo
echo "== Show D.002 deviation section from misra-deviations.md =="
cat -n docs/misra-deviations.md | sed -n '140,205p'

echo
echo "== Verify the exact suppression entry in misra_suppressions.txt at line 27 =="
cat -n misra_suppressions.txt | sed -n '20,35p'

Repository: DavidCozens/solid-syslog

Length of output: 7428


Remove this suppression or realign it to a non-D.002 justification.

docs/misra-deviations.md D.002 explicitly excludes SolidSyslogCircularBuffer from its scope (lines 168–173: "moved off this pattern under E11 (S11.01)... the instance struct now lives in a library-internal static pool"). However, the Rule 11.3 suppression at line 91 remains attributed to D.002. The cast (struct SolidSyslogCircularBuffer*) base in CircularBuffer_SelfFromBase is a base-to-derived pointer cast that is no longer covered by the deviation. Either remove the suppression and refactor the code to eliminate the cast, or document a separate justification if the cast is intentionally retained.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@misra_suppressions.txt` at line 27, The suppression for misra-c2012-11.3
attributed to D.002 referring to SolidSyslogCircularBuffer must be removed or
rejustified: either delete the line
"misra-c2012-11.3:Core/Source/SolidSyslogCircularBuffer.c:91" from
misra_suppressions.txt and refactor CircularBuffer_SelfFromBase to avoid the
base-to-derived cast (replace the cast with a safe accessor or container/lookup
that returns a SolidSyslogCircularBuffer* without casting), or keep a
suppression but update its justification to a correct deviation entry (not
D.002) and add a matching explanation in docs/misra-deviations.md referencing
SolidSyslogCircularBuffer and the reason for the base-to-derived cast; ensure
the change references the function CircularBuffer_SelfFromBase and the
SolidSyslogCircularBuffer type so reviewers can locate the fix.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted as a documentation update in 9bd1d39 — the suppression line itself stays put.

The cast inside CircularBuffer_SelfFromBase is the vtable downcast (struct SolidSyslogBuffer*struct SolidSyslogCircularBuffer*), not the caller-storage void* cast that E11 retired. That shape is shared by every other class listed in this D.002 entry — they all have an equivalent SelfFromBase cast for the same reason — so the deviation continues to cover it. The corresponding paragraph in docs/misra-deviations.md now says so explicitly, distinguishing the vtable-downcast concern from the (now-gone) caller-storage concern for CircularBuffer.

Removing the cast would mean abandoning the vtable interface (SolidSyslogBuffer_Read / _Write dispatch through a struct SolidSyslogBuffer*), which would ripple through every consumer — not a S11.01 change. If the deviation should be split into two separate blocks (caller-storage vs vtable-downcast) for the whole library, that's a follow-up worth doing across the codebase rather than just this entry.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DavidCozens — understood, and the clarification is well-reasoned. The vtable downcast in CircularBuffer_SelfFromBase is structurally identical to the SelfFromBase pattern in every other class under D.002, so the deviation continues to cover it. My original comment incorrectly conflated the two distinct cast concerns: the (now-retired) caller-storage void* cast and the (still-present) vtable downcast. The documentation update in 9bd1d39 making that distinction explicit is exactly the right fix. Agreed that splitting D.002 into two blocks (caller-storage vs. vtable-downcast) for the whole library is a sensible follow-up rather than something to do here.


✏️ Learnings added
Learnt from: DavidCozens
Repo: DavidCozens/solid-syslog PR: 394
File: misra_suppressions.txt:27-27
Timestamp: 2026-05-18T05:42:55.403Z
Learning: In the solid-syslog codebase (`misra_suppressions.txt` and `docs/misra-deviations.md`), D.002 (Rules 11.2/11.3/11.5) covers two distinct cast patterns: (1) the vtable downcast (`struct SolidSyslogBuffer*` → `struct SolidSyslogCircularBuffer*` via `CircularBuffer_SelfFromBase`, and analogous `SelfFromBase` helpers in every other class) which is retained for all classes including those migrated under E11; and (2) the caller-storage `void*` cast which E11-migrated classes (currently only `SolidSyslogCircularBuffer`) no longer use. Do not flag the `SelfFromBase` vtable downcast as uncovered by D.002 — it is explicitly documented in `docs/misra-deviations.md` (updated in commit 9bd1d39 of PR `#394`). A potential future refactor to split D.002 into two separate blocks for the whole library was noted as out of scope for S11.01.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Addresses the analyze-iwyu CI failure and the two actionable
CodeRabbit comments worth acting on:

- Tests/SolidSyslogCircularBufferTest.cpp: explicit `#include <stdint.h>`
  for uint8_t. The new TEST_GROUP_BASE-derived groups declare
  `uint8_t ring[...]` directly; uint8_t previously came in transitively
  through a SolidSyslog header. (IWYU finding.)

- scripts/iwyu_filter.py: extend the filter to drop CppUTest
  macro-generated `class TEST_<Group>_<Test>_Test;` forward-decl
  additions. Same category of false positive as the existing
  `- #include "CppUTest/TestHarness.h"` filter -- IWYU doesn't model
  CppUTest's TEST() / TEST_GROUP_BASE() macro expansion. Filter
  smoke-tested locally; running IWYU in the clang container reports
  every TU "has correct #includes/fwd-decls" with no actionable
  findings.

- docs/misra-deviations.md (RING_BYTES section): reword "caller-supplied
  ring memory in messages" -> "caller-supplied ring buffer in bytes,
  derived from a maximum message count" with a worked example. The
  macro returns bytes; the old wording was unit-ambiguous.
  (CodeRabbit minor.)

- docs/misra-deviations.md (D.002 section): extend the E11-exception
  paragraph to explicitly note that the surviving 11.3 suppression for
  Core/Source/SolidSyslogCircularBuffer.c covers the vtable downcast
  inside CircularBuffer_SelfFromBase, not the caller-storage void*
  cast that's now gone. Same deviation block continues to cover both
  shapes because both are 11.3 firings driven by the same "interface
  decoupled from concrete type" design. (CodeRabbit major -- accepted
  as a documentation/comment update rather than removal; the cast
  itself is the standard OO-in-C vtable downcast that every other
  class in the deviation already shares.)

The three other CodeRabbit findings (Destroy-warns-on-Fallback,
function-order, "unused" SolidSyslogPrival.h) are addressed by reply
on the PR rather than code change -- see PR #394 thread.

1127 tests pass; IWYU clean; cppcheck-misra count stays at 87.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@DavidCozens

Copy link
Copy Markdown
Owner Author

Going through the two nits in the review summary, both declined with rationale:

Static.c:59-114 — "place _Destroy immediately after _Create"

The convention in this repo is "helpers defined immediately beneath the function that first calls them", established in FileBlockDevice.c and recorded under "Function Ordering" in CLAUDE.md:

Helper functions are forward-declared at the top of the file (after constants/types, before the first definition), usually static inline, and defined immediately beneath the function that first calls them. If a second public function also calls that helper, the helper stays where it was — with its first caller.

AcquireFirstFree is first-called from _Create, so it sits between _Create and _Destroy. Reordering to put _Destroy second would move helpers below _Destroy away from their first caller and break the convention. Same pattern as SolidSyslogFileBlockDevice.c (helpers like SelfFromStorage, InitialiseVtable are interleaved between _Create and _Destroy in that file too).

Tests/SolidSyslogCircularBufferTest.cpp:13 — "remove unused SolidSyslogPrival.h`"

It is used — provides SOLIDSYSLOG_SEVERITY_ERROR and SOLIDSYSLOG_SEVERITY_WARNING, which the pool tests reference:

$ grep -n SOLIDSYSLOG_SEVERITY Tests/SolidSyslogCircularBufferTest.cpp
416:    LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_ERROR, ErrorHandlerFake_LastSeverity());
484:    LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_WARNING, ErrorHandlerFake_LastSeverity());
498:    LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_WARNING, ErrorHandlerFake_LastSeverity());

Three test bodies (ExhaustedCreateReportsError, DestroyOfUnknownHandleReportsWarning, DestroyOfStaleHandleReportsWarning) would fail to compile without the include.

@github-actions

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   build-linux-gcc: 100% successful (✔️ 1133 passed, 🙈 2 skipped)
   🚦   build-freertos-host-tdd: 100% successful (✔️ 1253 passed, 🙈 2 skipped)
   🚦   build-linux-clang: 100% successful (✔️ 1085 passed, 🙈 2 skipped)
   🚦   sanitize-linux-gcc: 100% successful (✔️ 1085 passed, 🙈 2 skipped)
   🚦   integration-linux-openssl: 100% successful (✔️ 9 passed)
   🚦   integration-windows-openssl: 100% successful (✔️ 9 passed)
   🚦   bdd-linux-syslog-ng: 94% successful (✔️ 46 passed, 🙈 3 skipped)
   🚦   bdd-windows-otel: 90% successful (✔️ 44 passed, 🙈 5 skipped)
   🚦   bdd-freertos-qemu: 73% successful (✔️ 36 passed, 🙈 13 skipped)
   🚦   build-windows-msvc: 100% successful (✔️ 989 passed, 🙈 1 skipped)
   🚦   build-linux-tunable-override: 100% successful (✔️ 1085 passed, 🙈 2 skipped)
   ⚠️   Clang-Tidy: 10 warnings (normal: 10)
   ⚠️   CPPCheck: No warnings


Created by Quality Monitor v1.14.0 (#f3859fd). More details are shown in the GitHub Checks Result.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
scripts/iwyu_filter.py (1)

128-135: ⚡ Quick win

Use descriptive variable names in list comprehensions.

The variable name l is ambiguous and can be confused with 1 or I. Use a more descriptive name like line or addition.

📝 Proposed fix for improved readability
-    add_real = [
-        l for l in add_lines
-        if l.strip() and not _is_filtered_addition(l)
-    ]
-    remove_real = [
-        l for l in remove_lines
-        if l.strip() and not _is_filtered_removal(l)
-    ]
+    add_real = [
+        line for line in add_lines
+        if line.strip() and not _is_filtered_addition(line)
+    ]
+    remove_real = [
+        line for line in remove_lines
+        if line.strip() and not _is_filtered_removal(line)
+    ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/iwyu_filter.py` around lines 128 - 135, Rename ambiguous
comprehension iteration variables from `l` to descriptive names: use `line` (or
`addition`/`removal`) in the comprehensions that build `add_real` and
`remove_real`. Update the two list comprehensions that iterate over `add_lines`
and `remove_lines` and call `_is_filtered_addition` / `_is_filtered_removal` so
they use `line` (e.g., `for line in add_lines`) instead of `l` to improve
readability.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@scripts/iwyu_filter.py`:
- Around line 128-135: Rename ambiguous comprehension iteration variables from
`l` to descriptive names: use `line` (or `addition`/`removal`) in the
comprehensions that build `add_real` and `remove_real`. Update the two list
comprehensions that iterate over `add_lines` and `remove_lines` and call
`_is_filtered_addition` / `_is_filtered_removal` so they use `line` (e.g., `for
line in add_lines`) instead of `l` to improve readability.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b054c7ca-c96e-4eab-af4c-e064618e567b

📥 Commits

Reviewing files that changed from the base of the PR and between 286194b and 9bd1d39.

📒 Files selected for processing (3)
  • Tests/SolidSyslogCircularBufferTest.cpp
  • docs/misra-deviations.md
  • scripts/iwyu_filter.py
✅ Files skipped from review due to trivial changes (1)
  • docs/misra-deviations.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • Tests/SolidSyslogCircularBufferTest.cpp

@DavidCozens

Copy link
Copy Markdown
Owner Author

One more validation step done locally: full unit test suite re-run with SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE = 3 (via a temporary tunables override file). All 1127 tests pass.

echo '#define SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE 3' > /tmp/pool3-tunables.h
cmake -B build/proof-pool-3 -DCMAKE_BUILD_TYPE=Debug \
      -DSOLIDSYSLOG_USER_TUNABLES_FILE=/tmp/pool3-tunables.h
cmake --build build/proof-pool-3 --target SolidSyslogTests
build/proof-pool-3/Tests/SolidSyslogTests
...
OK (1129 tests, 1127 ran, 2481 checks, 2 ignored, 0 filtered out, 23 ms)

Check count moves 2477 → 2481 (+4) — that's CHECK_IS_FALLBACK iterating two extra pool slots × two CHECK_TEXT per slot. The interesting bit is that CreateLocksOncePerSlotProbedWhenPoolIsFull is genuinely observing per-iteration locking: at pool size 3 it asserts and gets 3 lock acquisitions, whereas at default pool size 1 it gets 1. A future regression to "lock once around the loop" in _Create would now fail this test at any tunable-override build with pool size > 1 — exactly what was asked for when the lock-duration concern surfaced earlier in the branch.

That clears the last bullet from my work list for S11.01. Ready when you are.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

S11.01: Pilot — CircularBuffer pool migration (E11 canonical pattern)

1 participant