Skip to content

Brainstorming: what could a Future-based libtock-rs look like? #494

@jrvanwhy

Description

@jrvanwhy

This week's discussions have convinced me to take another look at how libtock-rs could integrate with the futures ecosystem. Unfortunately, I'm having a hard time seeing how that could work. I decided to open this issue to gather ideas.

For example: what should the async equivalent of this API look like?

/// Reads data from the UART into `buffer`. Blocks
/// until the buffer is completely full, then returns.
fn read(buffer: &mut [u8]) { ... }

One idea is to return a type that implements Future which completes when the read is finished:

/// Fills `buffer` with data from the UART. The returned
/// future completes when `buffer` has been filled.
fn read(buffer: &mut [u8]) -> impl Future<Output = ()> { ... }

However, I don't see a sound way to implement that API. If read() calls ReadWriteAllow, then the following invocation causes a use-after-free:

{
    let mut buffer = [0; 8];
    // Shares buffer with the kernel
    let future = read(&mut buffer);
    // Prevents the un-allow from happening
    core::mem::forget(future);
}
// The kernel has a dangling ReadWriteAllow here.

We could delay sharing buffer with the kernel until the returned Future is polled, but this is unsound as well:

{
    let waker = /* Waker creation omitted because it's verbose */;
    let mut buffer = [0; 8];
    let mut future = Box::pin(read(&mut buffer));
    // Shares buffer with the kernel.
    let _ = future.as_mut().poll(&mut Context::from_waker(&waker));
    // Prevents the un-allow from happening.
    core::mem::forget(future);
}
// The kernel has a dangling ReadWriteAllow here.

I can think of other designs that are probably sound, but impractical and/or unergonomic:

// Note the 'static -- this only works with global buffers! IIRC that's
// unacceptable for Ti50, and very expensive in general.
fn read(buffer: &'static mut [u8]) -> impl Future<Output = &'static mut [u8]> { ... }

// Pass ownership of the buffer, relies on dynamic memory allocation. Probably
// more practical than the above but still expensive.
fn read(buffer: Box<[u8]>) -> impl Future<Output = Box<[u8]>> { ... }

Any other ideas on how this could work?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions