Communication with an external device where Input and Output have a mutable shared state

I'm programming an interface to a device called Launchpad. Launchpad features a grid of lightable buttons. My interface sets up an input and an output connection to the device. Additionally, there's a shared state that both the input and the output connection need mutable access to.

I need the shared state because messages to the device affect how messages from the devices are interpreted. An example and how I would implement it:

There's a "device inquiry" byte sequence/message. Launchpad replies to it with device information. The input connection I mentioned will receive the device information. To make sense of it, it needs to know that a device inquiry was sent. Otherwise, it has no idea what the bytes are supposed to mean.

I would try to implement this with a FIFO queue that both the output and the input connection have access to. When the output connection sends the device inquiry message, it pushes a device inquiry token onto the queue. When the input connection receives a message, it will pop the device inquiry token of the queue and process the message accordingly.

This idea, however, clashes with Rust's rules of borrowing and ownership - there can't be two mutable references to the queue at the same time.

This matter is further complicated by the callback that needs to be passed to the input connection. This callback holds a reference to the output connection to trigger a light. This reference is mutable because any action on the output connection might mutate the shared state. So I have another mutable reference...

Here's a short code snippet that contains the core problem

You can use Rc for sharing and RefCell to regain mutability. If you need to be threadsafe then you could use Arc for sharing and Mutex/RwLock to regain mutability.

1 Like

That makes sense, thank you (playground). I tried to stay away from Rc<RefCell> or the thread-safe variant because I've heard that it's not idiomatic but I guess it has its uses.

It still won't fully compile, because the static callback tries to capture output by move, even though it's still borrowed. That's not part of what I wrote in the title though, so I'm gonna try to solve it on my own.

Cool, I'm not sure where you found that they aren't idiomatic, but they are the only way to do shared mutability without using external crates or unsafe.

Can you use typed messages instead? Output sends a typed message (eg Request::DeviceInquiry enum variant) and input returns a Response::DeviceInquiry. Both variants can contain the raw payload (eg Vec<u8>), but the enum variant tells both sides how to interpret them.


Sorry for late reply
I'm not sure I understood that correctly. You mean that input should pass on any messages it receives as raw payload along with the information what kind of message it is so that Output can parse them?
Or do you mean I should replace my queue with two channels in opposite directions, so instead of pushing and popping data of a shared mutable queue, data is sent through one or the other channel?

Something like this

In this example I used a thread and an infinite loop to show how it would work, but it is not needed. The idea is that you can send updates to Output, which will process them and use it to mutate it's state.