From 0c6cd4df24d71bb22f46728f3c23bb0e31ffe691 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 18 May 2026 08:24:24 +0100 Subject: [PATCH 1/3] feat: S11.02 add SolidSyslogPoolAllocator helper TU-internal helper that owns the slot-walk and per-iteration LockConfig over a caller-supplied bool[]. Two operations + a static-inline predicate: - AcquireFirstFree: locks per probe; first claim wins; Count on exhaustion. - FreeIfInUse: locks once; invokes cleanup INSIDE the lock before clearing InUse; the lock-held-during-cleanup invariant prevents a concurrent Acquire from grabbing the slot mid-cleanup and racing Initialise vs Cleanup on the same memory. - IndexIsValid: static-inline predicate over Count. Not wired into CircularBufferStatic yet -- the migration is the next commit. 100% line coverage; zero new MISRA findings. Refs #395. --- Core/Source/CMakeLists.txt | 1 + Core/Source/SolidSyslogPoolAllocator.c | 75 +++++++++++ Core/Source/SolidSyslogPoolAllocator.h | 35 ++++++ Tests/CMakeLists.txt | 1 + Tests/SolidSyslogPoolAllocatorTest.cpp | 164 +++++++++++++++++++++++++ 5 files changed, 276 insertions(+) create mode 100644 Core/Source/SolidSyslogPoolAllocator.c create mode 100644 Core/Source/SolidSyslogPoolAllocator.h create mode 100644 Tests/SolidSyslogPoolAllocatorTest.cpp diff --git a/Core/Source/CMakeLists.txt b/Core/Source/CMakeLists.txt index abb990e6..09dc424c 100644 --- a/Core/Source/CMakeLists.txt +++ b/Core/Source/CMakeLists.txt @@ -9,6 +9,7 @@ set(SOURCES SolidSyslogNullBuffer.c SolidSyslogCircularBuffer.c SolidSyslogCircularBufferStatic.c + SolidSyslogPoolAllocator.c SolidSyslogMutex.c SolidSyslogNullMutex.c SolidSyslogSender.c diff --git a/Core/Source/SolidSyslogPoolAllocator.c b/Core/Source/SolidSyslogPoolAllocator.c new file mode 100644 index 00000000..7170d1fd --- /dev/null +++ b/Core/Source/SolidSyslogPoolAllocator.c @@ -0,0 +1,75 @@ +#include "SolidSyslogPoolAllocator.h" + +#include + +#include "SolidSyslogConfigLock.h" + +static bool PoolAllocator_TryClaim(struct SolidSyslogPoolAllocator* self, size_t index); +static inline bool PoolAllocator_SlotIsFree(const struct SolidSyslogPoolAllocator* self, size_t index); +static inline bool PoolAllocator_SlotIsInUse(const struct SolidSyslogPoolAllocator* self, size_t index); +static inline void PoolAllocator_MarkInUse(struct SolidSyslogPoolAllocator* self, size_t index); +static inline void PoolAllocator_MarkFree(struct SolidSyslogPoolAllocator* self, size_t index); + +size_t SolidSyslogPoolAllocator_AcquireFirstFree(struct SolidSyslogPoolAllocator* self) +{ + size_t acquired = 0; + while ((acquired < self->Count) && !PoolAllocator_TryClaim(self, acquired)) + { + acquired++; + } + return acquired; +} + +static bool PoolAllocator_TryClaim(struct SolidSyslogPoolAllocator* self, size_t index) +{ + bool claimed = false; + SolidSyslog_LockConfig(); + if (PoolAllocator_SlotIsFree(self, index)) + { + PoolAllocator_MarkInUse(self, index); + claimed = true; + } + SolidSyslog_UnlockConfig(); + return claimed; +} + +static inline bool PoolAllocator_SlotIsFree(const struct SolidSyslogPoolAllocator* self, size_t index) +{ + return !PoolAllocator_SlotIsInUse(self, index); +} + +static inline bool PoolAllocator_SlotIsInUse(const struct SolidSyslogPoolAllocator* self, size_t index) +{ + return self->InUse[index]; +} + +static inline void PoolAllocator_MarkInUse(struct SolidSyslogPoolAllocator* self, size_t index) +{ + self->InUse[index] = true; +} + +bool SolidSyslogPoolAllocator_FreeIfInUse( + struct SolidSyslogPoolAllocator* self, + size_t index, + SolidSyslogPoolCleanup cleanup, + void* context +) +{ + SolidSyslog_LockConfig(); + bool released = PoolAllocator_SlotIsInUse(self, index); + if (released) + { + if (cleanup != NULL) + { + cleanup(index, context); + } + PoolAllocator_MarkFree(self, index); + } + SolidSyslog_UnlockConfig(); + return released; +} + +static inline void PoolAllocator_MarkFree(struct SolidSyslogPoolAllocator* self, size_t index) +{ + self->InUse[index] = false; +} diff --git a/Core/Source/SolidSyslogPoolAllocator.h b/Core/Source/SolidSyslogPoolAllocator.h new file mode 100644 index 00000000..c52ac49a --- /dev/null +++ b/Core/Source/SolidSyslogPoolAllocator.h @@ -0,0 +1,35 @@ +#ifndef SOLIDSYSLOGPOOLALLOCATOR_H +#define SOLIDSYSLOGPOOLALLOCATOR_H + +#include "ExternC.h" + +#include +#include + +EXTERN_C_BEGIN + + struct SolidSyslogPoolAllocator + { + bool* InUse; + size_t Count; + }; + + typedef void (*SolidSyslogPoolCleanup)(size_t index, void* context); + + size_t SolidSyslogPoolAllocator_AcquireFirstFree(struct SolidSyslogPoolAllocator * self); + + bool SolidSyslogPoolAllocator_FreeIfInUse( + struct SolidSyslogPoolAllocator * self, + size_t index, + SolidSyslogPoolCleanup cleanup, + void* context + ); + + static inline bool SolidSyslogPoolAllocator_IndexIsValid(const struct SolidSyslogPoolAllocator* self, size_t index) + { + return index < self->Count; + } + +EXTERN_C_END + +#endif /* SOLIDSYSLOGPOOLALLOCATOR_H */ diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index e27a66fa..88ea219a 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -25,6 +25,7 @@ set(TEST_SOURCES SolidSyslogOriginSdTest.cpp SolidSyslogNullBufferTest.cpp SolidSyslogCircularBufferTest.cpp + SolidSyslogPoolAllocatorTest.cpp SolidSyslogNullMutexTest.cpp SolidSyslogNullStoreTest.cpp SolidSyslogNullSecurityPolicyTest.cpp diff --git a/Tests/SolidSyslogPoolAllocatorTest.cpp b/Tests/SolidSyslogPoolAllocatorTest.cpp new file mode 100644 index 00000000..51cfd203 --- /dev/null +++ b/Tests/SolidSyslogPoolAllocatorTest.cpp @@ -0,0 +1,164 @@ +#include + +#include "CppUTest/TestHarness.h" + +#include "ConfigLockFake.h" +#include "SolidSyslogPoolAllocator.h" +#include "TestUtils.h" + +using namespace CososoTesting; // NOLINT(google-build-using-namespace) -- test-file scope only; brings ONCE/NEVER into scope for CALLED_FAKE + +namespace +{ +constexpr size_t TEST_POOL_SIZE = 3; +} + +static int cleanupSpyCallCount; +static size_t cleanupSpyLastIndex; +static int cleanupSpyLockDeltaAtCall; + +static void CleanupSpy(size_t index, void* context) +{ + (void) context; + cleanupSpyCallCount++; + cleanupSpyLastIndex = index; + cleanupSpyLockDeltaAtCall = ConfigLockFake_LockCallCount() - ConfigLockFake_UnlockCallCount(); +} + +// clang-format off +TEST_GROUP(SolidSyslogPoolAllocator) +{ + bool inUse[TEST_POOL_SIZE] = {}; + struct SolidSyslogPoolAllocator allocator = {inUse, TEST_POOL_SIZE}; + + void setup() override + { + cleanupSpyCallCount = 0; + cleanupSpyLastIndex = 0; + cleanupSpyLockDeltaAtCall = 0; + } + + void teardown() override + { + ConfigLockFake_Uninstall(); + } + + void FillPool() + { + for (auto& flag : inUse) + { + flag = true; + } + } +}; + +// clang-format on + +TEST(SolidSyslogPoolAllocator, IndexAtCountIsInvalid) +{ + CHECK_FALSE(SolidSyslogPoolAllocator_IndexIsValid(&allocator, TEST_POOL_SIZE)); +} + +TEST(SolidSyslogPoolAllocator, IndexBelowCountIsValid) +{ + CHECK_TRUE(SolidSyslogPoolAllocator_IndexIsValid(&allocator, TEST_POOL_SIZE - 1)); +} + +TEST(SolidSyslogPoolAllocator, AcquireFirstFreeOnEmptyPoolReturnsZeroAndMarksItInUse) +{ + size_t index = SolidSyslogPoolAllocator_AcquireFirstFree(&allocator); + + LONGS_EQUAL(0, index); + CHECK_TRUE(inUse[0]); +} + +TEST(SolidSyslogPoolAllocator, AcquireFirstFreeWalksPastInUseSlots) +{ + inUse[0] = true; + + size_t index = SolidSyslogPoolAllocator_AcquireFirstFree(&allocator); + + LONGS_EQUAL(1, index); + CHECK_TRUE(inUse[1]); +} + +TEST(SolidSyslogPoolAllocator, AcquireFirstFreeOnExhaustedPoolReturnsCount) +{ + FillPool(); + + size_t index = SolidSyslogPoolAllocator_AcquireFirstFree(&allocator); + + LONGS_EQUAL(TEST_POOL_SIZE, index); +} + +TEST(SolidSyslogPoolAllocator, FreeIfInUseOnInUseSlotReleasesItAndInvokesCleanup) +{ + inUse[1] = true; + + bool released = SolidSyslogPoolAllocator_FreeIfInUse(&allocator, 1, CleanupSpy, nullptr); + + CHECK_TRUE(released); + CHECK_FALSE(inUse[1]); + CALLED_FUNCTION(cleanupSpy, ONCE); + LONGS_EQUAL(1, cleanupSpyLastIndex); +} + +TEST(SolidSyslogPoolAllocator, FreeIfInUseOnAlreadyFreeSlotReturnsFalseAndSkipsCleanup) +{ + bool released = SolidSyslogPoolAllocator_FreeIfInUse(&allocator, 0, CleanupSpy, nullptr); + + CHECK_FALSE(released); + CALLED_FUNCTION(cleanupSpy, NEVER); +} + +TEST(SolidSyslogPoolAllocator, FreeIfInUseWithNullCleanupStillReleasesSlot) +{ + inUse[2] = true; + + bool released = SolidSyslogPoolAllocator_FreeIfInUse(&allocator, 2, nullptr, nullptr); + + CHECK_TRUE(released); + CHECK_FALSE(inUse[2]); +} + +TEST(SolidSyslogPoolAllocator, AcquireFirstFreeLocksOnceForOneProbedSlot) +{ + ConfigLockFake_Install(); + + SolidSyslogPoolAllocator_AcquireFirstFree(&allocator); + + CALLED_FAKE(ConfigLockFake_Lock, ONCE); + CALLED_FAKE(ConfigLockFake_Unlock, ONCE); +} + +TEST(SolidSyslogPoolAllocator, AcquireFirstFreeLocksOncePerSlotWhenPoolIsFull) +{ + FillPool(); + ConfigLockFake_Install(); + + SolidSyslogPoolAllocator_AcquireFirstFree(&allocator); + + LONGS_EQUAL(TEST_POOL_SIZE, ConfigLockFake_LockCallCount()); + LONGS_EQUAL(TEST_POOL_SIZE, ConfigLockFake_UnlockCallCount()); +} + +TEST(SolidSyslogPoolAllocator, FreeIfInUseLocksOnce) +{ + inUse[1] = true; + ConfigLockFake_Install(); + + SolidSyslogPoolAllocator_FreeIfInUse(&allocator, 1, CleanupSpy, nullptr); + + CALLED_FAKE(ConfigLockFake_Lock, ONCE); + CALLED_FAKE(ConfigLockFake_Unlock, ONCE); +} + +TEST(SolidSyslogPoolAllocator, FreeIfInUseInvokesCleanupWhileHoldingTheLock) +{ + inUse[1] = true; + ConfigLockFake_Install(); + + SolidSyslogPoolAllocator_FreeIfInUse(&allocator, 1, CleanupSpy, nullptr); + + LONGS_EQUAL(1, cleanupSpyLockDeltaAtCall); +} From f4eb730ec49fbd8fc51d10b42b7fd0129864361c Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 18 May 2026 08:28:53 +0100 Subject: [PATCH 2/3] refactor: S11.02 migrate CircularBufferStatic onto PoolAllocator Drops 10 file-scope helpers (AcquireFirstFree/AcquireIfFree/Acquire/ PoolItemIsFree/PoolItemIsInUse/MarkInUse/MarkFree/HandleFromIndex/ HandleIsValid/FreeIfInUse) and retires the `struct Slot` wrapper. `Pool` is now a bare array of SolidSyslogCircularBuffer; the `InUse[]` flags live alongside it and are owned by a TU-static `SolidSyslogPoolAllocator` instance. Public API (_Create / _Destroy signatures) and per-instance state (SolidSyslogCircularBuffer struct, Initialise / Cleanup contracts) are byte-identical. The existing SolidSyslogCircularBufferTest suite passes unchanged, including the lock-count assertions that pin the per-probe LockConfig invariant -- the helper preserves that behaviour exactly. What stays per-class: IndexFromHandle (the reverse lookup is the one genuinely typed bit), CleanupAtIndex (3-line bridge from the helper's typeless callback to CircularBuffer_Cleanup), and the Fallback vtable. Two helpers plus two vtable entries -- the rest is the public API. Validated by running the full test suite at SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE=3 (1139 tests pass; default POOL_SIZE=1 also green). Zero new cppcheck-misra findings (count 60 vs 61 pre-migration -- one fewer line eligible for an 8.9 diagnostic). 100% line coverage on both SolidSyslogCircularBufferStatic.c and SolidSyslogPoolAllocator.c. Refs #395. --- Core/Source/SolidSyslogCircularBufferStatic.c | 124 +++--------------- 1 file changed, 17 insertions(+), 107 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 836e274a..b76b3fbf 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -6,37 +6,23 @@ #include "SolidSyslogBufferDefinition.h" #include "SolidSyslogCircularBufferPrivate.h" -#include "SolidSyslogConfigLock.h" #include "SolidSyslogError.h" #include "SolidSyslogErrorMessages.h" +#include "SolidSyslogPoolAllocator.h" #include "SolidSyslogPrival.h" #include "SolidSyslogTunables.h" struct SolidSyslogMutex; -struct Slot -{ - struct SolidSyslogCircularBuffer Object; - bool InUse; -}; - static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead); static void Fallback_Write(struct SolidSyslogBuffer* base, const void* data, size_t size); -static struct SolidSyslogBuffer* CircularBuffer_AcquireFirstFree(void); -static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t poolIndex); -static inline bool CircularBuffer_PoolItemIsFree(size_t poolIndex); -static inline bool CircularBuffer_PoolItemIsInUse(size_t poolIndex); -static inline struct SolidSyslogBuffer* CircularBuffer_Acquire(size_t poolIndex); -static inline void CircularBuffer_MarkInUse(size_t poolIndex); -static inline struct SolidSyslogBuffer* CircularBuffer_HandleFromIndex(size_t poolIndex); -static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* handle); static size_t CircularBuffer_IndexFromHandle(const struct SolidSyslogBuffer* base); -static inline bool CircularBuffer_PoolIndexIsValid(size_t poolIndex); -static bool CircularBuffer_FreeIfInUse(size_t poolIndex); -static inline void CircularBuffer_MarkFree(size_t poolIndex); +static void CircularBuffer_CleanupAtIndex(size_t index, void* context); -static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; +static bool InUse[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; +static struct SolidSyslogCircularBuffer Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; static struct SolidSyslogBuffer Fallback = {Fallback_Write, Fallback_Read}; +static struct SolidSyslogPoolAllocator Allocator = {InUse, SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE}; struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( struct SolidSyslogMutex* mutex, @@ -44,10 +30,12 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( size_t ringBytes ) { - struct SolidSyslogBuffer* handle = CircularBuffer_AcquireFirstFree(); - if (CircularBuffer_HandleIsValid(handle)) + size_t index = SolidSyslogPoolAllocator_AcquireFirstFree(&Allocator); + struct SolidSyslogBuffer* handle = &Fallback; + if (SolidSyslogPoolAllocator_IndexIsValid(&Allocator, index)) { - CircularBuffer_Initialise(handle, mutex, ring, ringBytes); + CircularBuffer_Initialise(&Pool[index].Base, mutex, ring, ringBytes); + handle = &Pool[index].Base; } else { @@ -56,71 +44,11 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( return handle; } -static struct SolidSyslogBuffer* CircularBuffer_AcquireFirstFree(void) -{ - struct SolidSyslogBuffer* handle = &Fallback; - for (size_t poolIndex = 0; poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; poolIndex++) - { - handle = CircularBuffer_AcquireIfFree(poolIndex); - if (CircularBuffer_HandleIsValid(handle)) - { - break; - } - } - return handle; -} - -static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t poolIndex) -{ - struct SolidSyslogBuffer* handle = &Fallback; - SolidSyslog_LockConfig(); - if (CircularBuffer_PoolItemIsFree(poolIndex)) - { - handle = CircularBuffer_Acquire(poolIndex); - } - SolidSyslog_UnlockConfig(); - return handle; -} - -static inline bool CircularBuffer_PoolItemIsFree(size_t poolIndex) -{ - return !CircularBuffer_PoolItemIsInUse(poolIndex); -} - -static inline bool CircularBuffer_PoolItemIsInUse(size_t poolIndex) -{ - return Pool[poolIndex].InUse; -} - -static inline struct SolidSyslogBuffer* CircularBuffer_Acquire(size_t poolIndex) -{ - CircularBuffer_MarkInUse(poolIndex); - return CircularBuffer_HandleFromIndex(poolIndex); -} - -static inline void CircularBuffer_MarkInUse(size_t poolIndex) -{ - Pool[poolIndex].InUse = true; -} - -static inline struct SolidSyslogBuffer* CircularBuffer_HandleFromIndex(size_t poolIndex) -{ - return &Pool[poolIndex].Object.Base; -} - -static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* handle) -{ - return handle != &Fallback; -} - void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) { - size_t poolIndex = CircularBuffer_IndexFromHandle(base); - bool released = false; - if (CircularBuffer_PoolIndexIsValid(poolIndex)) - { - released = CircularBuffer_FreeIfInUse(poolIndex); - } + size_t index = CircularBuffer_IndexFromHandle(base); + bool released = SolidSyslogPoolAllocator_IndexIsValid(&Allocator, index) && + SolidSyslogPoolAllocator_FreeIfInUse(&Allocator, index, CircularBuffer_CleanupAtIndex, NULL); if (!released) { SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_WARNING, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY); @@ -132,7 +60,7 @@ static size_t CircularBuffer_IndexFromHandle(const struct SolidSyslogBuffer* bas size_t result = SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; for (size_t poolIndex = 0; poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; poolIndex++) { - if (base == CircularBuffer_HandleFromIndex(poolIndex)) + if (base == &Pool[poolIndex].Base) { result = poolIndex; break; @@ -141,28 +69,10 @@ static size_t CircularBuffer_IndexFromHandle(const struct SolidSyslogBuffer* bas return result; } -static inline bool CircularBuffer_PoolIndexIsValid(size_t poolIndex) -{ - return poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; -} - -static bool CircularBuffer_FreeIfInUse(size_t poolIndex) -{ - bool released = false; - SolidSyslog_LockConfig(); - if (CircularBuffer_PoolItemIsInUse(poolIndex)) - { - CircularBuffer_Cleanup(CircularBuffer_HandleFromIndex(poolIndex)); - CircularBuffer_MarkFree(poolIndex); - released = true; - } - SolidSyslog_UnlockConfig(); - return released; -} - -static inline void CircularBuffer_MarkFree(size_t poolIndex) +static void CircularBuffer_CleanupAtIndex(size_t index, void* context) { - Pool[poolIndex].InUse = false; + (void) context; + CircularBuffer_Cleanup(&Pool[index].Base); } static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead) From 58cf5f7e02853ff88e34225e097c288a80f2a833 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 18 May 2026 08:29:41 +0100 Subject: [PATCH 3/3] docs: update DEVLOG for S11.02 --- DEVLOG.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/DEVLOG.md b/DEVLOG.md index 339e649d..ea8089c5 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,5 +1,92 @@ # Dev Log +## 2026-05-18 — S11.02: Extract SolidSyslogPoolAllocator helper (#395) + +S11.01 left every E11 class about to copy the same 13-function pool +plumbing — slot walk, per-iteration `LockConfig`, claim/release, +fallback dispatch, reverse lookup. S11.02 hoists everything except the +reverse lookup into a TU-internal `SolidSyslogPoolAllocator` that each +`*Static.c` instantiates with a caller-supplied `bool[]`. + +### Shape of the helper + +Three operations + a predicate, all under `Core/Source/` (never on the +public include path): + +- `SolidSyslogPoolAllocator_AcquireFirstFree(self)` — walks the pool + with `LockConfig` wrapping every per-slot probe (matches the pilot's + invariant exactly), returns first claimed index or `Count` on + exhaustion. +- `SolidSyslogPoolAllocator_FreeIfInUse(self, index, cleanup, ctx)` — + locks once, runs `cleanup` *inside* the lock before clearing + `InUse`, returns whether the slot was actually freed. The cleanup + callback is the one bit of class-specific behaviour the helper + can't see; passing it in keeps the lock-held-during-cleanup + invariant intact across every consumer. +- `SolidSyslogPoolAllocator_IndexIsValid(self, index)` — static-inline + predicate over `Count`. + +The struct is two fields (`bool* InUse; size_t Count;`), declared +public so each `*Static.c` can do `static struct SolidSyslogPoolAllocator +Allocator = {InUseArray, POOL_SIZE};` at file scope — no `_Create` +needed, no storage cast. + +### Cycle-by-cycle TDD pacing + +Happy-path then locking, as agreed in the design discussion. Twelve +tests across five behaviours: `IndexIsValid` true/false, `AcquireFirstFree` +empty/walks/exhausted, `FreeIfInUse` happy/already-free/NULL-callback, +locking-per-probe (1 vs N), locking-exactly-once on free, and the +cleanup-inside-the-lock invariant (the spy captures `LockCount - +UnlockCount` at the moment of invocation and asserts it's exactly 1). +The cleanup-inside-the-lock test would catch any drift where someone +moves the cleanup call out of the critical section. + +### Migration of `CircularBufferStatic.c` + +107 deletions, 17 insertions. The `struct Slot` wrapper retired — +`Pool` is now a bare array of `SolidSyslogCircularBuffer`, `InUse` is +its own array, and direct `&Pool[index].Base` access replaces the +old `HandleFromIndex` helper. Ten file-scope helpers retired +(`AcquireFirstFree`, `AcquireIfFree`, `Acquire`, `PoolItemIsFree`, +`PoolItemIsInUse`, `MarkInUse`, `MarkFree`, `HandleFromIndex`, +`HandleIsValid`, `FreeIfInUse`). What's left in the file: `_Create`, +`_Destroy`, `IndexFromHandle` (the per-class reverse lookup — +intentionally kept per-class because hoisting it would require a +typeless callback that breaks the no-cast invariant), `CleanupAtIndex` +(3-line bridge from the helper's typeless cleanup signature to +`CircularBuffer_Cleanup`), and the two `Fallback` vtable entries. + +### Test suite untouched + +`SolidSyslogCircularBufferTest.cpp` was not modified — that was the +regression net. Every assertion including the lock-count tests +(`CreateAcquiresAndReleasesConfigLockOnFirstFreeSlot`, +`CreateLocksOncePerSlotProbedWhenPoolIsFull`, +`DestroyOfPooledHandleLocksOnce`, `DestroyOfUnknownHandleDoesNotLock`) +passes against the new implementation because the helper preserves the +per-probe and per-free lock invariants exactly. + +### Decisions confirmed + +- **Helper does not report exhaustion errors itself** — each class wants + its own `SOLIDSYSLOG_ERROR_MSG__POOL_EXHAUSTED` string. +- **`IndexFromHandle` stays per-class** — hoisting would need a + typeless callback or a `const void*` cast that breaks the no-cast + invariant; the 5 lines per class aren't worth it. +- **No public-header audience-table entry** — helper is TU-internal, + not an integration point. + +### Gates + +debug, clang-debug, sanitize, coverage (helper and migrated file both +at 100% line), tidy, cppcheck, cppcheck-misra (60 hits — one fewer than +pre-migration; zero new), clang-format, IWYU. Full suite revalidated +at `SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE=3` (the validation step from +end of S11.01) — 1139 tests pass. + +--- + ## 2026-05-17 — S11.01 commits 2–N: CircularBuffer pool migration (E11 pilot, #392) The body of S11.01 — every commit after the `SolidSyslog_SetConfigLock`