-
Notifications
You must be signed in to change notification settings - Fork 28
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
Comments
On the topic of workarounds, I eventually decided to put the thing as 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:
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 // 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 |
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 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 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 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/ |
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: 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. |
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.
Of course, the manually writing a state machine is a pain. So, coroutines to the rescue!... right?
Unfortunately, this fails to build:
Knowing how
Fn
traits desugar, the reason why the latter fails while the former succeeds is fairly evident:impl for<'a> FnMut(Event<'a>)
. Thus, every call of the function could use a distinct lifetime.Gen<'_, (), Option<Event<'a>>>
for a single lifetime'a
. This single lifetime is forced to encompass all calls togen.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!The text was updated successfully, but these errors were encountered: