Skip to content

E11: Static-Allocation Variant #29

Description

@DavidCozens

Motivation

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).

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-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 cleanup inside 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.
  • SolidSyslogPoolAllocator_IndexIsValid(self, index) — static-inline.

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:

static bool                            ClassInUse[SOLIDSYSLOG_<CLASS>_POOL_SIZE];
static struct Class                    Pool[SOLIDSYSLOG_<CLASS>_POOL_SIZE];
static struct SolidSyslogPoolAllocator Allocator = {ClassInUse, SOLIDSYSLOG_<CLASS>_POOL_SIZE};

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_FILESolidSyslogTunables.h
SolidSyslogTunablesDefaults.h). New knobs follow SOLIDSYSLOG_<CLASS>_POOL_SIZE and,
for variable-payload classes, SOLIDSYSLOG_<CLASS>_CAPACITY_BYTES.

Bucket E11 work Tunable Classes
Pure singleton_Create(void), no state None — already correct n/a NullSecurityPolicy, Crc16Policy, NullMutex, NullStore
Structurally singleton — config-driven but one-per-target by design Adopt canonical file layout. Double-Create errors. Pool size 1 fixed, no tunable SolidSyslog
Stateful, file-scope singleton today — pool-migration only 3-TU pool template + shared helper Yes PassthroughBuffer (renamed from NullBuffer in a preceding story), UdpSender, SwitchingSender, MetaSd, TimeQualitySd, OriginSd
Stateful + storage cast today — pool-migration and delete the void* cast 3-TU pool template + shared helper + remove SOLIDSYSLOG_<*>_STORAGE_SIZE enums Yes Core: CircularBuffer (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: TlsStream

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 SolidSyslogSolidSyslog_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.
  • SolidSyslogFormatter — transient stack-built object, lifecycle is fundamentally
    different. Stays unchanged.
  • 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.

Sequencing

  1. Pilot (S11.01)SolidSyslogCircularBuffer. In Core, has both stateful
    Create 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.]
  2. SolidSyslogPoolAllocator extraction (S11.02) — hoist the slot walk, per-
    iteration LockConfig, and cleanup-inside-lock invariant into one TU-internal
    helper. Migrate CircularBuffer onto it as the first consumer. Establishes the
    per-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.]
  3. NullBufferPassthroughBuffer rename (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.]
  4. 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.
  5. 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).
  6. Sweep — POSIX adapters (S11.06).
  7. Sweep — Windows adapters (S11.07).
  8. Sweep — FreeRTOS adapters (S11.08).
  9. Sweep — AtomicCounter family + TLS + FatFs (S11.09).
  10. SolidSyslog retrofit (S11.10) — already singleton-shaped, adopt the canonical
    file layout. Pool size fixed at 1, no tunable.
  11. Final sweep (S11.11) — remove MISRA storage-cast deviations + update docs
    (CLAUDE.md public-header audience table, SKILL.md, naming docs).

Exit criteria

  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    epicEpic issue

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions