Argument requires that ... is borrowed for `'static`

  1. Start with #![deny(elided_lifetimes_in_paths)] at the root of your src/{lib,main}.rs file.

  2. From there, it should lint about the missing lifetime parameter in, for instance, render's signature, expecting something like:

    fn render (mut f: impl FnMut(&mut RenderPass<'_>))
    
  3. Now to apply the rules of lifetime elision:

    1. Identify the lifetime parameters / placeholders / "holes":

      //                           v               vv
      fn render (mut f: impl FnMut(&mut RenderPass<'_>))
      

      i.e.,

      //                            vv                vv
      fn render (mut f: impl FnMut(&'_ mut RenderPass<'_>))
      
    2. Find the closest "function-signature boundary" surrounding those parameters. These can be:

      • The fn render function itself;

      • fn(…) -> … pointers

      • impl or dyn Fn{,Mut,Once}(…) -> … traits

    3. And, "at that level", for each placeholder in function-input position, introduce new and distinct generic lifetime parameters / names.

      In our case, the "level" is FnMut:

      // pseudo-code!
      fn render (
          mut f: impl FnMut<'_0, '_1> (&'_0 mut RenderPass<'_1>),
      )
      

      In the case of traits or fn(…) -> … pointers, these generics are actually written using for<…> syntax:

      // Real code!
      fn render (
          mut f: impl for<'_0, '_1> FnMut(&'_0 mut RenderPass<'_1>),
      )
      

These are called Higher-Rank Trait Bounds (see also Lifetime Quantification and Higher-Ranked Trait Bounds — Infinite Negative Utility), and are used to express an infinite sum/family of bounds; the bound applies for every possible choice of each lifetime parameter:

for<'_0, '_1> Stuff<'_0, '_1>

// is the same as

Stuff<'static, 'static> +
Stuff<'static, 'a> +
Stuff<'static, 'b> +
Stuff<'static, '…> +

Stuff<'a, 'static> +
Stuff<'a, 'b> +
Stuff<'a, '…> +

Stuff<'b, 'static> +
Stuff<'b, 'b> +
Stuff<'b, '…> +

Stuff<'…, 'static> +
Stuff<'…, 'b> +
Stuff<'…, '…> +

In our case, for<'_0, '_1> FnMut(&'_0 mut RenderPass<'_1>) thus includes a bound such as:

FnMut(&'whatever mut RenderPass<'static>)

This is how 'static managed to creep into your code :grinning_face_with_smiling_eyes:.

To fix this, back to the pseudo code, remember that you had:

fn render (
     mut f: impl FnMut<'any, …> (&mut RenderPass<'any>),
)

where 'any had that universal / higher-level / higher-quantification that meant that it included the 'any = 'static requirement: the caller has no say in this!

What the caller can choose are generic parameters at the level of the function itself (here, render):

  • higher-order generic lifetimes: universal, not chosen by the caller! :warning:

    • Often these are needed when the lifetime is callee-chosen
  • "normal" generic lifetimes: chosen by the caller :white_check_mark:

So we want to do:

fn render<'caller_chosen> (
    mut f: impl FnMut(&mut RenderPass<'caller_chosen>),
)

and this does remove the 'static requirement.

We thus reach the following

which does compile at the call-site, but now the callee-site fails with:

error[E0597]: `encoder` does not live long enough
  --> src/main.rs:31:20
   |
28 | fn render<'caller_chosen>(mut f: impl FnMut(&mut RenderPass<'caller_chosen>))
   |           -------------- lifetime `'caller_chosen` defined here
...
31 |     let mut pass = encoder.pass();
   |                    ^^^^^^^^^^^^^^
   |                    |
   |                    borrowed value does not live long enough
   |                    assignment requires that `encoder` is borrowed for `'caller_chosen`
...
36 | }
   | - `encoder` dropped here while still borrowed

So here it turns out that the higher-order lifetime was warranted / we did need to let the callee pick its own "arbitrary"(ly small) lifetime.

  • We will need to go back to for<'encoder> lifetime parameters.

But when we did do that, Rust complained about an arbitrarily big lifetime ('static)!

Well, now we know that we want to allow for arbitrarily low / small lifetimes! And it just happens that the naïve / default for<'any> syntax…

  • requires support for arbitrarily small lifetimes (good for the callee) :white_check_mark:

  • while also requiring support for arbitrarily big lifetimes (bad for the caller!) :x:

So we just need to opt out of the latter.

In an ideal world, this would be written as:

// pseudo-code!
fn render<'upper_bound, F> (mut f: F)
where
                    // 'upper_bound ⊇ 'encoder
    for<'encoder where 'upper_bound : 'encoder>
        F : FnMut(&mut RenderPass<'encoder>)
    ,

Sadly we cannot yet introduce explicit bounds for higher-order lifetimes; we'll thus need to ruse and abuse the fact that we can use implicit bounds!

fn render<'upper_bound, F>(mut f: F)
where
    for<'enc>
        F : FnMut(&mut RenderPass<'enc>, [&'enc &'upper_bound (); 0])
        //                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^
        //                         HACK: this empty array parameter,
        //                         akin to `()` or `PhantomData`,
        //                         carries no runtime data.
        //                         But in the type-system, at compile-time,
        //                         the ref represents a type which is
        //                         only valid when the inner lifetime
        //                         is bigger than the outer one;
        //                         i.e., mentioning this type yields
        //                         a `'upper_bound : 'enc` bound
    ,
{
    let mut encoder = Encoder {};
    let mut pass = encoder.pass();
    
    f(&mut pass, []);
    
    drop(pass);
}
  • Playground

  • This quirk may be more readable by defining a

    type PhantomBound<'big, 'small> = PhantomData<&'small &'big ()>;
    

    and then write the signature as:

    for<'enc>
        F : FnMut(&mut RenderPas<'enc>, PhantomBound<'upper_bound, 'enc>)
    ,
    

Improving the ergonomics

If you don't want that extra quasi-vestigial parameter to be that visible in the closure, if you control the definition of RenderPass, you can embed the 'upper_bound inside it, which yields the following API (which does compile!):

fn main ()
{
    let value = 1;
    
    render(|pass| {
        pass.set(&value);
    });
}

fn render<'upper_bound, F>(mut f: F)
where
    for<'enc>
        F : FnMut(&mut RenderPass<'enc, 'upper_bound>)
    ,
// or just:
    F : FnMut(&mut RenderPass<'_, 'upper_bound>),
{
    let mut encoder = Encoder {};
    let mut pass = encoder.pass();
    
    f(&mut pass);
    
    drop(pass);
}

The implementation of it is then just:

#![deny(elided_lifetimes_in_paths)]
  
  struct Encoder {}
  
  impl Encoder {
-     pub fn pass<'a>(&'a mut self) -> RenderPass<'a> {
+     pub fn pass<'a>(&'a mut self) -> RenderPass<'a, 'static> {
          RenderPass { parent: self, _upper_bound: <_>::default() }
      }
  }
  
- struct RenderPass<'a> {
+ struct RenderPass<'a, 'upper_bound : 'a> {
      parent: &'a Encoder,
+     _upper_bound: ::core::marker::PhantomData<&'upper_bound ()>,
  }
  
- impl<'a> RenderPass<'a> {
+ impl<'a> RenderPass<'a, '_> {
      pub fn set(&mut self, _x: &'a i32) {}
  }
  • EDIT: this is the approach since taken by the standard library itself in their scoped threads API: notice their 'env upper bound on the higher-order 'scope.
34 Likes