Consuming and replacing a struct member

I'm writing a state machine which relies on the fact that it isn't Clone for memory safety (as it is a proxy for operations that are not data-race safe). In addition, it is using the nb crate to support non-blocking operation (without programming myself into requiring nonblocking operation, as is the goal of nb). This means that I need to provide a polling function that can be called over and over again and directly returns a result of type nb::Result.

Note that this is targeting an ARM Cortex-M microcontroller and I am using no_std.

For this question, here is a contrived example


pub struct MainState {
    _0: ()
}

impl MainState {
    fn try_advance(&mut self) -> nb::Result<AdvanceResult, MyError> {
        // although this method takes a mut self, it is actually an
        // idempotent method. Calling it over and over does not actually
        // mutate self. The mut self is to enforce that the unsafe memory
        // memory operations that happen here are free from races.
        // ... do unsafe stuff ...
    }
}

pub enum AdvanceResult {
    Advance(AdvanceToken),
    Done,
}

pub struct AdvanceToken {
    fn advance(self, state: MainState) -> IntermediateState {
        ...
    }
}

pub struct IntermediateState {
    _0: ()
}

impl IntermediateState {
    fn do_a_thing(self, data: u8) -> MainState {
        ...
    }
}

My specific problem lies in the AdvanceToken::advance method requiring that the passed StateMachine be consumed. Let's say I want to wrap this state machine in a convenient struct (perhaps to facilitate a borrow of some data):

pub struct Convenience<'a> {
    state: MainState
    data: &'a [u8],
    index: u8
}

impl<'a> Convenience<'a> {
    pub fn try_finish(&mut self) -> nb::Result<(), MyError> {
        match self.state.try_advance() {
            //...blah blah handle Err(nb::Result::WouldBlock) and other stuff..
            Ok(result) => match result {
                AdvanceResult::Advance(t) => {
                    self.state = t.advance(self.state).do_a_thing(self.data[self.index]);
                    self.index += 1;
                    Err(nb::Result::WouldBlock)
                }
                AdvanceResult::Done => Ok(())
            }
        }
    }
}

We now have a problem. The self.state = t.advance(self.state).do_a_thing(self.data[self.index])); line causes the borrow checker to complain. It rightfully states that I am "moving self.state out of borrowed content". So yes, I move it out, but I put it back later. What I basically need to be able to do is consume a member of a struct, promising the borrow checker that I will replace it with another value (without actually using mem::replace, since I don't have anything to replace it with until I consume it), and then replace it with the new value.

Is there any way to solve this problem? Or should I reconsider how I am doing this? I keep running into this over and over again when I try to build state machines that use a type to represent each state, since I tend to keep those state machine instances in structs elsewhere.

2 Likes

Would using an Option, using mem::swap with None, and then mem::swapping back when you're done work?

1 Like

There's https://crates.io/crates/take_mut which gives you basically take_mut(&mut T, Fn(T) -> T). It is made safe by aborting on panic, which I don't think you need to care about under no_std anyway. (I hope that the catch_unwind is optimized out in that case...)

1 Like

Hmm. While the Option is nice because it wouldn't require any new dependencies, I'm really liking take_mut since it seems likely to optimize fairly well and in a straightforward manner, whereas the Option would likely end up consuming more memory (I only have 4KB to work with :grin:).

Edit: It would appear that take_mut depends on std. I've ended up forking the crate and making a no_std variant. The downside is that I had to get rid of the abort, since abort doesn't exist when using only libcore. I also had to get rid of the panic catch too, since panic actually ends up compiling to "udf #254" when using thumbv6, so there's no way to catch it anyway.

Was looking for this. In my case I wanted to replace the struct member.