What’s the Reactor Pattern, Anyway?
The Reactor is an OOP architectural pattern that allows event-driven applications to demultiplex and dispatch service requests that are delivered to an application from one or more clients. It allows a single-threaded application to handle multiple I/O events from different sources concurrently without blocking.
It is particularly useful in resource-constrained environments where the overhead of creating multiple threads is prohibitive.
It originates from the UNIX era so it is intended for use in systems with operating systems but can surely be applied in bare-metal embedded environments.
Its core components are:
- Handle: identifies event sources that can produce and queue indication events, from either external or internal requests
- Event Handler: defines an interface with a set of callback methods that represents the dispatching operations available to process events
- Concrete Event Handler: specializes the event handler for a particular service, and implements the callback methods
- Reactor: defines an interface that allows applications to register or remove event handlers and their associated handles, and run the application's event loop. A reactor uses its synchronous event demultiplexer to wait for indication events to occur on its handle set. When this occurs, the reactor first demultiplexes each indication event from the handle on which it occurs to its associated event handler, then it dispatches the appropriate hook method on the handler to process the event.
- Synchronous Event Demultiplexer: a function called to wait for one or more indication events to occur on a set of handles—a handle set. This call blocks until indication events on its handle set inform the synchronous event demultiplexer that one or more handles in the set have become 'ready', meaning that an operation can be initiated on them without blocking.
The above diagram is a direct reproduction from «Pattern-Oriented Software Architecture, Patterns for Concurrent and Networked Objects, Volume 2» by Douglas Schmidt.
A Light Weight Bare-Metal Approach
In the original approach, a handle represents an event source. The OS can watch the handles and mark them as «ready» when an associated event occurs. Each handle can be associated with multiple event types such as READ_EVENT, WRITE_EVENT, etc.
In the PIC mcu world the handles could be thought of as hardware peripherals e.g. UART, DMA, TIMER etc., which in most of the cases will generate only one meaningful event, by setting an asscociated interrupt bit. So, in practice, handles and event types overlap.
For example, the UART1 peripheral can be considered a handle and the UART1 Receive Interrupt Flag bit (U1RXIF) represents the event. Since we only care about this specific interrupt, we can treat the handle and the event type as a unified concept.
Let’s alter the original implementation to best fit our light-weight bare-metal approach. The following UML diagram is designed with C in mind:
The Event Handler Interface serves as a common abstraction for all event handlers. Each handler is declared as a const struct and encapsulates both the associated event and its callback mechanism.
The Event_t enumeration assigns each event a unique power-of-two value, somewhat enabling a simple multiplexing mechanism· all active events can be represented within a single variable. This approach allows for straightforward demultiplexing. By applying bitwise masks to the event_flags variable, the reactor can quickly determine which events are set and dispatch the associated callbacks.
After registering the event handlers in the handlers_array, the reactor iterates through each entry and invokes the corresponding callback when its associated event is set.
Events can be raised either from the main app or from the ISR, by setting the appropriate bit of the event_flags variable.
The process is straight forward and very well suited for an 8-bit mcu.
Code implementation in C
This implementation targets deployment on a PIC microcontroller and supposes a timer-driven interrupt that periodically polls the hardware peripherals and raises the associated event flags.
The reactor loop just monitors the global event_flags variable and invokes the appropriate handler when a matching event is detected.
Although using a global variable for the event flags isn't ideal from a software architecture standpoint, it provides a notable advantage in this context. On PIC microcontrollers, bit manipulation operations are executed in a single instruction cycle, by BSF and BCF instructions. As a result, a direct bit operation on the event_flags ensures atomicity and eliminates race conditions between the ISR and the main app.
Reactor.h
#ifndef REACTOR_H
#define REACTOR_H
#define HANDLERS_COUNT 8
#include <stdint.h>
#include <stdbool.h>
typedef enum {
EVENT_TYPE1 = 1 << 0,
EVENT_TYPE2 = 1 << 1,
EVENT_TYPE3 = 1 << 2,
EVENT_TYPE4 = 1 << 3
} event_t;
typedef struct {
void (*callback)(void);
event_t event;
}event_handler_t;
extern __near volatile event_t g_event_flags;
void REACTOR_Init(void);
void REACTOR_RegisterEventHandler(const event_handler_t *handler);
void REACTOR_RemoveEventHandler(const event_handler_t *handler);
void REACTOR_HandleEvents(void);
Reactor.c
#include "reactor.h"
#include <stddef.h>
#include <string.h>
__near volatile event_t g_event_flags = 0;
static const event_handler_t* handlers_array[HANDLERS_COUNT];
void REACTOR_Init(void){
//nullify all array entries
memset(handlers_array, 0, sizeof(handlers_array));
}
void REACTOR_RegisterEventHandler(const event_handler_t *handler) {
for (uint8_t i = 0; i < HANDLERS_COUNT; i++) {
if (handlers_array[i] == NULL) {
handlers_array[i] = handler;
return;
}
}
}
void REACTOR_RemoveEventHandler(const event_handler_t *handler) {
for (uint8_t i = 0; i < HANDLERS_COUNT; i++) {
if (handlers_array[i] == handler) {
handlers_array[i] = NULL;
return;
}
}
}
void REACTOR_HandleEvents(void) {
while (1) {
if (!g_event_flags) continue;
event_t event_flags_snapshot = g_event_flags;
g_event_flags &= ~event_flags_snapshot; //clear the flags
for (uint8_t i = 0; i < HANDLERS_COUNT; ++i) {
const event_handler_t *handler_ptr = handlers_array[i];
if (handler_ptr!=NULL && handler_ptr->event & event_flags_snapshot) {
if (handler_ptr->callback) handler_ptr->callback();
}
}
}
}
handler1.c
#include "reactor.h"
static void Handler(void){
//implementation
//...
//...
//...
//might also raise an other event
//g_event_flags|= EVENT_TYPE4;
}
const event_handler_t EventHandler1 = {
.callback=&Handler,
.event=EVENT_TYPE1
};
handler2.c
#include "reactor.h"
static void Handler(void){
//implementation
//...
//...
//...
}
const event_handler_t EventHandler2 = {
.callback=&Handler,
.event=EVENT_TYPE2
};
handlers.h
#include "reactor.h"
extern const event_handler_t EventHandler1;
extern const event_handler_t EventHandler2;
isr.c
#include <xc.h>
#include "reactor.h"
void __interrupt() INTERRUPT_InterruptManager(void){
if (PIE4bits.TMR1IE==1 && PIR4bits.TMR1IF==1){
if (PIR3bits.U1RXIF){
PIR3bits.U1RXIF=0;
g_event_flags|= EVENT_TYPE1; //ASM: BSF g_event_flags, 0, ACCESS
}
if (PIR2bits.DMA1SCNTIF){
PIR2bits.DMA1SCNTIF=0;
g_event_flags|= EVENT_TYPE2; //ASM: BSF g_event_flags, 1, ACCESS
}
//...
//...
//...
PIR4bits.TMR1IF == 0;
}
main.c
#include "reactor.h"
#include "handlers.h"
REACTOR_Init();
REACTOR_RegisterEventHandler(&EventHandler1);
REACTOR_RegisterEventHandler(&EventHandler2);
//endless loop
REACTOR_HandleEvents();
One basic flaw of the reactor pattern is that a long-running event handler can block the Reactor. If only we could have some basic task switching mechanism of an RTOS system, right?
There is an easy way out: just re-trigger the event and yield.
The handler establishes a simple yield mechanism that breaks its workload into smaller chunks. This way, it performs just a portion of the task, saves its progress and signals the Reactor to revisit it.
void Handler(void){
static uint8_t val=99;
for (uint8_t cnt=val;cnt>=0;cnt--){
printf("Counter is %hhu \n",cnt);
if (!cnt){
val=99;
break;
}else if (cnt%10==0) {
printf("yielding...\n");
val=cnt-1;
g_event_flags|=EVENT_TYPE1;
break;
}
}
}
The Reactor will process all other active events and will re-dispatch the handler in the very next loop iteration.