bacelar8
bacelar8

Reputation: 21

Is the observer pattern impossible to implement in embedded Rust?

I work with C/C++ for embedded software development and I've been learning Rust recently trying to implement the same concepts I use at work.

In our application, the observer pattern is largely used for communication between components, where Observer and Publisher generic classes are inherited by the component wanting to implement its corresponded behavior. Everything is static, so the Publisher class knows at compile time the size of the array for storing the Observer references attached. This allows us to instantiate everything in the beginning of the program, making all the necessary attaches using polymorphism before letting the idle task do it's work.

Example in C++

This is a header library, but I'm hiding the implementation for simplicity:

template <typename T>
class Observer {
    public:
        virtual void Update(const &T data) = 0;
}

template <typename T, uint8_t N = 1u>
class Publisher {
    public:
        bool Subscribe(Observer<T>& observer);
        bool Unsubscribe(const Observer<T>& observer);
    protected:
        void Notify(const T& data);
    private:
        std::array<Observer<T>*, N> observers;
}

Basically, as usual in the observer pattern, the publisher stores an array of references to the listeners that have been attached to it, and once the notify function is called, every observer is notified. This allows us to inherit these classes depending on the role a component should have, where a component A can be an observer and/or publisher to one or different types.

Rust's Implementation

When trying to replicate this in Rust, I quickly realize that the concept goes against the language's paradigm. Taking this simple example:

trait Listener<T> {
    fn update(&self, data: &T);
}

struct Publisher<'a, T, const N: usize = 1> {
    listeners: [Option<&'a dyn Listener<T>>; N],
}

impl<'a, T, const N: usize> Publisher<'a, T, N> {
    fn new() -> Self {
        Self {
            listeners: [None; N],
        }
    }

    fn subscribe(&mut self, listener: &'a dyn Listener<T>) {
        for slot in self.listeners.iter_mut() {
            if slot.is_none() {
                *slot = Some(listener);
            }
        }
    }

    fn notify(&self, data: &T) {
        for listener in self.listeners.iter().flatten() {
            listener.update(data);
        }
    }
}

When implementing it for a given Component:

#[derive(Default)]
struct Component {
    data: Cell<u32>,
    flag: Cell<bool>,
    some_counter: u8,
}

impl Component {
    fn update_counter(&mut self) {
        self.some_counter += 1;
    }
}

impl Listener<u32> for Component {
    fn update(&self, data: &u32) {
        self.data.replace(*data);
    }
}

impl Listener<bool> for Component {
    fn update(&self, data: &bool) {
        self.flag.replace(*data);
    }
}

fn main() {
    // Listener
    let listener = Component::default();

    // Publisher u32
    let mut publisher_u32: Publisher<'_, u32> = Publisher::new();

    // Publisher bool
    let mut publisher_bool: Publisher<'_, bool> = Publisher::new();

    publisher_u32.subscribe(&listener);
    publisher_bool.subscribe(&listener);

    publisher_u32.notify(&0);
    publisher_bool.notify(&false);
}

It doesn't seem like the correct approach, since giving an immutable reference to the Publisher kinda prevents every future modification I need to do on the Component, since a concurrent mutable reference isn't allowed, like in a Component's method that takes &mut self:

#[derive(Default)]
struct Component {
    data: Cell<u32>,
    flag: Cell<bool>,
    some_counter: u8,
}

impl Component {
    fn update_counter(&mut self) {
        self.some_counter += 1;
    }
}
...

// At the end of the main function, this won't compile...
component.update_counter();

Conclusion

It seems to me that inter component communication isn't the right approach in Rust, which honestly makes development hard specially when the system's architecture is specified as so. I know that's the whole point of the ownership principle, but the fact that the publisher can't have a mutable reference to the listener and that updating the listener can't take a &mut self, makes the updating process useless, since the only thing I can do is changing a member's value in a Cell but no further actions can be done synchronously inside the listener to handle the new value, forcing me to de-synchronize and spawn the handling of the new updated value to later.

What are your thoughts about it? Am I missing something here or I'm just facing the problem with a wrong approach?

Upvotes: 2

Views: 82

Answers (0)

Related Questions