Cedux: Experimenting with the Redux Model in C

Article summary

The world of embedded software development can feel like a very isolated place. Earlier in my career, when I was doing mostly embedded work, I remember often feeling jealous of my colleagues who were working on mobile and web applications. I would constantly hear them talking about exciting new libraries, frameworks, and tools with catchy names that supposedly made their lives easier. I was saddened by the lack of excitement and advancement of tools for those of us writing C.

As I’ve progressed in my career, I’ve spent quite a lot time writing both mobile and web applications, and I’ve found that many of the concepts and best practices used in higher-level applications can also apply in C. There’s no reason why we can’t learn from the advancements in other languages and use those lessons to help us write better embedded code.

One technology I’ve been experimenting with is Redux. Redux is a JavaScript library that contains tools to help developers better manage state in their applications.

The general idea is this: Application state is held in an isolated tree called the _store_. When a part of the application wants to mutate the state, it sends (dispatches) an action to the store containing only the data that’s needed to describe the change. The actions are then sent to reducer functions, which have an opportunity to update items in the state tree. The updated snapshot of the state tree is then sent to the application so that it can render views or react to the changes in a pure, predictable way.

I’ve written before about why I believe that thoughtful handling of application state is one of biggest challenges and most important aspects of a software architect’s job. The React style of state management excited me, so I decided to see if I could pull off a similar tool for C. I ended up writing Cedux!

What is Cedux?

Like Redux, Cedux provides a mechanism for creating a store which contains a tree of state. Additionally, it provides a way to register reducer functions and a way to dispatch actions to those reducers. All of this can be done with very little code, as I will demonstrate.

To start, define a structure that will be your state tree. For a simple example, let’s say we’re writing firmware for a toy helicopter. The helicopter has one motor that controls the propeller speed to create lift. The user can press buttons to increase and decrease the elevation of the helicopter. An altimeter on the toy is used to determine the current elevation.

In this case, the state tree might look something like this:


struct helicopter_state {
  uint32_t motor_speed;
  uint32_t desired_elevation;
  uint32_t current_elevation;
};

Next, we need to define a type that can be used to represent the actions that can be dispatched to the store. A tagged union is a great data structure to use for actions. For example, we might do something like this:


enum helicopter_action_type {
  NEW_ELEVATION,
  INCREASE_ELEVATION,
  DECREASE_ELEVATION,
};

struct absolute_elevation {
  uint32_t elevation;
};

struct elevation_change {
  int16_t delta;
};

struct helicopter_action {
  enum helicopter_action_type type;
  union {
    struct absolute_elevation new_elevation;
    struct elevation_change increase_elevation;
    struct elevation_change decrease_elevation;
  };
};

Our action data structure, helicopter_action, contains everything we need to describe updates to the current elevation, as well as changes to the desired elevation. Now we can create our store! Somewhere in our application, outside of any function, we can employ the following macro to do just that.


CEDUX_DEFINE_STORE(struct helicopter_state, struct helicopter_action, my_store);

The first parameter is our state tree data structure, the second parameter is our action data structure, and the third parameter is the name that will be used as the suffix for the generated functions and data structures. The name parameter allows you to create multiple stores if you desire to do so.

This macro will define and declare a data structure that will be our store. It will also generate three functions.

Init store

The first generated function is the store initializer. In this case, it will be cedux_init_my_store, which takes a pointer to our store as a parameter. Somewhere in the beginning of our application, we’ll need to call it like so:


cedux_init_state(&my_store);

The store consists of our state tree struct, a queue to which actions will be dispatched, and a list which will store all registered reducers. The init function performs the necessary initialization of said queue and the list.

Register reducers

The next function generated by the above macro is the cedux_register_reducer. In this case, it will be called cedux_register_reducer_my_store, and it will take two parameters: a pointer to the store, and a function pointer to the reducer function. The reducer function must take two parameters: a pointer to the state tree data structure and an action. We defined the action type above.

For our example application, we might define the following reducers:


void handle_new_elevation(struct helicopter_state * p_state, struct helicopter_action action)
{
  if (action.type == NEW_ELEVATION) {
    p_state->current_elevation = action.new_elevation.elevation;
    uint32_t new_speed = determine_new_speed(p_state->desired_elevation, p_state->current_elevation);
    p_state->motor_speed = new_speed;
  }
}

void handle_new_desired_elevation(struct helicopter_state * p_state, struct helicopter_action action)
{
  if (action.type == INCREASE_ELEVATION) {
    // Do some bounds checking of course...
    p_state->desired_elevation += action.increase_elevation.delta;
  } else if (action.type == DECREASE_ELEVATION) {
    // Do some bounds checking of course...
    p_state->desired_elevation -= action.decrease_elevation.delta;
  } else {
    return;
  }
  uint32_t new_speed = determine_new_speed(p_state->desired_elevation, p_state->current_elevation);
  p_state->motor_speed = new_speed;
}

Now, to register these reducers, all we have to do is this:


cedux_register_reducer_my_store(&my_store, handle_new_elevation);
cedux_register_reducer_my_store(&my_store, handle_new_desired_elevation);

Dispatch actions

Finally, the last function generated by the macro is the cedux_dispatch_action. In this case, it will be called cedux_dispatch_my_store, and again, it will take two arguments: the first being the pointer to the store, and the second being an instance of the action type.

In our example application, we would dispatch actions from a couple of different places. We might have an interrupt that gets fired when the altimeter produces a new value. In our altimeter ISR, we could write:


void altimeter_isr() {
  uint32_t new_elevation = read_elevation();
  cedux_dispatch_my_store(&my_store, (struct helicopter_action){
    .type = NEW_ELEVATION,
    .elevation = new_elevation
  });
}

The beauty here is that this code need not be concerned with who or what is going to be using this elevation; nor should it be. This code is only responsible for handling the event that a new elevation is available, and all it has to do is dispatch that information. It doesn’t care what it’s used for.

We might also have an interrupt that gets fired whenever the user pushes the button to increase or decrease the desired elevation. The Up ISR would look very similar, but it would dispatch a different action to the store.


void up_button_isr() {
  cedux_dispatch_action(&my_store, (struct helicopter_action){
    .type = INCREASE_ELEVATION,
    .increase_elevation = {
      .delta = 10
    }
  });
}

Again here, the button ISR needs zero knowledge of what happens when the button is pressed. All it needs to do is convey the fact that the button was pressed. It is the responsibility of the reducers to handle these actions and update the state accordingly. A huge bonus here is that reducers are also dead-simple to unit test because they don’t interact with any device-specific registers or peripherals.

So that’s it. That’s Cedux. I wish I could claim credit or the idea, but obviously, I just borrowed somebody else’s really good idea and leveraged it in a different space. I think that’s what being a good programmer is all about—continuously reading, learning new things, trying things, failing, trying something else, and then improving the code you produce via the lessons you’ve learned.

Cedux is still in its infancy. At the time I’m writing this, I haven’t actually used it in a real application yet. Stay tuned for my next post, where I’ll provide a demo to do exactly that.

If you’d like to give Cedux a try, you can get the source and read more details here.