|
Safe API Framework
Layered API framework for safety-related applications (ERTMS RBC reference targeting CENELEC EN 50128 SIL 4)
|
Status: Draft Date: 2026-08-02 Applies to: safeAPIFreamwork, all include/safeapi/os/*.h services
ADR-001 section 3.5 anticipated this decision but deferred it: "Each service's implementation is reached through a function-pointer table ... selected at build/link time ... This ADR only defines the public API; backend selection/registration is left to a follow-up ADR." Until now, the only "implementation" of any OAL service was the stub .c file shipped with the framework itself (always returning SAPI_STATUS_NOT_IMPLEMENTED), which meant an integrator's only option was to edit those files directly - not swappable, not testable with a mock backend, and every target (POSIX dev host, an RTOS, bare metal) would fight over the same files.
This ADR resolves that open item: runtime callback registration, the same pattern already used for sapi_safestate's per-level handlers (ADR-004 section 2.2), applied uniformly to all seven OAL services (timer, NVM, memory, task, IPC, log, reboot).
Each service defines a struct of function pointers matching its operations, and a single registration entry point:
An integrator provides their own implementation by populating a static const sapi_timer_backend_t my_posix_timer_backend = { ... }; and calling sapi_timer_register_backend(&my_posix_timer_backend) during system startup, before any other sapi_timer_* call. This is the answer to "how does a user provide their own implementation": no framework source file needs editing, and multiple backends (POSIX for host-side testing, an RTOS backend for target hardware) can live side by side in an integrator's codebase, selected by which one gets registered.
One global slot per service (a single static const sapi_*_backend_t *s_backend in each service's .c file), not one per handle/instance - consistent with there being exactly one real OS underneath a given build, mirroring the one-handler-per-level design already used for sapi_safestate. Registering again replaces the previous backend (no error on re-registration), matching the same choice made for sapi_safestate_register_handler.
Every public API function keeps its existing defensive parameter validation (null checks, config sanity checks) before touching the backend, and only then dispatches:
This means a backend author only has to implement the actual mechanism correctly for already-validated inputs - it never has to re-derive the framework's own precondition checks, and a backend that only implements a subset of a service's optional operations can leave the corresponding vtable slot NULL rather than providing a dummy function.
This is still a pure C ABI (ADR-001 section 3.1): a vtable here is an ordinary struct of function pointers, not a C++ vtable/virtual class. No dynamic allocation is introduced - the vtable itself is a static const struct the integrator defines (typically at file scope), and the single backend-pointer slot per service is a static variable inside the framework, exactly like sapi_safestate's handler array.
sapi_log gets the same treatment for consistency even though its default (no backend registered) behavior is identical to today's stub: writes are silently dropped (matches REQ-OAL-LOG-001's "never blocks or fails the caller" requirement regardless of whether a backend exists yet). sapi_reboot (new in ADR-004 section 3) is built with this pattern from the start - no separate stub-then-retrofit step.
Superseded by ADR-007. See below for the original paths; each OAL service's backend type/registration function now lives in its own per-feature header (include/safeapi/<feature>/sapi_<feature>.h) and .c file (src/<feature>/sapi_<feature>.c), not a shared os/ folder.
Every include/safeapi/os/*.h gains a sapi_<service>_backend_t type and a sapi_<service>_register_backend() declaration; every src/os/sapi_*.c is rewritten to validate-then-dispatch instead of always returning SAPI_STATUS_NOT_IMPLEMENTED.