Skip to content

The Reactive Model

A Termina program does not run from a main function that drives the system from beginning to end. Instead, it is organized as a set of components that wait for events and respond to each one by executing an action. This chapter describes the model in which these components operate: where events come from, which components react to them, how the components are connected, and the guarantees the model provides. The tutorial earlier in the book showed this model at work in a concrete application; the purpose here is to set out the concepts and vocabulary that the chapters on each component type will build on.

The execution environment

The transpiler translates a Termina program into MISRA C. That generated code does not call the operating system directly. It runs on top of the Termina Operating System Abstraction Layer, the OSAL, which provides a uniform interface to the services a reactive program needs: scheduling, interrupt handling, synchronization, and the passing of messages between components. Each supported platform, whether a real-time operating system such as RTEMS or FreeRTOS or the posix-gcc platform used for development, supplies its own implementation of this interface. The Termina code is therefore the same across platforms, and each OSAL implementation maps the interface onto the primitives of its target.

Events and their sources

Events are generated by emitters, of which Termina recognizes four kinds. A periodic timer emits an event at a fixed interval and drives activities that must run regularly, such as the acquisition of housekeeping data. A hardware interrupt is triggered when a device signals it, and it carries the interrupt vector as its value. A system event marks a distinguished moment in the system's life, the most important being the initialization event, delivered once at start-up. Finally, a runtime exception is emitted when the runtime detects an abnormal condition, such as an array index out of bounds, a signed arithmetic overflow, a division by zero, or an action that returns a failure. Exceptions are a source of events, and the language has no try or catch construct.

Each event is delivered to a single component, triggering the execution of one action.

Reactive entities and passive resources

Termina has three kinds of classes: the task and the handler, which react to events, and the resource, which does not.

Tasks and handlers are the reactive entities of the system: they hold the ports through which events reach them, and they define the actions that run in response. A task has its own thread of execution and a scheduling priority, and it reacts to timer events and to messages arriving on its input queues. A handler has no thread of its own; its action runs in the context of the source that triggered it, typically an interrupt, and is meant for the short, time-critical work that must be done immediately before control returns to the rest of the system.

A resource, by contrast, is a passive entity that encapsulates shared state together with the procedures that operate on it. A resource never reacts to an event; it performs its work only when a task or handler calls one of its procedures, and those calls execute under mutual exclusion.

The unqualified term class covers all three; the terms reactive entity and reactive class refer specifically to tasks and handlers.

Run to completion

When an event triggers an action, that action runs to completion before the entity it belongs to will react to another event. An action does not block midway and wait; it performs a bounded sequence of steps and returns. Combined with the rules already seen in the language basics, that every loop has a statically known bound and that recursion is forbidden, the run-to-completion discipline makes it possible to determine the worst-case duration of each response in advance, as a real-time system requires.

Ports and connections

Reactive entities do not refer to one another, nor to the resources they use, by name. Each entity instead exposes ports, and connections between ports are established separately in the application module when the system is assembled. This separation keeps a component independent of the specific instances to which it is wired.

A sink port receives events, such as timer or interrupt events, and names the action they trigger. An input port receives messages from a queue, and likewise names the action that processes them. An output port sends messages to a queue. An access port grants an entity access to a resource via one of its interfaces. The chapters that follow show how each port type is declared, and the chapter on ports and channels in the next part treats them in full.

The following diagram shows the emitters, the reactive entities, the channels between them, and the resources they access.

Reactive model overview
Events, messages, and shared-state access between the components of a Termina system

Communication, and freedom from deadlock

Components share data through resources accessible via ports, and exchange messages asynchronously through channels, which are bounded message queues. There is no shared mutable global state outside of resources, and no synchronous rendezvous between components. A send places the message on the channel and returns without waiting; if the channel is full, the runtime raises an exception instead of blocking the sender. A resource may call another resource through an access port, but a connection in the application module can only name an instance declared earlier, so the calls between resources follow a directed acyclic graph and never close a cycle. For these two reasons a Termina system is free of deadlock by construction.

Automatic protection

Access to a resource is mutually exclusive, but the programmer never writes the code that enforces it. The transpiler analyzes how each resource is shared and inserts the appropriate protection on its own. Depending on the case, that protection may be a priority-ceiling mutex, when the resource is used by more than one task; the disabling of interrupts, when it is shared between a task and a handler; lock-free atomic access, for the atomic array types; or no protection at all, when a resource is reached by a single entity and none is needed.

The chapters ahead

The three chapters that follow examine each kind of class in turn: the resource class, which defines passive shared components; the task class, which defines the priority-scheduled reactive entities; and the handler class, which defines the entities without a thread of their own that respond to interrupts and other events. The ports introduced here, and the channels through which messages flow, are then treated in detail in the part on communication and memory.