3

I have a function that requires an asynchronous callback (a request handler); I'm currently trying to accept things that look like this:

async fn handle_request<'a>(request: Request, body: &'a mut (dyn AsyncRead + 'a)) -> HandlerResponse

It had been working up until the addition of the second parameter body, which is causing me grief. The function that accepts the parameter looks like this:

pub async fn process_requests<H, F>(
    mut connection: Box<dyn AsyncConnection>,
    request_handler: &H,
) -> Result<(), DecodeError>
where
    for<'a> H: Fn(crate::Request, &'a mut (dyn AsyncRead + 'a)) -> F + 'a,
    F: Future<Output = HandlerResponse>,
{

Part way through this function, we call a helper function:

handle_request(&mut connection, request_handler, request)

which has a very similar signature; in particular, the signature for request_handler is identical. It does some minor pre-processing before invoking request_handler. When I attempt to compile this, I get:

error[E0310]: the parameter type `H` may not live long enough
    |
106 | pub async fn process_requests<H, F>(
    |                               - help: consider adding an explicit lifetime bound `H: 'static`...
...
142 |                     handle_request(&mut connection, request_handler, request)
    |                     ^^^^^^^^^^^^^^
    |
note: ...so that the type `H` will meet its required lifetime bounds
    |
142 |                     handle_request(&mut connection, request_handler, request)
    |                     ^^^^^^^^^^^^^^

Why do I need this / what do I do about this? Indeed adding 'static to H: in the where does seem to silence the error, but is that the right thing to do? Couldn't the type implementing H carry a reference, and 'static would forbid that? I don't necessarily want to do that, but any amount of trying to annotate a lifetime that isn't 'static onto H has not worked; e.g., 'a + Fn(...) -> F + 'a does not work, nor does adding a new generic 'b lifetime. I'd rather not 'static something that doesn't need it, but I don't see how to do that.

(I'm also a bit perplexed by the wording of the message — that the parameter type — not some argument or variable — doesn't live long enough. How does a type not live long enough?)

I've played with things a bit more, but I still can't get anything that actually compiles. This playground example shows another one of the more perplexing error messages that I'm running into. I've tried dropping some of the lifetime annotations, and moved the for<'a> bit (I'm not sure what the difference is?).

Boiethios
  • 38,438
  • 19
  • 134
  • 183
Thanatos
  • 42,585
  • 14
  • 91
  • 146
  • Since this is a common sense, in case if you missed it, i want to point: `'static` lifetime boundary doesn't mean that it is going remain until the process terminates. `'static` boundary usually used to request ownership of the variable. – Ömer Erden Feb 10 '20 at 07:45
  • _I'd rather not 'static something that doesn't need it, but I don't see how to do that._ We need an owner since we don't exactly know when `asycn fn` will be executed. Simply we can't be sure that the borrowed argument will live on that moment. It needs to be `static` or `owned` or `unmovable`(refering `Pin`s in here, which i have superficial knowledge about it) – Ömer Erden Feb 10 '20 at 07:51
  • While the explanation in your second comment is correct, I disagree with your first statement _`'static` boundary usually used to request ownership of the variable_. A `'static` lifetime bound means the borrow has `'static` lifetime, meaning the entire duration of the program. It is still a borrow though. – L. Riemer Feb 10 '20 at 08:11
  • Do reading [this](https://stackoverflow.com/a/42648637/8511998) address your issue? – L. Riemer Feb 10 '20 at 08:31
  • @L.Riemer It may not cover the whole concept but i don't think it is wrong, It basically ensures that `H` can be a argument type as `String` not a `&String`.(assuming `String` has implemented `Fn`). If you really need a static borrow you need to define your parameter like : `&'static H` – Ömer Erden Feb 10 '20 at 08:37
  • It is wrong in that transferring ownership includes giving up ownership in one scope and acquiring it in an other. With `&'static` you don't do that. Most times, such values are binary-included globals, which you can't possibly own (or closures, which have this lifetime by birthright). That's a strict difference. – L. Riemer Feb 10 '20 at 08:49

1 Answers1

2

A callback argument passed as reference does not work with HRTB constraints when the callback is marked with the async keyword.

The signature using async/await:

async fn handle_request<'a>(request: Request, body: &'a mut (dyn AsyncRead + 'a)) -> HandlerResponse

Is equivalent to:

fn handle_request<'a>(request: Request, body: &'a mut (dyn AsyncRead + 'a)) -> Future<Output=HandlerResponse> + 'a

This implies that input lifetimes of an async function are captured in the future returned by the async function.

See the paragraph "Lifetime capture in the anonymous future" of RFC 2394.

Declaring the function that accepts the parameter as:

pub async fn process_requests<H, F>(
    mut connection: Box<dyn AsyncConnection>,
    request_handler: &H,
) -> Result<(), DecodeError>
where
    for<'a> H: Fn(crate::Request, &'a mut (dyn AsyncRead + 'a)) -> F + 'a,
    F: Future<Output = HandlerResponse>,
{

Give a compilation error because the HRTB requirement:

for<'a> H: Fn(crate::Request, &'a mut (dyn AsyncRead + 'a)) -> F + 'a

"unlink" the lifetime bound from the caller and produce the compilation error expected bound lifetime parameter 'a, found concrete lifetime

for more details about HRTB see here.

To make it works you have to write:

pub async fn process_requests<'a, H, F>(
    mut connection: Box<dyn AsyncConnection>,
    request_handler: &H,
) -> Result<(), DecodeError>
where
    H: Fn(crate::Request, &'a mut (dyn AsyncRead + 'a)) -> F + 'a,
    F: Future<Output = HandlerResponse>,
{

But this get you to another problem:

`body` does not live long enough

because the local body struct does not outlive request_handler:

async fn handle_request<'a, H, F>(
    request_handler: &H,
    request: Request,
) -> io::Result<()>
where
    H: Fn(Request, &'a mut (dyn AsyncRead + 'a)) -> F,
    F: Future<Output = String>,
{
    let mut body = Body {};
    request_handler(request, &mut body);
    unimplemented!();
}

If feasible one possible solution could be to use Boxed trait objects and get rid off HTRB constraints.

attdona
  • 17,196
  • 7
  • 49
  • 60
  • I'm aware that input lifetimes to `async fn`s end up "on" the `impl Future`; I mostly follow your answer. But you say, "this gets you to another problem" "`body` does not live long enough" — and that `body` must outlive `request_handler` — why must it outlive `request_handler`? I would only expect it to need to outlive the future that `request_handler` returns, no? Oh, b/c you've removed the HRTB. Yes, w/o the HRTB, I agree, `body` does not live long enough / I agree w/ the tail end of your post. That was what pushed me to HRTB; but why does it not work w/ the HRTB? – Thanatos Feb 17 '20 at 02:29
  • As for your recommendation to `Box` trait objects… would you mean the `Fn` or the `AsyncRead`, or both? The `Fn` would probably be considerably simpler if `Box`ed, and I think I'll do that. The `AsyncRead` has a reference to something `handle_request` needs, unfortunately, _and_ that function makes use of it after the callback (`request_handler`). – Thanatos Feb 17 '20 at 02:40
  • It does not work with HRTB because the arg of`process_requests(&handler)` depends on an `AsyncRead` reference that depends on a bound lifetime `'b`: `async fn handler<'b>(..., body: &'b mut (dyn AsyncRead + 'b))` But in your `process_request` you are declaring an HRTB constraint `for<'a>Fn(crate::Request, &'a mut (dyn AsyncRead + 'a))` that is incompatible with the "single" bounded lifetime requirement stated with the def of `handler`. – attdona Feb 17 '20 at 13:22
  • I was generic about the Boxed trait suggestion because it really depends on your requirements. – attdona Feb 17 '20 at 13:31
  • I'm still not following the reasoning on why the HRTB example doesn't work. You call it a "bound lifetime", but in what way (particularly at the site of the error, which is is it "bound"? Is it not "bound" when I finally pass in a `&dyn AsyncRead`, at which point the lifetime is "bound" to that of the concrete `AsyncRead` that I am passing in? Will it not work as written, or at all? If not at all, how it not a bug in the language that I can't pass this function as a generic argument? – Thanatos Feb 18 '20 at 02:43
  • See [here](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9d3f4135acd47371168d1e24cc27227e), perhaps it can help. – attdona Feb 18 '20 at 08:25
  • I eventually moved this to a trait that had a member `fn`; after I did that, I realized the member fn needed to be async, and so then I ran into async-trait and http://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/ ; I *think* perhaps that the problems outlined there are essentially what I'm running into here? At any rate, dtolnay's async-trait worked out, and I think the core advice here — get rid of the need for HTRB — was pretty close to what I needed. – Thanatos Mar 07 '20 at 19:18