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
The library uses a uniform _Create / _Destroy API. For classes whose Create
captures state (config args, callbacks, references), the current pattern is a
file-scope singleton: the first _Create initialises the static instance, and any
subsequent _Create silently overwrites it. A multi-Create scenario — multiple SolidSyslog instances in one target, or any unintended re-entry — produces a latent
bug where the second Create mutates the first's state.
E11 fixes this by giving every stateful Created class pool semantics: each _Create draws from a library-internal pool of slots sized by a tunable parameter.
Multi-Create either produces distinct instances or errors cleanly via SolidSyslog_Error once the pool is exhausted.
The ongoing MISRA work (#12, E10) is what surfaced this. The dominant suppression
class today is the void*-storage cast in storage-injected _Create signatures. Pool
semantics remove those signatures (and their casts) entirely, so the MISRA cleanup
falls out for free on the subset of classes that also have storage today. But the
primary driver is correctness: a _Create that silently mutates a singleton on
second call is unsound regardless of whether anyone hits the case today.
E11 keeps the heap-free guarantee — pools are internal static arrays, no malloc.
Public _Create / _Destroy names stay unchanged; integrator code is
allocation-agnostic.
A possible future epic could add a dynamic (heap) allocation strategy on top of this
foundation — the file layout below anticipates it — but no dynamic work is committed
to as part of E11 or as a guaranteed follow-up.
Design
Three-TU pattern per class
Class.c — vtable + core logic, no allocation. Exports private Class_Initialise / Class_Cleanup helpers via a private header.
ClassStatic.c — internal pool of N slots. Create allocates from pool via the
shared SolidSyslogPoolAllocator helper (see below), Destroy releases. E11 ships this.
ClassDynamic.c — malloc/free counterpart. Not part of E11. A possible future
dynamic-allocation epic could add it as a sibling TU; the file layout here
anticipates that so adding a dynamic strategy later would be purely additive.
A CMake tunable SOLIDSYSLOG_ALLOCATION_STRATEGY selects Static or Dynamic and picks
which TU compiles. One strategy per build. E11 ships only Static; selecting Dynamic
gives a clear CMake error (no dynamic TUs exist yet).
Landed in S11.02 (#396). Core/Source/SolidSyslogPoolAllocator.{h,c} is TU-internal
(never in Core/Interface/) and owns the slot-walk and per-iteration LockConfig
on behalf of every *Static.c. Three operations + a predicate:
SolidSyslogPoolAllocator_AcquireFirstFree(self) — walks the pool with LockConfig wrapping every per-slot probe; returns first claimed index or Count
on exhaustion.
SolidSyslogPoolAllocator_FreeIfInUse(self, index, cleanup, ctx) — locks once;
invokes cleanupinside the lock before clearing InUse. Lock-held-during-
cleanup is load-bearing — prevents a concurrent Acquire from grabbing the slot
mid-cleanup and racing Initialise vs Cleanup on the same memory.
Struct is two fields (bool* InUse; size_t Count;). Each sweep class declares the
backing arrays and the allocator at file scope and calls the three operations — no _Create for the helper itself, no storage cast anywhere:
What stays per-class (intentionally): the reverse-lookup IndexFromHandle (5 lines)
and a 3-line CleanupAtIndex bridge from the helper's typeless callback to Class_Cleanup. Hoisting IndexFromHandle would need a typeless callback or a const void* cast that breaks the no-cast invariant below — not worth it.
Per-class delta target. Each sweep class lands at ~3 file-scope functions
(_Create, _Destroy, IndexFromHandle) + the tiny CleanupAtIndex bridge + its
vtable entries. If a sweep PR adds back the old plumbing (AcquireFirstFree, AcquireIfFree, PoolItemIsFree, MarkInUse, MarkFree, HandleFromIndex, HandleIsValid, etc.) it's a sign the helper's contract is wrong for that class —
stop and reshape the helper rather than duplicate. The CircularBuffer migration
(S11.02 commit 2) is the reference: 107 deletions, 17 insertions, no new helpers.
Pool sizing
Pool sizes default per class; integrator overrides via the existing tunables mechanism
(SOLIDSYSLOG_USER_TUNABLES_FILE → SolidSyslogTunables.h → SolidSyslogTunablesDefaults.h). New knobs follow SOLIDSYSLOG_<CLASS>_POOL_SIZE and,
for variable-payload classes, SOLIDSYSLOG_<CLASS>_CAPACITY_BYTES.
NullBuffer is misnamed — it isn't a GoF null object, it's a passthrough that forwards
writes directly to an injected Sender. The cross-tree rename to PassthroughBuffer
(file, type, public functions, every caller including BDD targets and examples) is its
own story (S11.03) preceding the Core sweep — it's a no-functional-change rename with
a wide blast radius and warrants its own review surface, separate from the pool
migrations.
Failure semantics (extends the bad-setup contract)
Pool exhaustion → return a fallback object pointer + SolidSyslog_Error(ERR, …).
Double-Create of SolidSyslog → SolidSyslog_Error and ignore.
Destroy of unknown / already-destroyed pointer → SolidSyslog_Error(WARNING, …)
and ignore.
The pool-exhaustion fallback reuses the GoF Null Object sibling where one exists
(NullMutex, NullSecurityPolicy, NullStore) — its vtable methods are already safe
no-ops so caller code keeps running. For classes without a Null sibling (Sender,
Stream, Datagram, Resolver, Buffer, SD, AtomicCounter), the pool TU declares a
class-private fallback instance with safe-no-op vtable methods.
MISRA conformance — verified as we go
The canonical pattern must be MISRA-clean by construction. The pilot was the
place to verify this; S11.02 confirmed the shared helper is also clean. Applying
the pattern to a class must not introduce new cppcheck-misra warnings or new // cppcheck-suppress / NOLINT additions. If the pattern itself requires a
suppression, the pattern is wrong and gets reshaped before the sweep continues.
cppcheck-misra runs in warning mode today, not error. E11 should treat any new
warning introduced by the new pattern as a failure even if cppcheck doesn't gate on
it yet. Existing class-specific deviations unrelated to the new pattern may persist
— they are not E11's problem.
Out of scope
Dynamic (heap) allocation — possible future epic, not committed to.
Stateless function adapters (SolidSyslogPosixClock_GetTimestamp, SolidSyslogPosixHostname_Get, SolidSyslogPosixSleep, etc.) — no state, no
instance, no Create.
The pure-singleton classes (NullSecurityPolicy, Crc16Policy, NullMutex, NullStore) — already in target shape; no migration needed.
Sweep — Core singleton stateful classes (S11.04) — PassthroughBuffer, UdpSender, SwitchingSender, MetaSd, TimeQualitySd, OriginSd. All adopt
the 3-TU split + shared helper. Introduces two new public GoF nulls
(SolidSyslogNullSd shared by the three SD classes; SolidSyslogNullSender shared by the two senders here plus S11.05's StreamSender). One PR, commit per class plus commit-per-null.
Sweep — Core storage-cast classes (S11.05) — BlockStore, FileBlockDevice, StreamSender. Pool migration and delete the public SolidSyslog<*>Storage
types + SOLIDSYSLOG_<*>_STORAGE_SIZE enums; update callers in BDD targets and
examples. Wider blast radius than S11.04 — separate review surface. Reuses the
GoF nulls from S11.04 (SolidSyslogNullSender for StreamSender, SolidSyslogNullStore for BlockStore).
Sweep — POSIX adapters (S11.06).
Sweep — Windows adapters (S11.07).
Sweep — FreeRTOS adapters (S11.08).
Sweep — AtomicCounter family + TLS + FatFs (S11.09).
SolidSyslog retrofit (S11.10) — already singleton-shaped, adopt the canonical
file layout. Pool size fixed at 1, no tunable.
Every stateful _Create / _Destroy class follows the pool template and uses the
shared SolidSyslogPoolAllocator helper for its slot walk.
SOLIDSYSLOG_<TYPE>_STORAGE_SIZE enums removed from public headers.
MISRA suppressions for storage casts deleted; cppcheck-misra and clang-tidy clean.
The canonical pattern itself introduces no new MISRA warnings or suppressions
(verified at the pilot, at S11.02's helper extraction, and at each sweep PR).
BDD targets, examples, and tests build and pass against the new API.
NullBuffer renamed to PassthroughBuffer across code, tests, BDD, examples, and
the public-header audience table.
Tunable defaults and override mechanism documented.
Stories
Decomposition firms up as we go. Pilot first; helper extracted second; sweep stories
created when the previous sweep is approaching merge. See sub-issues for the live
list and roll-up.
Motivation
The library uses a uniform
_Create/_DestroyAPI. For classes whoseCreatecaptures state (config args, callbacks, references), the current pattern is a
file-scope singleton: the first
_Createinitialises the static instance, and anysubsequent
_Createsilently overwrites it. A multi-Create scenario — multipleSolidSysloginstances in one target, or any unintended re-entry — produces a latentbug where the second Create mutates the first's state.
E11 fixes this by giving every stateful Created class pool semantics: each
_Createdraws from a library-internal pool of slots sized by a tunable parameter.Multi-Create either produces distinct instances or errors cleanly via
SolidSyslog_Erroronce the pool is exhausted.The ongoing MISRA work (#12, E10) is what surfaced this. The dominant suppression
class today is the
void*-storage cast in storage-injected_Createsignatures. Poolsemantics remove those signatures (and their casts) entirely, so the MISRA cleanup
falls out for free on the subset of classes that also have storage today. But the
primary driver is correctness: a
_Createthat silently mutates a singleton onsecond call is unsound regardless of whether anyone hits the case today.
E11 keeps the heap-free guarantee — pools are internal
staticarrays, nomalloc.Public
_Create/_Destroynames stay unchanged; integrator code isallocation-agnostic.
A possible future epic could add a dynamic (heap) allocation strategy on top of this
foundation — the file layout below anticipates it — but no dynamic work is committed
to as part of E11 or as a guaranteed follow-up.
Design
Three-TU pattern per class
Class.c— vtable + core logic, no allocation. Exports privateClass_Initialise/Class_Cleanuphelpers via a private header.ClassStatic.c— internal pool of N slots. Create allocates from pool via theshared
SolidSyslogPoolAllocatorhelper (see below), Destroy releases.E11 ships this.
ClassDynamic.c— malloc/free counterpart. Not part of E11. A possible futuredynamic-allocation epic could add it as a sibling TU; the file layout here
anticipates that so adding a dynamic strategy later would be purely additive.
A CMake tunable
SOLIDSYSLOG_ALLOCATION_STRATEGYselects Static or Dynamic and pickswhich TU compiles. One strategy per build. E11 ships only Static; selecting Dynamic
gives a clear CMake error (no dynamic TUs exist yet).
Shared slot-walk helper (
SolidSyslogPoolAllocator)Landed in S11.02 (#396).
Core/Source/SolidSyslogPoolAllocator.{h,c}is TU-internal(never in
Core/Interface/) and owns the slot-walk and per-iterationLockConfigon behalf of every
*Static.c. Three operations + a predicate:SolidSyslogPoolAllocator_AcquireFirstFree(self)— walks the pool withLockConfigwrapping every per-slot probe; returns first claimed index orCounton exhaustion.
SolidSyslogPoolAllocator_FreeIfInUse(self, index, cleanup, ctx)— locks once;invokes
cleanupinside the lock before clearingInUse. Lock-held-during-cleanup is load-bearing — prevents a concurrent
Acquirefrom grabbing the slotmid-cleanup and racing
InitialisevsCleanupon the same memory.SolidSyslogPoolAllocator_IndexIsValid(self, index)— static-inline.Struct is two fields (
bool* InUse; size_t Count;). Each sweep class declares thebacking arrays and the allocator at file scope and calls the three operations — no
_Createfor the helper itself, no storage cast anywhere:What stays per-class (intentionally): the reverse-lookup
IndexFromHandle(5 lines)and a 3-line
CleanupAtIndexbridge from the helper's typeless callback toClass_Cleanup. HoistingIndexFromHandlewould need a typeless callback or aconst void*cast that breaks the no-cast invariant below — not worth it.Per-class delta target. Each sweep class lands at ~3 file-scope functions
(
_Create,_Destroy,IndexFromHandle) + the tinyCleanupAtIndexbridge + itsvtable entries. If a sweep PR adds back the old plumbing (
AcquireFirstFree,AcquireIfFree,PoolItemIsFree,MarkInUse,MarkFree,HandleFromIndex,HandleIsValid, etc.) it's a sign the helper's contract is wrong for that class —stop and reshape the helper rather than duplicate. The CircularBuffer migration
(S11.02 commit 2) is the reference: 107 deletions, 17 insertions, no new helpers.
Pool sizing
Pool sizes default per class; integrator overrides via the existing tunables mechanism
(
SOLIDSYSLOG_USER_TUNABLES_FILE→SolidSyslogTunables.h→SolidSyslogTunablesDefaults.h). New knobs followSOLIDSYSLOG_<CLASS>_POOL_SIZEand,for variable-payload classes,
SOLIDSYSLOG_<CLASS>_CAPACITY_BYTES._Create(void), no stateNullSecurityPolicy,Crc16Policy,NullMutex,NullStoreSolidSyslogPassthroughBuffer(renamed fromNullBufferin a preceding story),UdpSender,SwitchingSender,MetaSd,TimeQualitySd,OriginSdvoid*castSOLIDSYSLOG_<*>_STORAGE_SIZEenumsCircularBuffer(done — S11.02),BlockStore,FileBlockDevice,StreamSender. Platform/Posix:PosixMutex,PosixMessageQueueBuffer,PosixTcpStream,PosixFile,PosixDatagram,PosixDnsResolver. Platform/Windows:WindowsMutex,WinsockTcpStream,WindowsFile,WindowsAtomicCounter. Platform/FreeRtos:FreeRtosMutex,FreeRtosTcpStream,FreeRtosDatagram,FreeRtosStaticResolver. Platform/Atomics:StdAtomicCounter. Platform/FatFs:FatFsFile. Platform/OpenSsl:TlsStreamNullBufferis misnamed — it isn't a GoF null object, it's a passthrough that forwardswrites directly to an injected
Sender. The cross-tree rename toPassthroughBuffer(file, type, public functions, every caller including BDD targets and examples) is its
own story (S11.03) preceding the Core sweep — it's a no-functional-change rename with
a wide blast radius and warrants its own review surface, separate from the pool
migrations.
Failure semantics (extends the bad-setup contract)
SolidSyslog_Error(ERR, …).SolidSyslog→SolidSyslog_Errorand ignore.SolidSyslog_Error(WARNING, …)and ignore.
The pool-exhaustion fallback reuses the GoF Null Object sibling where one exists
(
NullMutex,NullSecurityPolicy,NullStore) — its vtable methods are already safeno-ops so caller code keeps running. For classes without a Null sibling (Sender,
Stream, Datagram, Resolver, Buffer, SD, AtomicCounter), the pool TU declares a
class-private fallback instance with safe-no-op vtable methods.
MISRA conformance — verified as we go
The canonical pattern must be MISRA-clean by construction. The pilot was the
place to verify this; S11.02 confirmed the shared helper is also clean. Applying
the pattern to a class must not introduce new cppcheck-misra warnings or new
// cppcheck-suppress/NOLINTadditions. If the pattern itself requires asuppression, the pattern is wrong and gets reshaped before the sweep continues.
cppcheck-misra runs in warning mode today, not error. E11 should treat any new
warning introduced by the new pattern as a failure even if cppcheck doesn't gate on
it yet. Existing class-specific deviations unrelated to the new pattern may persist
— they are not E11's problem.
Out of scope
SolidSyslogFormatter— transient stack-built object, lifecycle is fundamentallydifferent. Stays unchanged.
SolidSyslogPosixClock_GetTimestamp,SolidSyslogPosixHostname_Get,SolidSyslogPosixSleep, etc.) — no state, noinstance, no
Create.NullSecurityPolicy,Crc16Policy,NullMutex,NullStore) — already in target shape; no migration needed.Sequencing
SolidSyslogCircularBuffer. In Core, has both statefulCreate and a storage cast — exercises both prongs of the migration in one place.
Establishes the canonical pattern (file naming, helper signatures, pool/slot
bookkeeping, fallback-object semantics, double-Create error path, test structure)
and confirms MISRA cleanliness of the pattern itself.
[DONE — S11.01: Pilot — CircularBuffer pool migration (E11 canonical pattern) #392 / feat: S11.01 CircularBuffer pool migration (E11 pilot) #394.]
SolidSyslogPoolAllocatorextraction (S11.02) — hoist the slot walk, per-iteration
LockConfig, and cleanup-inside-lock invariant into one TU-internalhelper. Migrate
CircularBufferonto it as the first consumer. Establishes theper-class delta target (~3 functions + bridge + vtable entries) every later
sweep class must match.
[DONE — S11.02: Extract SolidSyslogPoolAllocator and migrate CircularBuffer onto it #395 / feat: S11.02 extract PoolAllocator helper and migrate CircularBuffer #396.]
NullBuffer→PassthroughBufferrename (S11.03) — cross-tree rename only,no pool migration. File, type, public functions, every caller (Core, Tests, BDD
targets, examples, public-header audience table). Separate story because the
review surface is wide and unrelated to pool semantics.
[DONE — S11.03: Rename NullBuffer to PassthroughBuffer (no functional change) #397 / refactor: S11.03 rename NullBuffer to PassthroughBuffer #398.]
PassthroughBuffer,UdpSender,SwitchingSender,MetaSd,TimeQualitySd,OriginSd. All adoptthe 3-TU split + shared helper. Introduces two new public GoF nulls
(
SolidSyslogNullSdshared by the three SD classes;SolidSyslogNullSendershared by the two senders here plus S11.05'sStreamSender). One PR, commit per class plus commit-per-null.BlockStore,FileBlockDevice,StreamSender. Pool migration and delete the publicSolidSyslog<*>Storagetypes +
SOLIDSYSLOG_<*>_STORAGE_SIZEenums; update callers in BDD targets andexamples. Wider blast radius than S11.04 — separate review surface. Reuses the
GoF nulls from S11.04 (
SolidSyslogNullSenderforStreamSender,SolidSyslogNullStoreforBlockStore).SolidSyslogretrofit (S11.10) — already singleton-shaped, adopt the canonicalfile layout. Pool size fixed at 1, no tunable.
(
CLAUDE.mdpublic-header audience table,SKILL.md, naming docs).Exit criteria
_Create/_Destroyclass follows the pool template and uses theshared
SolidSyslogPoolAllocatorhelper for its slot walk.SOLIDSYSLOG_<TYPE>_STORAGE_SIZEenums removed from public headers.(verified at the pilot, at S11.02's helper extraction, and at each sweep PR).
NullBufferrenamed toPassthroughBufferacross code, tests, BDD, examples, andthe public-header audience table.
Stories
Decomposition firms up as we go. Pilot first; helper extracted second; sweep stories
created when the previous sweep is approaching merge. See sub-issues for the live
list and roll-up.