Firmware with extension points

I would like to provide a template for a firmware that should be extensible by other programmers.

The hardware might be a bit different from project to project but in general it has buttons, LEDs, Analog Inputs and MIDI I/O.

Currently I am struggling to provide an extension point for the processing.

My Idea was to expose a trait which can be implemented for a concrete firmware.

pub trait Handler {
    fn handle_human_input(&mut self, e: InputEvent) -> Actions;
}

Ideally there could be multiple such implementations provided which could be switched between at runtime (in the music hardware industry, such configs are often called Banks).

I learned about static and dynamic dispatch in Rust (there are very good articles and youtube videos around).

Currently I am using a static dispatch approach with an enum which also implements the Handler trait.
This works pretty well but is not very easy to extend, since every new implementation (Bank) will add a new enum variant.

I was wondering if also some dynamic dispatch approach would be possible by using trait objects. However, many of the examples (also the Rust book) are based on std:boxed::Box.

I was wondering what would be a pragmatic counterpart in the embedded no_std world?

Is the static Enum based dispatcher already the most pragmatic solution?

A &dyn Trait doesn't require boxing:

trait Runnable {
    fn run(&mut self);
}

struct X;

impl Runnable for X {
    fn run(&mut self) {
        println!("X");
    }
}

struct Y(i32);

impl Runnable for Y {
    fn run(&mut self) {
        println!("Y {}", self.0);
        self.0 += 1;
    }
}

fn run_them(objects: &mut [&mut dyn Runnable]) {
    loop {
        for obj in objects.iter_mut() {
            obj.run();
        }
    }
}

fn main() {
    let mut obj0 = X;
    let mut obj1 = Y(0);
    run_them(&mut [&mut obj0, &mut obj1]);
}

The additional complexity (sorry I forgot to mention that) is that I am using rtic and currently the Runnable are stored within a shared resource.

When trying to do that with a &dyn Trait I am getting

the trait `Send` is not implemented for `(dyn Handler + 'static)`

Storing the handlers by using an enum based static dispatcher works perfectly.
Same problem also when storing them as local rtic resource.

Since the enum-based dispatcher sees the concrete types, it knows that they are Send. Trait objects hide traits that you don't explicitly list.

dyn Handler + Send

Thank you @tbfleming for guiding me through this journey. Its a steep learning curve (at least for me).

I was thinking about it from my understanding its save to add Send to to the Handler because they are protected with a lock on the RTIC Task. I think otherwise I would also have the issues with the static dispatched code.

But now I am heading the next puzzle: How to I create the trait objects with the lifetime required for RTIC.
Since the Handlers (at least currently) would live for the whole lifetime of the application. 'static should be ok?

But how would I create a trait object with an explicit lifetime?

PS: I hope this journey might help also other newbies that are facing similar problem with Embedded RTIC based development

I'm unfamiliar with rtic, so I looked it up to see what it needs. It looks like rtic needs to know all mutexes in advance (#[shared]). One way to do this is to pack all of the handlers into a single struct and add an instance of that struct to Shared, like you probably did with the enum. It looks like that may take care of all of the lifetime and Send/Sync requirements. Once you have a lock on that structure, you can convert its members to trait objects to pass to generic functions.

Untested; I can get qemu to work with other embedded frameworks, but something about rtic's examples seems to throw it off.

#![no_main]
#![no_std]
#![feature(type_alias_impl_trait)]

use test_app as _; // global logger + panicking-behavior + memory layout

pub trait Runnable {
    fn run(&mut self);
}

pub struct X;

impl Runnable for X {
    fn run(&mut self) {
        defmt::info!("X");
    }
}

pub struct Y(i32);

impl Runnable for Y {
    fn run(&mut self) {
        defmt::info!("Y {}", self.0);
        self.0 += 1;
    }
}

pub fn run_them(objects: &mut [&mut dyn Runnable]) {
    for obj in objects.iter_mut() {
        obj.run();
    }
}

pub struct MultipleRunnables {
    x: X,
    y: Y,
}

impl MultipleRunnables {
    fn with_trait_objects<F: FnOnce(&mut [&mut dyn Runnable])>(&mut self, f: F) {
        f(&mut [&mut self.x, &mut self.y]);
    }
}

#[rtic::app(
    device = stm32f3xx_hal::pac,
    dispatchers = [SPI1]
)]
mod app {
    use crate::*;

    #[shared]
    struct Shared {
        runnables: MultipleRunnables,
    }

    #[local]
    struct Local {}

    #[init]
    fn init(_cx: init::Context) -> (Shared, Local) {
        defmt::info!("init");

        task1::spawn().ok();

        (
            Shared {
                runnables: MultipleRunnables { x: X, y: Y(0) },
            },
            Local {},
        )
    }

    #[task(priority = 1, shared = [runnables])]
    async fn task1(mut cx: task1::Context) {
        defmt::info!("Hello from task1!");
        cx.shared
            .runnables
            .lock(|runnables| runnables.with_trait_objects(run_them))
    }
}

The below is a reply about Rust and not your specific scenario. Not sure if it will help at all with the latter, but perhaps it will be worth something for your mental model of Rust.

Be careful equating lifetimes to the liveness of values. Lifetimes are a property of types, not values. Local variables can have types that satisfy a 'static bound but aren't alive for the whole runtime, for example.

That said, if you need a single value to be always usable (like some sort of singleton), it's probably going to satisfy a 'static bound.


All trait objects have a lifetime. If the base type you're coercing to dyn Trait satisfies a 'static bound, then you can coerce it to dyn Trait + 'static. So if you need to create a dyn Trait + 'static, you just need to make sure the implementing types are also 'static -- don't contain any non-'static references.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.