How to pass a "pointer" to an async function with generics

Hi all

So, I've successfully used the scheme documented in this post for one solution I've implemented.

Now I need to take this further - where the async function being called has a generic argument ... kinda like this (which, of course, doesn't work):

pub struct ChainError {
    pub msg: String
}
type ChainTaskResult = Result<bool, ChainError>;
type ChainTaskClosure<T: Send + Sync> = Box<dyn Fn(&mut T) -> Pin<Box<dyn Future<Output = ChainTaskResult> + Send>> + Send + Sync >;

pub struct Chain<T: Send + Sync> {
    tasks: Vec<ChainTask<T>>,
}

impl<T: Send + Sync> Chain<T> {
    pub fn new () -> Self {
        Chain {
            tasks: Vec::new()
        }
    }
    pub fn add_task<F>(mut self, task: fn(&mut T) -> F) -> Self
    where 
        F: Future<Output = ChainTaskResult> + 'static + Send 
    {
        self.tasks.push(make_async_caller(task));
        self
    }

}

fn make_async_caller<F,T: Send + Sync>(f: fn(&mut T) -> F) -> ChainTaskClosure<T>
where
    F: Future<Output = ChainTaskResult> + 'static + Send,
{
    Box::new(move |n| Box::pin(f(n)))
}

This (of course) gives

error[E0310]: the parameter type `T` may not live long enough
  --> src/lib.rs:29:5
   |
29 |     Box::new(move |n| Box::pin(f(n)))
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...so that the type `T` will meet its required lifetime bounds

Various efforts to resolve this have failed ... any ideas?

If you look at the whole error message, you'll see that rustc gives you an answer:

error[E0310]: the parameter type `T` may not live long enough
  --> src/lib.rs:32:5
   |
32 |     Box::new(move |n| Box::pin(f(n)))
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...so that the type `T` will meet its required lifetime bounds
   |
help: consider adding an explicit lifetime bound...
   |
28 | fn make_async_caller<F, T: Send + Sync + 'static>(f: fn(&mut T) -> F) -> ChainTask<T>
   |                                        +++++++++

For more information about this error, try `rustc --explain E0310`.

Add the 'static bound to T to make sure T only ever contains owned data and not any references that might not live long enough. Playground.

2 Likes

Thanks for the response - a case of not going far enough, then (I had tried 'static but was not convinced it would work or was correct - still trying to get my mind around exactly what 'static means! :))

As a matter of curiosity, would there be a solution where T could contain references - just asking makes me think it would be impossibly complex though!

Thank you

let your ChainTaskClosure accepts a lifetime parameter

type ChainTaskClosure<'a, T: Send + Sync> = Box<dyn Fn(&mut T) -> Pin<Box<dyn Future<Output = ChainTaskResult> + Send + 'a>> + Send + Sync >;

then

fn make_async_caller<'a, F,T: Send + Sync + 'a>(f: fn(&mut T) -> F) -> ChainTaskClosure<'a, T>
where
    F: Future<Output = ChainTaskResult> + 'static + Send,
{
    Box::new(move |n| Box::pin(f(n)))
}

and try again
the key is you need to tell rust that your T has a lifetime, and reflect it at T's lifetime boundd and the return type

I find that Rust by Example has a good definition of the 'static trait bound:

Took me a little to get the bounds right (forgot to add it to the dyn Fn type in ChainTask), but you can pull it off (if this is actually usable, I don't know). Playground.

Thank you for the pointers - reading assiduously...

NOW, what's there builds nicely ... but now I'm getting this - playground (which I was getting with the static version too)

Ah yes, async fn items capture the lifetimes of their input references, unable to live any longer. See here: async/await - Asynchronous Programming in Rust.

We need to make the lifetime of the mutable reference to T explicit as well, because the closure can never outlive that argument. Furthermore, the function now can not outlive the lifetime of the &'b mut T we pass to our function pointer, instead of the lifetime 'a we added as a bound to T. Lastly, we must make sure that 'a outlives 'b (otherwise we could pass dangling pointers inside of T), so we need to add the 'a: 'b bound. Playground.

Well - freaking he**!!!

Lemme ingest some mind altering substance and read that a thousand times! I MIGHT just get it!

But a 1000 thanks for doing this!

1 Like

The final piece - executing these things - playground

Inevitably the mutable-borrow-in-a-loop problem arises (and even cutting edge stuff like Polonius doesn't "fix" it)

Any help would be appreciated (though I do realize that the original topic of this thread is now a mere dot in the rear view mirror).

As a bound, it means "doesn't contain references shorter than 'static", or equivalently, "valid to store for the 'static lifetime".

(As the parameter of a reference, it means something completely different — it means that the referent actually lives for the 'static lifetime, ie. forever.)

Thank you for the explanation

At this point I don't believe you can pull it off with lifetimes. At least I can't. We'd need HRTBs to express that our tasks don't take ownership for the explicit lifetime 'b of our TestContext instance, but only for some unspecified lifetime (the lifetime of the given loop iteration). But our task functions are not actually implemented for any lifetime 'b, but only for lifetimes 'b that don't outlive 'a (the lifetime of T). Lifetime bounds are currently not supported in HRTBs (there's an open RFC to implement that feature). So without making T 'static to get rid of 'a, I don't think we can get your program to compile.

Well, I'm very glad that I came to that conclusion too - though by much less rigorous thought than your own. I actually have an "owned" version working, where each task takes ownership of the context and then returns it. But that's messy when dealing with errors, which the task functions have to handle "specially". I guess that's what I'm stuck with at the moment, then!

Can't thank you enough for your help in this!

Yeah I agree that this is not ideal when working with Result. Alternatively you can wrap your context in an Arc<Mutex<T>> and pass that to each task. Playground.

Yeahhhhh - I was trying avoid that, but perhaps that IS the door into this - thx

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.