Borrowing through a `yield`

I've been writing some embedded code using japaric's nb crate. The specific task that is being performed is to operate an I2C peripheral on my microcontroller. I have a couple structs that look like so:

pub struct MasterI2c {
    _0: ()
}

impl MasterI2c {
    /// Begins a write transaction with this peripheral. This consumes the proxy
    /// so that the write transaction is the exclusive operation going on.
    ///
    /// At the end of the write, the MasterI2c will be re-instantiated.
    pub fn begin_write<'a>(self, addr: u8, data: &'a [u8]) -> MasterWriteTransaction<'a> {
        ...blah blah blah...
        MasterWriteTransaction { data: data, index: 1 }
    }
}

pub struct MasterWriteTransaction<'a> {
    data: &'a [u8],
    index: usize,
}

impl<'a> MasterWriteTransaction<'a> {
    /// Attempts to end this write. This should be called periodically in order
    /// to keep the transaction going.
    pub fn end_write(&mut self) -> nb::Result<(), MasterI2cError> {
        ...send the next byte, return Ok if the transaction is finished or WouldBlock otherwise...
    }

    /// Finishes this transaction, whether or not the write has been completed.
    /// This method must be called in order to perform another transaction.
    pub fn finish(self) -> MasterI2c {
        ...blah blah blah...
        MasterI2c { _0: () }
    }
}

My concern lies around the begin_write function. I borrow a slice for the duration of the lifetime of the MasterWriteTransaction. This slice contains the data to be sent and it seemed like I should be passing this by reference rather than by value, since copying a large array is not a good thing to do on a microcontroller with 4KB of RAM.

When I try to use this API using nb's await macro (which basically just runs the passed function over and over again, yielding whenever it returns WouldBlock, until it returns an Ok) I run into some problems with the borrow checker:

#[macro_use(await)]
extern crate nb;

fn test() {
    ...blah blah blah...
    let mut i2c = ...methods to create a MasterI2c...

    ...blah blah blah...

    let mut addr_fn = move || {
        let cmd = [0, 0xae];
        loop {
            let mut trans = i2c.begin_write(0x78, &cmd);
            await!(trans.end_write()).unwrap();
            i2c = trans.finish();
        }
    };
    loop {
        addr_fn.resume();
    }
}

Here's the resulting error:

error[E0626]: borrow may still be in use when generator yields
  --> src/main.rs:59:52
   |
59 |             let mut trans = i2c.begin_write(0x78, &cmd);
   |                                                    ^^^
60 |             await!(trans.end_write()).unwrap();
   |             ------------------------- possible yield occurs here
   |
   = note: this error originates in a macro outside of the current crate (run with -Z external-macro-backtrace for more info)

If I'm understanding this correctly, the borrow checker is concerned that while the generator is yielded, someone else could come by and borrow cmd while trans is still borrowing it (which I intended to occur until trans.finish and then the borrow would be complete...but I could easily be misunderstanding, as I have only been playing with rust for a little while now). How can I communicate to the borrow checker that this shouldn't happen? cmd is scoped such that nothing but that generator owns it (I'm not sure if that's the write terminology...but it can't accessed outside addr_fn). I really like being able to pass around variable-length slices in my API, but this seems to be telling me that generators don't like borrows that last through a yield.

There has been a lot of work recently in the area of references across yield points in generators. @withoutboats has a great blog series that covers the topic:

In short, it's not done yet! But you should be able to start using it right now in the nightly compiler. All of the normal API/ABI stability caveats apply.

2 Likes