Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to handle resume types with lifetimes #14

Open
ExpHP opened this issue Mar 3, 2020 · 3 comments
Open

How to handle resume types with lifetimes #14

ExpHP opened this issue Mar 3, 2020 · 3 comments

Comments

@ExpHP
Copy link

ExpHP commented Mar 3, 2020

Just something I've run into and have been thinking about. I'm not precisely sure how much it would impact the API (probably too much!), and what the advantages and disadvantages are, or any of the workarounds, but I figured the maintainer might have some thoughts to add on the matter.


Basically: What might it take to be able to get the "resume" type (R) to support short-lived, non-overlapping lifetimes?

Consider the following program, which implements an event loop as an internal iterator.

pub fn main() {
    event_loop(event_handler());
}

pub struct Knob;
impl Knob {
    pub fn get_from_somewhere() -> Self { Knob }
    pub fn frobnicate(&mut self) { println!("tweaking knob"); }
    pub fn use_somehow(&self) { println!("using knob somehow"); }
}

pub struct Event<'a> { knob: &'a mut Knob }
pub enum LoopStatus<T> { Continue, Break(T) }

pub fn event_loop<T>(
    mut callback: impl FnMut(Event<'_>) -> LoopStatus<T>,
) -> T {
    loop {
        let mut knob = Knob::get_from_somewhere();
        match callback(Event { knob: &mut knob }) {
            LoopStatus::Continue => {},
            LoopStatus::Break(x) => return x,
        }
        knob.use_somehow();
    }
}

pub fn event_handler() -> impl FnMut(Event<'_>) -> LoopStatus<()> {
    // Do something, I dunno, implement a finite state machine or something.
    // Here's a very not interesting example.
    let mut times_to_frobnicate = 3i32;
    move |Event { knob }| {
        if times_to_frobnicate > 0 {
            times_to_frobnicate -= 1;
            knob.frobnicate();
            LoopStatus::Continue
        } else {
            LoopStatus::Break(())
        }
    }
}

Of course, the manually writing a state machine is a pain. So, coroutines to the rescue!... right?

use genawaiter::{GeneratorState, yield_};
use genawaiter::stack::{Co, Gen, let_gen_using};
use std::future::Future;

pub fn main() {
    let_gen_using!(gen, |co| event_handler(co));
    event_loop(gen);
}

pub struct Knob;
impl Knob {
    pub fn get_from_somewhere() -> Self { Knob }
    pub fn frobnicate(&mut self) { println!("tweaking knob"); }
    pub fn use_somehow(&self) { println!("using knob somehow"); }
}

pub struct Event<'a> { knob: &'a mut Knob }

pub fn event_loop<T>(
    gen: &mut Gen<'_, (), Option<Event<'_>>, impl Future<Output=T>>,
) -> T {
    gen.resume_with(None);
    loop {
        let mut knob = Knob::get_from_somewhere();
        match gen.resume_with(Some(Event { knob: &mut knob })) {
            GeneratorState::Yielded(()) => {},
            GeneratorState::Complete(x) => return x,
        }
        knob.use_somehow();
    }
}

pub async fn event_handler(co: Co<'_, (), Option<Event<'_>>>) {
    let times_to_frobnicate = 3i32;
    for _ in 0..times_to_frobnicate {
        let Event { knob } = co.yield_(()).await.unwrap();
        knob.frobnicate();
    }
    co.yield_(()).await.unwrap(); // make sure the last knob is used
}

Unfortunately, this fails to build:

error[E0597]: `knob` does not live long enough
  --> src\bin\event-loop.rs:25:50
   |
20 |     gen: &mut Gen<'_, (), Option<Event<'_>>, impl Future<Output=T>>,
   |                                        -- let's call this `'1`
...
25 |         match gen.resume_with(Some(Event { knob: &mut knob })) {
   |               -----------------------------------^^^^^^^^^----
   |               |                                  |
   |               |                                  borrowed value does not live long enough
   |               argument requires that `knob` is borrowed for `'1`
...
30 |     }
   |     - `knob` dropped here while still borrowed

Knowing how Fn traits desugar, the reason why the latter fails while the former succeeds is fairly evident:

  • The original event handler was impl for<'a> FnMut(Event<'a>). Thus, every call of the function could use a distinct lifetime.
  • The new event handler is Gen<'_, (), Option<Event<'a>>> for a single lifetime 'a. This single lifetime is forced to encompass all calls to gen.resume_with.

