Structs with temporary buffers

I am working on a library for work which (currently?) requires a structure to represent a state machine. During the processing of some states, I need to attach a mutable slice of u8s to serve as a temporary buffer. Since I'm working with hardware in an embedded context, I don't have the benefit of relying on the standard library. (Though, to be fair, I've tried this with the standard library, and I still can't figure this out.)

The general flow of control that is strongly desired is shown in the pseudo-code below:

sm = StateMachine::new(...);

let mut myBufferHere = Vec::<u8>::new();
myBufferHere.resize(MY_SIZE, 0);

// Give state machine ownership for the duration of an I/O read operation.
sm.send_read_cmd(..., myBufferHere);
while sm.state.get() != State::Done {
    sm.poll();
}

let myBufferHere = sm.reclaim_buffer_somehow();
// interrogate contents of buffer, yada yada, ...

How would I implement something like this?

Thanks.

If you can use std or alloc, you can hold the buffer as Option<Vec<u8>>. Otherwise perhaps Option<&mut [u8]> or some other owned vec-replacement type.

You can give it back by using option.take().

If the sm object is shared, then use RefCell<Option<T>>.

Lifetimes of &mut [u8] will be painful. Alternative design is to make a throw-away object only for the duration of the StateMachine+buffer combo:

struct Combined<'a, 'b> {
   state: &'a mut StateMachine, buffer: &'b mut [u8],
}

and implement buffer-dependent methods on this.

I did try this approach, but lifetimes continue to prevent compilation.

I want to avoid having to implement a TypeState pattern. Consider the while loop in my example pseudo-code above, where the body of the code will be ignorant of type changes that as the state machine progresses.

Is there a reason you can't pass the buffer to poll?

That is a possibility I'd not thought about. It feels clumsy, but it would work, I think... I'll give that a shot and see how it goes in the code review session. :slight_smile:

If you can make poll on the state machine take any extra parameters you need, you could use this strategy to wrap the state machine in a config type that managed the buffer lifetime

Then you could enforce that the right options were always passed to poll by making those configure steps require you to build the right config struct which can pass it's options to the inner state machine.

But if you really only need to manage the buffer for this one command that might be overkill ¯\_(ツ)_/¯

I am sorry; I did not provide a complete picture. The other half of the equation is that there are interrupt handlers which will invoke methods on the state machine as well.

This means that a device driver will (somehow) need to register a buffer with the state machine as well, which just shifts the problem away from the application and to the device driver.

Basically a sequence like this would be an issue:

  1. State machine is created and a read is started.
  2. Time passes. An interrupt occurs, and a read data payload is delivered to the state machine. However, we can't copy the data into the receiving buffer here because poll isn't being executed, and so no buffer reference exists. Somehow, the IRQ handler needs to tell the application where to find the data and return right away.
  3. Application calls poll again, and picks up the data from the buffer the IRQ handler shared.
  4. After the data has been copied, the application or state machine must notify the interrupt handler that it can reclaim its buffer again.

I just don't see how to pull this off without using raw pointers.

Yeah you will probably need raw pointers or some other kind of dynamic ownership then.

You could take some inspiration from the newly stable scoped threads API for how to ensure the buffer slice stays valid while the state machine keeps a raw pointer (and length) to that memory though to help minimize the unsafety.

I'd do something like this...

sm = StateMachine::new(...);

let mut myBufferHere = Vec::<u8>::new();
myBufferHere.resize(MY_SIZE, 0);

// StateMachine is consumed and converted to a StateMachineWithBuffer
smwb = StateMachineWithBuffer::new(sm, myBufferHere);

// use your StateMachineWithBuffer

// StateMachineWithBuffer is consumed and converted to a (StateMachine, Vec<u8>).  
let (sm, myBufferHere) = smwb.inner();

That allows StateMachine and StateMachineWithBuffer to have different interfaces that are checked at compile time. For example, if only StateMachineWithBuffer has a poll method then you cannot possibly call poll unless your state machine has a buffer.

1 Like

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.