What is an Active Object?

In a previous post titled “Unit Testing of Active Objects and State Machines” I outlined various strategies for enabling unit tests of an active object and its associated state machine. However, I assumed the reader already understood the active object concept. This post attempts to rectify this short-coming by outlining the core requirements of a successful active object based module or class design.

So, what is an active object? An active object is a concurrency design pattern enabling reliable and safe asynchronous behavior. A typical active object implementation involves some sort of external API, a concurrency safe queue, and a thread to execute the required behavior. Keep reading for more details!

The Fundamentals

Fundamentally, an active object consists of three elements:

  • A public API, providing asynchronous access to the services of the module or class.
  • A concurrency safe queue.
  • A thread, which interprets and executes behavior associated with the queued events.

The following diagram also details the fundamental elements:

Elements of a Basic Active Object Design

Key points:

  • The concept is language agnostic.
    • Use C or C++ or any language supporting the minimum thread and queue requirements.
  • The concept is operating system agnostic.

Public API

Any class or traditional C module nearly always includes a public API. In an active object design, this public API provides asynchronous access to the services or data provided by the module or class in question. The public API primarily injects appropriate data and requests onto the module’s internal concurrency safe message queue. We may be tempted to provide a scattering of synchronous APIs, but these should be considered carefully given the potential concurrency issues such APIs may introduce. When in doubt: make the design’s public API entirely asynchronous. The following pseudocode example demonstrates an example public API implementation:

void MyModuleFooAsync(uint8_t fooData)
{
   //prep event for this request
   Event event;
   event.sig = SIG_FOO;   //event identifier for this function
   event.data = fooData; 

   //Post the event to our active object queue
   PostEvent(&event);
}

Key points:

  • The API is clearly marked “Async”.
  • An enumerated event is created and the event allows for data.
  • Input data is copied, avoiding concurrency issues.
  • The resulting event is posted to the internal active object queue.

Concurrency safe queue

Central to any active object design is an event or message queue. The queue must be concurrency safe such that external users running in another thread or interrupt context may safely push messages onto the queue. Typically this queue is provided by the underlying operating system or RTOS. For example, if authoring firmware with FreeRTOS, we would select a FreeRTOS queue as our concurrency safe queue. Avoid re-inventing this wheel: use a proven concurrency safe event queue ideally provided by the underlying RTOS.

The queue also represents a potential weak link in our active object design. Questions often arise such as:

  • How deep should the queue be?
  • What happens if the queue is full?
  • How will we handle that one pesky event requiring a large data transfer?

Answers to those questions tend to be application specific. For example, a queue holding non-critical events may be allowed to drop events when the queue is full. However, a queue holding critical events (e.g. an “emergency stop” event) must never overflow and must never drop events. When in doubt, a default implementation should assert upon a full queue, treating it as a system failure.

A Thread

The active object’s internal thread is the beating heart of our design. The following pseudocode sample is the simplest expression of an active object thread.

void ActiveObjectTask()
{
   Event event;

   while(true) //loop forever
   {
      //wait/block forever for an event
      GetEventFromQueue(&event); 

      //process the received event
      ProcessEvent(&event);
   }
}

What does the function ProcessEvent() do? In my preferred approach it executes an event driven hierarchical state machine. However, it could be a trivial flat state machine or functions mapped to each unique event type.

Key points:

  • The thread’s overall behavior is trivially observable: wait on a queue, get an event, process the event. Repeat.
  • This thread owns the module’s internal private data. This is a key restriction to avoid concurrency bugs.
  • All code executed by this thread should follow a run-to-completion model with little-to-no blocking or busy waits. This enables a responsive active object and helps avoid various race conditions.

An example C module

To illustrate further, a concrete example project was created for this post, available from github. For build and target details, please see the project’s readme file.

Some background: In my previous post, I provided an example C++ active object design using an object oriented C++ design. In order to distill this topic to the basics, I started with the same service (HwLockCtrlService) and moved it from C++ to C with the same requirements and associated unit tests. To emulate a typical RTOS environment, I created a “FauxRTOS” module loosely modeled after FreeRTOS, but internally using modern C++ threading libraries. If you are interested in seeing the transition from C++ to C, then pull up the C++ project and use your favorite diff tool to view the differences or simply review the commit history starting in April 2021.

The Public API

Our example C module’s public API may be found in the file hwLockCtrlService.h. The module’s C prefix is HLCS, providing for a form of a namespace using a prefix approach common in C modules. The reader should note the following:

  • Separate Init() and Destroy() methods. A Destroy() function is not necessarily common in typical firmware designs, however it is needed to enable clean and predictable unit testing of the module.
  • Functions such as RegisterChangeStateCallback() enable external registration of callbacks for users to receive events from this module. These functions are commented to note that events will be received from a separate thread context.
  • Several functions, all marked with “Async” in their function names, drive the primary behavior of our module.
  • There is a single synchronous getter style function, GetState(), enabling thread safe inquiry into the state of this module. This method must be thread safe, hence the very limited synchronous data access and the use of the C11 Atomic data type.
  • As with the previous post focused on unit testing, the header exposes a function that would normally be private: ProcessOneEvent(…). See the previous post for details on how this is used to enable unit testing of our active object.

The Thread

The HLCS thread follows the pattern noted in our earlier summary section and is therefore not particularly exciting. See HLCS_Task() for details. The thread is executing a simple event driven flat state machine. See the functions ProcessOneEvent(…) and SmProcess(…) for details.

The Queue

The HLCS queue follows the pattern noted in our earlier summary section and is therefore not particularly exciting. The HLCS code will assert if the queue is full. All events are trivial with no data payload. See the two internal private helper functions PushEvent() and PushUrgentEvent() for details.

But what about … ?

Timers

How does an active object handle timed or periodic repeating events? Typically the code uses whatever mechanism is provided by the underlying RTOS. For example, if using FreeRTOS, the module could create a FreeRTOS timer registering an internal private function to be called periodically by the RTOS. This internal timer function in-turn posts an appropriate enumerated event onto the active object’s event queue. This ensures that our active object processes the timed event appropriately and in-order compared to other events.

Interrupts

Nearly all firmware requires timely access to hardware events through the use of interrupts. In an active object design, the interrupts associated with the object are nothing more than another enumerated event, potentially with associated data, which must be posted to our active object’s event queue. However, this does imply that our event queue must be both thread and interrupt safe.

Coupling

Some readers may be concerned with the registration of callbacks to receive events from our active object module. That is understandable as these callbacks will increase coupling amongst modules. Additionally, the callbacks require threading and run-to-completion behavior from the users of the module, which may be difficult to control. If your project could benefit from reduced coupling, then consider a formal framework with publish/subscribe capabilities. An example of such a framework is Samek’s QP framework.

Conclusion and References

Active objects are one of my favorite approaches to enabling robust firmware. This post outlines the key elements necessary to implement this design pattern and provides an example using a traditional C module approach. To learn more, please consider the following references.

2 comments

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Discover more from Cove Mountain Software

Subscribe now to keep reading and get access to the full archive.

Continue reading