Typically, the solution to this is to try to either introduce some trait with a lifetime parameter (so that we can have for<'a> Trait<'a>), or to introduce a trait with a method that has a lifetime parameter (trait Trait { fn method<'a>(...) { ... }}). But that's only a very vague plan... I'm not entirely sure where either of these techniques could be applied to the design of genawaiter!

@ExpHP
Copy link
Author

ExpHP commented Mar 3, 2020

On the topic of workarounds, I eventually decided to put the thing as Yield output. In order to properly have yield respond to the previous send, the event handler needs a response variable that always holds the response to the prior yield.

pub struct Event { knob: Knob }
pub struct Response { knob: Knob }

pub fn event_loop<T>(
    gen: &mut Gen<'_, Option<Response>, Option<Event>, impl Future<Output=T>>,
) -> T {
    gen.resume_with(None);
    loop {
        let mut knob = Knob::get_from_somewhere();
        knob = match gen.resume_with(Some(Event { knob })) {
            GeneratorState::Yielded(response) => response.expect("got no response").knob,
            GeneratorState::Complete(x) => return x,
        };
        knob.use_somehow();
    }
}

pub async fn event_handler(co: Co<'_, Option<Response>, Option<Event>>) {
    let mut response = None;
    let times_to_frobnicate = 3i32;
    for _ in 0..times_to_frobnicate {
        let Event { mut knob } = co.yield_(response).await.unwrap();
        knob.frobnicate();
        response = Some(Response { knob })
    }
    co.yield_(response).await.unwrap(); // make sure the last knob is used
}

In my actual code base, this solution is quite a bit uglier, for two reasons:

  • My Event type is an Enum.
  • My &mut field was actually a &mut dyn Trait.
pub enum Event {
    AttachProcess { pid: ProcessId },
    DetachProcess { pid: ProcessId },
    AttachThread { pid: ProcessId, tid: ThreadId },
    DetachThread { pid: ProcessId, tid: ThreadId },
    Exception {
        pid: ProcessId,
        tid: ThreadId,
        context: Box<dyn ModifyContext>,
    },
}

/// Yield type of an event handler.
#[derive(Derivative)]
#[derivative(Debug)]
pub enum Response {
    Exception { context: Box<dyn ModifyContext> },
    Other,
}

// rather than a `_ => {}` branch, you should have a `ev => Response::from(ev)`
// branch when you want to ignore other events
impl From<Event> for Response {
    fn from(e: Event) -> Self {
        match e {
            Event::Exception { context, .. } => Response::Exception { context },
            _ => Response::Other,
        }
    }
}

and the pattern for receiving an event in the handler looks like

response = Some(match co.yield_(response).await.unwrap() {
    ...
});

which feels like it is a bit much to unpack. Also, changing &mut dyn ModifyContext to Box<dyn ModifyContext> now forces the event loop to do downcasting after the event loop receives ownership of the context back from the handler.

// was
let ref mut context = thread.get_context(windows::flags::Context32::ALL)?;
emit!(Event::Exception { pid, tid, code, address, is_chain, context });
thread.set_context(context)?;

// now
let context = Box::new(thread.get_context(windows::flags::Context32::ALL)?);
let context = resume_handler! {
    send: Some(Event::Exception { pid, tid, code, address, is_chain, context }),
    recv: Some(Response::Exception { context }) => context,
};
thread.set_context(context.downcast_ref().expect("changed type of context!"))?;  // <--ugly

@whatisaphone
Copy link
Owner

I poked at your code for a while, and I don't believe it's possible to express what you want in Rust's type system right now (though I'd be happy to be proven wrong!) Let's see if I can explain why. Let's take a look at your event handler:

for _ in 0..times_to_frobnicate {
    let Event { knob } = co.yield_(()).await.unwrap();
    knob.frobnicate();
}

To do what you want, the compiler would need to be able to reason that knob's lifetime starts at the call to yield_, and ends at the next call to yield_. Here's the same with an unrolled loop:

let Event { knob } = co.yield_(()).await.unwrap();
// knob is valid
let Event { knob2 } = co.yield_(()).await.unwrap();
// knob is now invalid

You can express this easily in non-async Rust with a type signature such as func<'a>(&'a mut self) -> Event<'a>. But as far as async code goes, I don't think it's possible. You would need to add that constraint to the signature of Co::yield_, something like this:

impl<A: Airlock> Co<A> {
-    pub fn yield_<'r>(&'r self, value: A::Yield) -> impl Future<Output = A::Resume     > + 'r {
+    pub fn yield_<'r>(&'r self, value: A::Yield) -> impl Future<Output = A::Resume + 'r> + 'r {
        // ...

But the above won't compile, since you can't put + 'a after a type. It can only appear after a trait. What you need here is a generic associated type. Maybe once that RFC is implemented, this will be possible.

Your workaround essentially takes the analysis you wish you could express at the type level, and expresses it at the value level instead 🙂

This problem seems closely related to the streaming iterator problem, which happens to be the motivating example for that RFC. Here's a more recent blog post about it: https://smallcultfollowing.com/babysteps/blog/2019/12/10/async-interview-2-cramertj-part-2/

@LionsAd
Copy link

LionsAd commented Aug 3, 2020

We played around with that exact problem for AsyncTaskFunctions for goose and while it is only tangentially related it is indeed possible to put the lifetime argument for mut arguments into an intermediate struct that works like a proxy for the original function.

My generic approach is here:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c460adce977bb8dffe6c429ed84ad45f

I was not able to make it less complex.

But given the description that it works for sync but not async functions made me think that a wrapper approach might be possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants