|
Safe API Framework
Layered API framework for safety-related applications (ERTMS RBC reference targeting CENELEC EN 50128 SIL 4)
|
Status: Accepted Date: 2026-08-20 Applies to: new include/safeapi/notify/sapi_notify.h (header-only, no src/, no SAFEAPI_ENABLE_* option, no tests/ executable of its own - see §2.3/§5).
safeAPIRBC2oo2 (downstream, separate repo) is introducing a GA/GP split inside its A/B redundant-channel application: a "GP" orchestrator module (the cyclic train-route connect sweep) needs to (a) ask its registered "GA" peers "is this proposed action OK" before proceeding - a veto gate, every registered peer must agree - and (b) separately tell its registered GA peers "this happened" once it has - a fan-out notification, no return value. Today it does neither: it #includes its peer module's header directly and calls its functions by name, the same tight compile-time coupling ADR-025's own §1 context flags for channel_ab_crosscompare.c before that ADR - "already reimplemented outside the framework because no equivalent primitive existed here." The same trigger applies again: no registered-callback list primitive exists in this framework, so a downstream project reaches for a hand-rolled one.
A repository-wide search confirmed this is a genuine gap, not a naming mismatch: sapi_channel_t/sapi_voter_t/sapi_cross_comparator_t each carry a single, concretely-typed callback field per config (sapi_voter_compare_fn, on_disagreement, ...) - none register a bounded LIST of N independently-registered {callback, context} pairs. sapi_ipc_pubsub is structurally the closest cousin (_topic_create/ _subscribe/_publish) but a different shape entirely - subscribers poll a bounded async queue (sapi_ipc_pubsub_receive with a timeout), not a synchronous callback invocation - wrong fit for a same-cycle "ask before proceeding" gate or an immediate "tell everyone now" notification, both of which need to resolve within the same call, not on a later poll.
docs/MISRA_COMPLIANCE_REPORT.md Rule 11.1 ("Conversions shall not be performed between a pointer to a function and any other type") rules out the obvious type-erased design (store callbacks as void *, cast back to the real signature at dispatch) outright - a genuinely generic, signature-agnostic dispatcher is not an option here the way it might be in a non-MISRA codebase.
SAFEAPI_DECLARE_CALLBACK_LIST(list_type, callback_fn_type, max_subscribers) declares list_type_slot_t ({callback_fn_type fn; void *context;}) and list_type ({list_type_slot_t slots[max_subscribers]; uint32_t count;})
Each concrete instantiation gets its own small, ordinary list_type_init()/_register()/dispatch function, following the worked example in sapi_notify.h's own header doc. _register() calls sapi_lifecycle_check_setup_allowed() as its first check, mirroring every other setup-only constructor in this framework (sapi_timer_create(), sapi_voter_init()/_register_channel(), sapi_cross_comparator_init()/_register_channel(), sapi_watchdog_create()) - registering a callback is exactly the same kind of INIT-phase-only resource construction ADR-026 already gates. A veto-gate dispatch function returns false if ANY registered validator returns false, and if zero validators are registered (fail-safe default-deny: nothing is authorized without an explicit, registered authority - the safety-appropriate default for code that will run in a SIL2/SIL3 context). A fan-out notify dispatch function NULL-guards every slot before calling it, the same "same function pointer type, same call site, same NULL-guard" precedent sapi_watchdog_create() already established (per docs/MISRA_COMPLIANCE_REPORT.md's own note on that function).
Unlike sapi_voter/sapi_cross_comparator (full runtime modules with their own .c, own enable flag, own dependency-graph entry per ADR-024), this is a single header-only macro with zero runtime code of its own - the actual logic lives in each caller's own hand-written functions (§2.2), which compile as part of that caller's own translation unit. It belongs alongside sapi_buffer/sapi_types as an always-available foundational header, not as an optional subsystem a consuming project opts into.