How to drive/wake up a Future with an hardware interrupt

Hi there,

I'm quite new to Rust and trying to understand the Future concepts. So far I'm trying to proof my knowledge while implementing a small embedded/bare metal no_std Future "runtime".
I'm currently stuck at a certain point.
I understand that the Future is polled whenever the Waker is notified to poll the Future for the next time.
I've seen many examples where this is triggered inside the poll function like so:

fn poll(&mut self, ctx: &Context) -> Poll<Self::Output> {        
        match self.counter {
            10 => Poll::Ready(10),
            _ => {
                self.counter += 1;
                ctx.waker().wake();
                Poll::Pending
            }
        }
    }

So the waker to poll the Future again is given in the context to the poll function from the Executor.
In my scenario I'd like to wait for an interrupt to trigger the waking of the Future. What is a good solution for this?
The interrupt handler is a function that does not know about the instance of the Future that would like to get woken up. How could I link the Waker given by the poll function to a signal/semaphore/flag the Interrupt handler is able to set? Or am I getting the concepts wrong and need to deal with the interrupt handler and the waking in the Executor?

Any hint would be much appreciated.
Thanks in advance.

1 Like

This example isn't very good, because it's completely synchronous and misses the key point. If your future is not ready, you have to save a copy of the Waker for later. In your case it'd probably be some global array.

You can't call wake() from an interrupt, so you'll need another level of indirection. You'll need a thread that waits for an interrupt somehow, and then call wake() on all previously saved wakers.

Hi,

thx for the response. So to get your proposal right it would look like this in pseudo code:

static IRQ_WAKER: &Waker;

fn irq_handler() {
    WAKER_TO_WAKE.push(IRQ_WAKER);
}

impl MyFuture {
    fn poll(ctx: &Context) {
        match state {
             not_ready => IRQ_WAKER = ctx.waker().clone();
    }
}

static WAKER_TO_WAKE: Vec<Waker>;
fn main_waker_thread() {
     for w in WAKER_TO_WAKE {
         w.wake();
         WAKER_TO_WAKE.remove(w);
     }
}

Does this make sense?

IRQ_WAKER doesn't make sense. You can't know how many times poll() is called between interrupts. Mutex<Vec<Waker>> should be enough to add and remove them.

Apart from that it's roughly the right direction. You'll probably want a more efficient way to sleep in main (or if you're ok with busylooping, then atomic flag set from an interrupt handler?)

1 Like

Hi,
well I'm with you having a list of Wakers an IRQ might need to trigger as different Futures might wait for the same IRQ, but my understanding would be, that for one Future(-instance) the poll is called at minimum once and then only when the associated Waker of this future is signaled. Or am I getting something wrong with the current design of how a Future is processed?

Thx again for your support.

Why do you say this? My expectation (and implementations) directly wake the future from an interrupt. (An interrupt is just a very limited form of thread, and wakers must be thread safe).

I would recommend setting up your hardware handling such that a single instance of some type owns the hardware device, and that instance can have at most one outstanding future, that way you can get away with having only one globally accessible waker to share between the future and the interrupt instead of needing an array (or maybe a few known wakers for different uses, e.g. independent TX and RX sides of a communication device).

1 Like

Interrupt-safety is much stricter than thread-safety. For example, it's unsafe to use a Mutex from an interrupt. This means you can't touch anything that could allocate new memory.

IIRC you only need to care about one (last) waker per Future, therefore self.waker = ctx.waker.clone() would be fine. However, there may be many futures, so you can't clobber a single global waker from a number of futures. You don't have a guarantee that an interrupt will run between two poll() calls, so the wakers you overwrite won't be called, and the other futures may be stuck forever.

1 Like

Ah, you're talking about POSIX interrupts I guess? I was thinking of embedded interrupts where you generally have a target-specific Mutex/other synchronization primitive that is interrupt-safe.

However, there may be many futures, so you can't clobber a single global waker from a number of futures.

That's what I was talking about with setting the handling up such that there is a single value that owns the hardware device and interrupt handler. If that only allows a single future to exist at a time you know there can only be one relevant waker (having multiple tasks depending on that future would require something like Shared which multiplexes all of those tasks wakers down to a single waker).

1 Like

Technically if there's one instance of the future globally, it would work. But for this API, that's a very odd thing to exist. Futures have a single ownership, and are one-time-use only. The contract of poll is that once it's ready, you're not allowed to use it again.

If you wrap that in Shared, you're just making a convoluted verison of lazy_static.

1 Like

So the specific example I was basing this on is wrapping a UART interface. In that case you aren't providing a Future as the API but instead a pair of Tx: AsyncWrite and Rx: AsyncRead. From those two types you can create futures that have a unique borrow of the Tx or Rx instance for sending/receiving, therefore there can only be one of those futures in existence for each direction at any point in time, so you only have to allocate storage for 2 wakers in the context shared between the Tx/Rx types and the interrupt handler.

I don't have a good example of where you would actually use Shared with a setup like this, for IO you would need something else sharing it at a higher level granting unique access to one user at a time until their transfer is complete. It was more of an example that to have multiple wakers interested in a single event you need to have something performing the multiplexing of that event out to all the wakers, so the event only has a single waker it actually needs to worry about.

Hi there,

thx for your input so far ;). I'm not sure whether I can completely follow your different approaches as they feel quite abstract to me.

What I got so far:
There should be a shared list of Waker the interrupt handler, the Future has access to. The Waker stored in this list should be somewhat shared references to the Waker provided by the Context the poll function of the Future is called with...

The poll function is than pushing it's waker to this global list and the interrupt handler is reading this list and waking the waker if the handler is called.

This awaking would then picked up by the main eventloop to call poll for this specific Future again.

Does this make sense?
As a PoC I'm implementing a timer based Future like this:

static TIME_WAKER: Singleton<Vec<Arc<Waker>>> = Singleton::new(Vec::<Arc<Waker>>::new());

#[IrqHandler(ArmTimer)]
fn thought_timer() {
    TIME_WAKER.take_for(|tw| {
        for w in tw.into_iter() {
            w.wake();            
        }
        tw.clear();
    });
}

impl Future for TimedFuture {
    type Output = u32;

    fn poll(&mut self, ctx: &Context) -> Poll<Self::Output> {
        match self.counter {
            10 => Poll::Ready(10),
            _ => {
                self.counter += 1;
                // store the waker for this Future into the waker list the interrupt can access  
                TIME_WAKER.take_for(|tw| {
                    tw.push(ctx.waker());
                });
                Poll::Pending
            }
        }
    }
}

Does this look like the approaches you had in mind?
What I'm not sure about is this:

Futures have a single ownership, and are one-time-use only

Is this constrain somehow ensured with additional code I'm missing?

Thx and I really appriciate your efforts helping me out to learn more on this stuff :wink:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.