You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
S11.01 (#392, merged via #394) landed the canonical pattern for E11 — a library-internal static pool of class instances, with SolidSyslog_LockConfig injection wrapping each slot probe and a Fallback singleton on pool exhaustion. The pilot is SolidSyslogCircularBuffer.
Looking at the final Core/Source/SolidSyslogCircularBufferStatic.c (12 functions + a struct + two static globals + a vtable pair), it's clear every other E11 class is going to copy the same shape:
static struct X Pool[N];
static bool InUse[N]; (currently embedded in a struct Slot wrapper alongside X)
AcquireFirstFree() — walk pool, lock-per-iteration, claim first free slot
PoolItemIsFree(i) / PoolItemIsInUse(i) — flag reads (chained pair)
HandleFromIndex(i) — return &Pool[i].Object.Base;
HandleIsValid(p) — return p != &Fallback;
IndexFromHandle(base) — reverse-lookup walk
PoolIndexIsValid(i) — i < POOL_SIZE
FreeIfInUse(i) — locked Cleanup + MarkFree
That's a big DRY violation about to be replicated across BlockStore, FileBlockDevice, StreamSender, every Platform Mutex / Stream / Datagram / File, the Atomic counters, TLS, FatFs, … Worth extracting before the second E11 class lands.
Goal
A new TU-internal helper, SolidSyslogPoolAllocator, that owns the slot-walk and the per-iteration LockConfig/UnlockConfig over a caller-supplied bool[]. Each E11 class drops from ~13 pool-management functions to ~3 (_Create, _Destroy, IndexFromHandle + a 3-line cleanup callback).
Decisions locked in (do not relitigate)
These were agreed at the end of the S11.01 session — they're load-bearing:
Helper API — exactly this shape
/* Core/Source/SolidSyslogPoolAllocator.h — TU-internal, NOT in Core/Interface/ */structSolidSyslogPoolAllocator
{
bool*inUse; /* caller-owned array, length == count */size_tcount; /* number of slots */
};
/* Returns index of first free slot (now marked in-use), or count on * exhaustion. LockConfig wraps EACH per-slot probe -- not lock-once- * around-loop. Initialise of the concrete object runs OUTSIDE the lock, * after this returns. */size_tSolidSyslogPoolAllocator_AcquireFirstFree(structSolidSyslogPoolAllocator*allocator);
typedefvoid (*SolidSyslogPoolCleanup)(size_tindex, void*context);
/* Returns true if the slot was in-use and is now free. The cleanup * callback (if non-NULL) fires INSIDE the per-slot lock, BEFORE InUse * is cleared. The lock-held-during-Cleanup invariant is the whole * reason for the callback signature -- doing Cleanup after the helper * unlocks would let a concurrent AcquireFirstFree grab the slot and * race Initialise vs Cleanup on the same memory. */boolSolidSyslogPoolAllocator_FreeIfInUse(
structSolidSyslogPoolAllocator*allocator,
size_tindex,
SolidSyslogPoolCleanupcleanup,
void*context
);
staticinlineboolSolidSyslogPoolAllocator_IndexIsValid(
conststructSolidSyslogPoolAllocator*allocator,
size_tindex)
{
returnindex<allocator->count;
}
Caller-supplied storage (option B, not macro-instantiated)
The helper does not own the bool[]. The caller declares static bool ClassInUse[N]; and threads it through. Reasons rejected for the alternative (macro-instantiated helper with internal bool inUse[N]):
Macro-instantiated headers get ugly fast and surprise readers.
Caller-supplied storage matches the project's established pattern (cf. SolidSyslogFormatterStorage, SolidSyslogPosixMutexStorage, …).
Allows pool size to remain a per-class tunable in SolidSyslogTunablesDefaults.h.
Initialise outside lock, Cleanup inside lock
Same invariant S11.01 established:
Acquire side: helper returns index, caller runs Initialise lock-free. The slot is reserved by InUse=true; no other task can see it because they have no handle yet.
Free side: helper runs Cleanup via callback while still holding the lock, then marks InUse=false. Releasing the lock before Cleanup would let a concurrent AcquireFirstFree grab the slot and race Initialise vs Cleanup on the same memory.
This asymmetry is the right answer; do not flatten it into a single shape.
No new MISRA suppressions / NOLINTs
The E11 invariant. The helper introduces:
Function pointer typedef — fine (existing pattern, see SolidSyslogStoreFullCallback).
void* context parameter — passed through to the cleanup callback. CircularBuffer passes NULL and the callback ignores it. No casts.
A bool* field in the helper struct — no casts.
Zero new 11.3 / 11.5 / 11.8 hazards. If a cast appears anywhere in the helper or in the migrated class, stop and reshape — that's the E11 invariant.
Three-TU split survives unchanged
CircularBuffer.c and CircularBufferPrivate.h do not change. CircularBufferStatic.c rewrites against the helper. The pattern memory project_e11_three_tu_split.md still applies — the new helper is not a fourth TU per class; it's one shared TU pair used by every *Static.c.
struct Slot wrapper is gone — Pool is now an array of bare struct SolidSyslogCircularBuffer, InUse is its own array. The wrapper survived from the index-pair pattern; the helper makes it redundant.
13 helper functions collapse to 3 (IndexFromHandle, CleanupAtIndex, the two Fallback_* vtable entries — really 2 helpers + 2 vtable entries).
Direct field access (&Pool[index].Base) replaces HandleFromIndex(index) — that helper added zero value once struct Slot was retired.
Acceptance criteria
New Core/Source/SolidSyslogPoolAllocator.{h,c} exists. .h is TU-internal (referenced only from Core/Source/).
SolidSyslogCircularBufferStatic.c uses the helper; the 10 file-scope helpers listed in Context are gone.
struct Slot is retired — Pool and InUse are separate arrays.
New Tests/SolidSyslogPoolAllocatorTest.cpp exercises the helper directly via ConfigLockFake and an in-test cleanup-callback spy. Aim for 100% line / function / branch coverage like CircularBufferStatic.c achieved.
The existing Tests/SolidSyslogCircularBufferTest.cpp mostly survives unchanged — the Pool TEST_GROUP tests the public API which still works. The lock-count assertions still hold:
One TDD cycle at a time. The helper has only three operations + the predicate — build it test-first:
IndexIsValid (one line) — drive both true and false cases.
AcquireFirstFree happy — empty pool, returns 0, marks slot 0.
AcquireFirstFree walk — slot 0 in use, returns 1.
AcquireFirstFree exhausted — all slots in use, returns count.
AcquireFirstFree locks per probe — ConfigLockFake count == probed slots (1 for empty, N for full).
FreeIfInUse happy — marks slot free, returns true, invokes cleanup callback with right index.
FreeIfInUse already-free — returns false, does NOT invoke cleanup.
FreeIfInUse invokes cleanup INSIDE the lock — fake cleanup checks LockCallCount at invocation time vs UnlockCallCount (one more lock than unlock at the moment of cleanup).
FreeIfInUse NULL cleanup callback — skips the call, still marks free.
FreeIfInUse locks exactly once — fake count == 1.
Then migrate CircularBufferStatic.c in a separate commit. Existing CircularBuffer tests must continue to pass through the migration — that's the regression net.
Sequencing on the work branch
Recommended commit shape on feat/s11-02-pool-allocator:
feat: S11.02 add SolidSyslogPoolAllocator helper — new helper + tests. Not yet wired into CircularBuffer.
refactor: S11.02 migrate CircularBufferStatic onto PoolAllocator — drops the 10 helper functions, drops struct Slot, retains 100% coverage and all existing test assertions.
Whether the helper should report its own SolidSyslog_Error on exhaustion instead of returning count and leaving the caller to do it. Argument for: removes a per-class duplicated error-report. Argument against: error messages are class-specific (_CIRCULARBUFFER_POOL_EXHAUSTED vs _BLOCKSTORE_POOL_EXHAUSTED etc), so each class wants its own text. Default: leave error reporting to the caller (status quo) unless the next session sees a clean way to parameterise.
Whether IndexFromHandle should also be hoisted. Probably not — the per-class reverse-lookup is the one bit that's genuinely typed and would need either a const void* cast or a per-class function pointer. The five lines per class aren't worth the abstraction overhead. Default: leave per-class.
Whether PoolAllocator deserves an entry in Core/Interface/ audience table in CLAUDE.md. No — it's TU-internal, not an integration point. Default: no doc surface.
Parent epic: #29
Context
S11.01 (#392, merged via #394) landed the canonical pattern for E11 — a library-internal static pool of class instances, with
SolidSyslog_LockConfiginjection wrapping each slot probe and a Fallback singleton on pool exhaustion. The pilot isSolidSyslogCircularBuffer.Looking at the final
Core/Source/SolidSyslogCircularBufferStatic.c(12 functions + a struct + two static globals + a vtable pair), it's clear every other E11 class is going to copy the same shape:static struct X Pool[N];static bool InUse[N];(currently embedded in astruct Slotwrapper alongsideX)AcquireFirstFree()— walk pool, lock-per-iteration, claim first free slotAcquireIfFree(i)— single-slot lock + check + claimAcquire(i)— mark-in-use + return handleMarkInUse(i)/MarkFree(i)— flag mutationsPoolItemIsFree(i)/PoolItemIsInUse(i)— flag reads (chained pair)HandleFromIndex(i)—return &Pool[i].Object.Base;HandleIsValid(p)—return p != &Fallback;IndexFromHandle(base)— reverse-lookup walkPoolIndexIsValid(i)—i < POOL_SIZEFreeIfInUse(i)— locked Cleanup + MarkFreeThat's a big DRY violation about to be replicated across BlockStore, FileBlockDevice, StreamSender, every Platform Mutex / Stream / Datagram / File, the Atomic counters, TLS, FatFs, … Worth extracting before the second E11 class lands.
Goal
A new TU-internal helper,
SolidSyslogPoolAllocator, that owns the slot-walk and the per-iterationLockConfig/UnlockConfigover a caller-suppliedbool[]. Each E11 class drops from ~13 pool-management functions to ~3 (_Create,_Destroy,IndexFromHandle+ a 3-line cleanup callback).Decisions locked in (do not relitigate)
These were agreed at the end of the S11.01 session — they're load-bearing:
Helper API — exactly this shape
Caller-supplied storage (option B, not macro-instantiated)
The helper does not own the
bool[]. The caller declaresstatic bool ClassInUse[N];and threads it through. Reasons rejected for the alternative (macro-instantiated helper with internalbool inUse[N]):SolidSyslogFormatterStorage,SolidSyslogPosixMutexStorage, …).SolidSyslogTunablesDefaults.h.Initialise outside lock, Cleanup inside lock
Same invariant S11.01 established:
Initialiselock-free. The slot is reserved byInUse=true; no other task can see it because they have no handle yet.Cleanupvia callback while still holding the lock, then marksInUse=false. Releasing the lock beforeCleanupwould let a concurrentAcquireFirstFreegrab the slot and raceInitialisevsCleanupon the same memory.This asymmetry is the right answer; do not flatten it into a single shape.
No new MISRA suppressions / NOLINTs
The E11 invariant. The helper introduces:
SolidSyslogStoreFullCallback).void* contextparameter — passed through to the cleanup callback. CircularBuffer passesNULLand the callback ignores it. No casts.bool*field in the helper struct — no casts.Zero new 11.3 / 11.5 / 11.8 hazards. If a cast appears anywhere in the helper or in the migrated class, stop and reshape — that's the E11 invariant.
Three-TU split survives unchanged
CircularBuffer.candCircularBufferPrivate.hdo not change.CircularBufferStatic.crewrites against the helper. The pattern memoryproject_e11_three_tu_split.mdstill applies — the new helper is not a fourth TU per class; it's one shared TU pair used by every*Static.c.Reference shape after migration
Core/Source/SolidSyslogCircularBufferStatic.cpost-migration:Notice:
struct Slotwrapper is gone —Poolis now an array of barestruct SolidSyslogCircularBuffer,InUseis its own array. The wrapper survived from the index-pair pattern; the helper makes it redundant.IndexFromHandle,CleanupAtIndex, the twoFallback_*vtable entries — really 2 helpers + 2 vtable entries).&Pool[index].Base) replacesHandleFromIndex(index)— that helper added zero value oncestruct Slotwas retired.Acceptance criteria
Core/Source/SolidSyslogPoolAllocator.{h,c}exists..his TU-internal (referenced only fromCore/Source/).SolidSyslogCircularBufferStatic.cuses the helper; the 10 file-scope helpers listed in Context are gone.struct Slotis retired —PoolandInUseare separate arrays.Tests/SolidSyslogPoolAllocatorTest.cppexercises the helper directly viaConfigLockFakeand an in-test cleanup-callback spy. Aim for 100% line / function / branch coverage likeCircularBufferStatic.cachieved.Tests/SolidSyslogCircularBufferTest.cppmostly survives unchanged — the Pool TEST_GROUP tests the public API which still works. The lock-count assertions still hold:CreateAcquiresAndReleasesConfigLockOnFirstFreeSlot→ 1 lock acquisition (one slot probed).CreateLocksOncePerSlotProbedWhenPoolIsFull→POOL_SIZElock acquisitions.DestroyOfPooledHandleLocksOnce→ 1.DestroyOfUnknownHandleDoesNotLock→ 0 (the helper'sIndexFromHandlefinds no match,PoolAllocator_FreeIfInUseis never reached).SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE = 3(the validation step David asked for at end of S11.01).Out of scope
Core/Interface/. It's a library-internal abstraction; integrator code must not see it.inUseorcountoutside the helper struct in operations. All access via the three operations +IndexIsValid._Create(mutex, ring, ringBytes)and_Destroy(base)stay byte-identical.SolidSyslogPoolAllocatorStorage-style external sizing. The struct is small enough to live as astaticinstance in each consumer's TU.TDD discipline (per project memory
feedback_tdd_and_review_style.md)One TDD cycle at a time. The helper has only three operations + the predicate — build it test-first:
IndexIsValid(one line) — drive both true and false cases.AcquireFirstFreehappy — empty pool, returns 0, marks slot 0.AcquireFirstFreewalk — slot 0 in use, returns 1.AcquireFirstFreeexhausted — all slots in use, returnscount.AcquireFirstFreelocks per probe —ConfigLockFakecount == probed slots (1 for empty, N for full).FreeIfInUsehappy — marks slot free, returns true, invokes cleanup callback with right index.FreeIfInUsealready-free — returns false, does NOT invoke cleanup.FreeIfInUseinvokes cleanup INSIDE the lock — fake cleanup checksLockCallCountat invocation time vsUnlockCallCount(one more lock than unlock at the moment of cleanup).FreeIfInUseNULL cleanup callback — skips the call, still marks free.FreeIfInUselocks exactly once — fake count == 1.Then migrate
CircularBufferStatic.cin a separate commit. Existing CircularBuffer tests must continue to pass through the migration — that's the regression net.Sequencing on the work branch
Recommended commit shape on
feat/s11-02-pool-allocator:feat: S11.02 add SolidSyslogPoolAllocator helper— new helper + tests. Not yet wired into CircularBuffer.refactor: S11.02 migrate CircularBufferStatic onto PoolAllocator— drops the 10 helper functions, dropsstruct Slot, retains 100% coverage and all existing test assertions.docs: update DEVLOG for S11.02Squash-merge target like S11.01:
feat: S11.02 extract PoolAllocator helper (E11 pre-sweep).Memory pointers for the next session
Read these before touching anything:
CLAUDE.md(project root) +SKILL.mdMEMORY.md(project memory index) — particularly:project_e11_static_pool_design.md— pool design agreementsproject_e11_three_tu_split.md— three-TU split shape (still applies; this story does NOT change it)project_configlock_injection.md—SolidSyslog_SetConfigLockshapeproject_allocation_epic.md— future dynamic-allocation epic contextfeedback_storage_pattern.md— caller-supplied storage conventionfeedback_tdd_and_review_style.md— pacingfeedback_drive_arg_values_in_same_test.md— TDD assertion bundlingfeedback_devlog_in_pr.md— DEVLOG commit goes on the work branchfeedback_no_git_commit_dash_s.md— never-Sfeedback_no_merge.md— never merge; David merges PRs himselfOpen questions for the next session to surface
SolidSyslog_Erroron exhaustion instead of returningcountand leaving the caller to do it. Argument for: removes a per-class duplicated error-report. Argument against: error messages are class-specific (_CIRCULARBUFFER_POOL_EXHAUSTEDvs_BLOCKSTORE_POOL_EXHAUSTEDetc), so each class wants its own text. Default: leave error reporting to the caller (status quo) unless the next session sees a clean way to parameterise.IndexFromHandleshould also be hoisted. Probably not — the per-class reverse-lookup is the one bit that's genuinely typed and would need either aconst void*cast or a per-class function pointer. The five lines per class aren't worth the abstraction overhead. Default: leave per-class.PoolAllocatordeserves an entry inCore/Interface/audience table in CLAUDE.md. No — it's TU-internal, not an integration point. Default: no doc surface.