Every module in the c-from-scratch framework implements the same four-function interface:
int module_init(module_t *ctx, const module_config_t *cfg);
int module_update(module_t *ctx, input_t input, uint64_t timestamp);
module_state_t module_status(const module_t *ctx);
void module_reset(module_t *ctx);This isn’t arbitrary convention. The Init-Update-Status-Reset pattern is the minimal interface that simultaneously enables static analysis, bounded resource usage, deterministic execution, and compositional verification.
Each function has a specific role in the module lifecycle, and the boundaries between them are load-bearing. Blur those boundaries and the guarantees collapse.
The Four Functions
init(): Configure Without Allocating
int baseline_init(baseline_t *ctx, const baseline_config_t *cfg) {
if (ctx == NULL || cfg == NULL) {
return BASELINE_ERR_NULL;
}
if (cfg->alpha <= 0.0 || cfg->alpha > 1.0) {
return BASELINE_ERR_CONFIG;
}
ctx->cfg = *cfg; // Copy configuration
ctx->state = BASELINE_LEARNING;
ctx->sample_count = 0;
ctx->ema = 0.0;
ctx->variance = 0.0;
ctx->last_timestamp = 0;
return BASELINE_OK;
}What init() does:
- Validates configuration parameters
- Copies configuration into the context (caller owns the memory)
- Sets initial state
- Zeroes runtime fields
What init() doesn’t do:
- Allocate memory (caller provides the context)
- Perform any I/O
- Depend on external state
- Do anything that could fail unpredictably
The caller owns memory allocation:
// Stack allocation - automatic lifetime
baseline_t monitor;
baseline_init(&monitor, &config);
// Static allocation - program lifetime
static baseline_t global_monitor;
baseline_init(&global_monitor, &config);
// Caller-managed heap - explicit lifetime
baseline_t *monitors = malloc(n * sizeof(baseline_t));
for (int i = 0; i < n; i++) {
baseline_init(&monitors[i], &configs[i]);
}This separation means the module never allocates. Static analysers can verify memory bounds. Certification auditors can trace memory ownership.
update(): The Single Point of State Change
int baseline_update(baseline_t *ctx, double value, uint64_t timestamp) {
// Precondition checks
if (ctx == NULL) return BASELINE_ERR_NULL;
if (timestamp <= ctx->last_timestamp) return BASELINE_ERR_TEMPORAL;
if (!isfinite(value)) return BASELINE_ERR_DOMAIN;
// Update timestamp
ctx->last_timestamp = timestamp;
// State machine transition
switch (ctx->state) {
case BASELINE_LEARNING:
update_learning(ctx, value);
break;
case BASELINE_MONITORING:
update_monitoring(ctx, value);
break;
case BASELINE_FAULT:
// Remain in fault until reset
break;
}
return BASELINE_OK;
}The critical constraint: all state changes happen in update().
This isn’t a stylistic preference. It’s what makes the module analysable:
Determinism: If all state changes occur in update(), then given the same sequence of update() calls with the same inputs, the module will always reach the same state. Execution can be replayed exactly.
Atomicity: Each update() call is a complete state transition. There’s no partial update, no intermediate state visible to other code. Either the update completes or it fails cleanly.
Testability: To test any state, construct the sequence of update() calls that reaches it. No hidden dependencies, no setup that isn’t explicitly part of the test.
Traceability: In safety-critical systems, auditors need to trace how state evolved. With all changes in update(), the trace is the sequence of inputs.
status(): Pure Query Function
baseline_state_t baseline_status(const baseline_t *ctx) {
if (ctx == NULL) return BASELINE_FAULT;
return ctx->state;
}The critical constraint: status() has no side effects.
The const qualifier on the context pointer isn’t optional decoration - it’s a contract enforced by the compiler. status() cannot modify the context, cannot increment counters, cannot update timestamps.
This purity enables:
Concurrent access: Multiple threads can call status() simultaneously without synchronisation. The function only reads.
Idempotent queries: Calling status() twice returns the same result (assuming no intervening update()). There’s no observer effect.
Debug instrumentation: Logging, monitoring, and debugging code can query status() freely without affecting behaviour. The act of observing doesn’t change what’s observed.
Formal verification: Pure functions are easier to reason about mathematically. The output depends only on the input, with no hidden state.
reset(): Return to Known State
void reset(baseline_t *ctx) {
if (ctx == NULL) return;
// Preserve configuration
baseline_config_t saved_cfg = ctx->cfg;
// Clear all runtime state
ctx->state = BASELINE_LEARNING;
ctx->sample_count = 0;
ctx->ema = 0.0;
ctx->variance = 0.0;
ctx->last_timestamp = 0;
// Restore configuration
ctx->cfg = saved_cfg;
}The critical constraint: reset() is always safe to call.
This seems trivial but has profound implications:
Recovery: When a module enters a fault state or behaves unexpectedly, reset() provides a guaranteed escape. The system can recover without restarting.
Testing: Tests can reset between scenarios without reinitialising. State from one test doesn’t leak into the next.
Long-running systems: Systems that run for months need a way to clear accumulated state. reset() provides this without the complexity of full reinitialisation.
Composition: When composing modules, a supervisor can reset any subordinate module at any time. This simplifies error handling at the system level.
Note what reset() preserves: configuration. The module returns to its initial state but remembers how it was configured. This distinction matters - configuration represents design-time choices, while runtime state represents execution history.
Why This Pattern Enables Certification
Safety-critical standards like DO-178C, IEC 62304, and ISO 26262 require properties that the Init-Update-Status-Reset pattern provides by design.
Bounded Memory (MC/DC Coverage)
// Maximum memory usage is sizeof(baseline_t)
// This is known at compile time
// No allocation = no fragmentation = no memory exhaustionCertification requires proving that software cannot exhaust memory. Dynamic allocation makes this proof difficult or impossible. With caller-provided memory and no internal allocation, the proof is trivial: memory usage equals the sum of context struct sizes.
Bounded Execution Time (WCET)
int baseline_update(baseline_t *ctx, double value, uint64_t timestamp) {
// Fixed number of operations
// No loops with data-dependent bounds
// No recursion
// WCET can be computed by static analysis
}Worst-Case Execution Time analysis requires that every code path have a provable upper bound. The pattern enforces this:
init(): Fixed sequence of assignmentsupdate(): Fixed state machine with bounded transitionsstatus(): Single field accessreset(): Fixed sequence of assignments
No function contains unbounded loops, recursion, or data-dependent iteration. WCET tools can analyse each function automatically.
Deterministic Execution
// Same sequence of inputs → same sequence of states
// No hidden state, no external dependencies, no randomnessReproducibility is fundamental to testing and certification. If a test passes, it must continue to pass. If a failure occurs, it must be reproducible for diagnosis.
The pattern guarantees determinism:
init()depends only on configuration (static)update()depends only on context and input (explicit)status()depends only on context (read-only)reset()depends only on context (deterministic)
There are no hidden inputs: no global variables, no system calls, no timestamps from hardware clocks (timestamps are passed as parameters).
Traceability
// Every state is reachable via a sequence of update() calls
// That sequence is the trace
// The trace is the specification for reproductionWhen something goes wrong, auditors need to know how the system reached that state. With all state changes in update(), the trace is simply the sequence of (input, timestamp) pairs passed to update().
This trace can be:
- Logged for post-incident analysis
- Replayed for debugging
- Analysed for coverage
- Verified against requirements
Comparison with Object-Oriented Patterns
Traditional OOP approaches to stateful components look different:
class BaselineMonitor {
public:
BaselineMonitor(const Config& cfg);
~BaselineMonitor();
void processValue(double value); // Modifies state
State getState() const; // Queries state
double getEMA() const; // Queries state
void setThreshold(double t); // Modifies config AND state?
private:
State state_;
double ema_;
// ...
};This design has several properties that complicate certification:
Constructor allocation: The constructor may allocate memory, making resource bounds harder to prove.
Multiple mutators: Both processValue() and setThreshold() modify state. State changes aren’t localised.
Blurred boundaries: Does setThreshold() just change configuration, or does it also affect runtime state? The interface doesn’t say.
Destructor complexity: Destructors can fail, throw exceptions, or have side effects. Resource cleanup is implicit.
No explicit reset: How do you return the object to initial state? Destroy and reconstruct? That involves allocation.
The Init-Update-Status-Reset pattern makes explicit what OOP often leaves implicit:
| Concern | OOP Approach | IUSR Pattern |
|---|---|---|
| Memory | Constructor allocates | Caller provides |
| State change | Multiple methods | Only update() |
| Configuration | Mixed with runtime | Separate in init() |
| Reset | Unclear/reconstruct | Explicit reset() |
| Analysis | Requires whole-program | Per-function |
Pattern Composition
Modules following the pattern compose naturally. Consider a system with temperature and pressure monitors:
typedef struct {
temp_monitor_t temp;
pressure_monitor_t pressure;
system_state_t state;
} system_t;
int system_init(system_t *sys, const system_config_t *cfg) {
int err;
err = temp_init(&sys->temp, &cfg->temp);
if (err != TEMP_OK) return SYSTEM_ERR_TEMP_INIT;
err = pressure_init(&sys->pressure, &cfg->pressure);
if (err != PRESSURE_OK) return SYSTEM_ERR_PRESSURE_INIT;
sys->state = SYSTEM_NOMINAL;
return SYSTEM_OK;
}
int system_update(system_t *sys, sensor_data_t *data, uint64_t timestamp) {
int temp_err = temp_update(&sys->temp, data->temperature, timestamp);
int press_err = pressure_update(&sys->pressure, data->pressure, timestamp);
// Compose component states into system state
if (temp_err != TEMP_OK || press_err != PRESSURE_OK) {
sys->state = SYSTEM_SENSOR_FAULT;
} else if (temp_status(&sys->temp) == TEMP_ALARM ||
pressure_status(&sys->pressure) == PRESSURE_ALARM) {
sys->state = SYSTEM_ALARM;
} else {
sys->state = SYSTEM_NOMINAL;
}
return SYSTEM_OK;
}
system_state_t system_status(const system_t *sys) {
return sys->state;
}
void system_reset(system_t *sys) {
temp_reset(&sys->temp);
pressure_reset(&sys->pressure);
sys->state = SYSTEM_NOMINAL;
}The system-level module follows the same pattern as its components. This fractal structure means:
- The system can be tested the same way as components
- System-level guarantees derive from component guarantees
- Another system can compose this system as a component
Composition preserves properties. If all components have O(1) memory, the composition has O(1) memory. If all components are deterministic, the composition is deterministic.
Implementing the Pattern
State Machine Structure
Most modules following this pattern contain an explicit finite state machine:
typedef enum {
STATE_INIT,
STATE_LEARNING,
STATE_MONITORING,
STATE_ALARM,
STATE_FAULT
} module_state_t;
// Transition table (documentation)
// STATE_INIT + init() → STATE_LEARNING
// STATE_LEARNING + enough_samples → STATE_MONITORING
// STATE_MONITORING + anomaly → STATE_ALARM
// STATE_ALARM + normal → STATE_MONITORING
// ANY + fault → STATE_FAULT
// STATE_FAULT + reset() → STATE_LEARNINGThe state machine makes transitions explicit and analysable. Tools can verify that all states are reachable, all transitions are handled, and no invalid transitions occur.
This connects to Heartbeats and State Machines - the same FSM principles apply to any temporal monitoring problem.
Error Handling
The pattern uses return codes rather than exceptions:
typedef enum {
MODULE_OK = 0,
MODULE_ERR_NULL = -1,
MODULE_ERR_TEMPORAL = -2,
MODULE_ERR_DOMAIN = -3,
MODULE_ERR_OVERFLOW = -4,
MODULE_ERR_STATE = -5,
MODULE_ERR_CONFIG = -6
} module_error_t;Return codes are:
- Analysable (static analysis can track error propagation)
- Predictable (no hidden control flow)
- Explicit (callers must handle them)
The pattern requires that update() returns an error code. Even when the update succeeds, the return value confirms it.
Time Handling
Time is passed as a parameter, not read from system clocks:
int module_update(module_t *ctx, double input, uint64_t timestamp);This design choice enables:
Deterministic replay: Given recorded inputs and timestamps, execution can be replayed exactly.
Testing: Tests can simulate arbitrary time progressions without mocking system calls.
Portability: No dependency on platform-specific time APIs.
Analysis: Time-dependent behaviour is explicit in the interface.
The module enforces monotonic time internally:
if (timestamp <= ctx->last_timestamp) {
return MODULE_ERR_TEMPORAL;
}Out-of-order timestamps are rejected rather than causing undefined behaviour.
When Not to Use This Pattern
The Init-Update-Status-Reset pattern optimises for specific properties. It’s not always the right choice.
Interactive applications: GUI components often need multiple entry points, event handlers, and partial updates. The strict lifecycle is too rigid.
Streaming data: When processing continuous streams with complex dependencies, the single update() entry point may force awkward batching.
Performance-critical inner loops: The precondition checks in update() add overhead. In tight loops where inputs are guaranteed valid, this overhead matters.
Exploratory development: When requirements are unclear, the upfront design required by this pattern can slow iteration.
The pattern shines when:
- Correctness must be provable
- Behaviour must be reproducible
- Resources must be bounded
- Components must compose reliably
- Certification is required
For safety-critical systems, these properties aren’t optional. For other systems, they’re often valuable but not mandatory.
Conclusion
The Init-Update-Status-Reset pattern is the minimal interface that provides O(1) resource bounds, deterministic execution, and compositional verification.
The four functions aren’t arbitrary - each serves a specific purpose:
- init() separates configuration from allocation
- update() localises all state changes
- status() guarantees side-effect-free queries
- reset() provides guaranteed recovery
Together, they enable the properties that safety-critical certification requires: bounded memory, bounded time, deterministic behaviour, and complete traceability.
The c-from-scratch repository demonstrates this pattern across multiple modules. Each module - Pulse, Baseline, Timing - implements the same interface, enabling composition and consistent testing.
The pattern’s constraints feel restrictive at first. All state changes in one function? No internal allocation? Explicit time parameters? These choices force discipline that pays off during testing, debugging, and certification.
As with any architectural pattern, appropriateness depends on requirements. For systems where “it usually works” isn’t acceptable, the Init-Update-Status-Reset pattern provides a foundation for code that provably works.
Four functions. Complete lifecycle. Verifiable contracts.