[][src]Macro stackbox::custom_dyn

macro_rules! custom_dyn {
    (
    #![dollar = $__:tt]
    $( #[doc = $doc:expr] )*
    $pub:vis
    dyn $Trait:ident $(
        <
            $($lt:lifetime),* $(,)? $($T:ident),* $(,)?
        >
    )?
        : $super:path
    $(
        where { $($wc:tt)* }
    )?
    {
        $(
            fn $method:ident (
                $self:ident :
                    $(
                        & $($ref:lifetime)?
                        $(mut $(@$mut:tt)?)?
                    )?
                    Self
              $(,
                $arg_name:ident : $ArgTy:ty )*
                $(,)?
            ) $(-> $RetTy:ty)?
            {
                $($body:tt)*
            }
        )*
    }
) => { ... };
    (
    // Missing dollar case
    $( #[doc = $doc:expr ] )*
    $pub:vis
    dyn $($rest:tt)*
) => { ... };
}

Helper macro to define custom StackBox<dyn …> trait objects.

Mainly useful for FnOnce, when higher-order lifetimes are needed.

What are higher-order lifetimes?

Consider the following example: you are able to create some local value within a function, and want to call some provided callback on a borrow of it.

fn with_nested_refcell (
    nested: &'_ RefCell<RefCell<str>>,
    f: fn(&'_ str),
)
{
    f(&*nested.borrow().borrow())
}

The main question then is:

what is the lifetime of that borrow?

  1. If we try to unsugar each elided lifetime ('_) with a lifetime parameter on the function, the code no longer compiles:

    This example deliberately fails to compile
    fn with_nested_refcell<'nested, 'arg> (
        nested: &'nested RefCell<RefCell<str>>,
        f: fn(&'arg str),
    )
    {
        f(&*nested.borrow().borrow())
    }
    Error message
    error[E0716]: temporary value dropped while borrowed
     --> src/lib.rs:8:9
      |
    3 | fn with_nested_refcell<'nested, 'arg> (
      |                                 ---- lifetime `'arg` defined here
    ...
    8 |     f(&*nested.borrow().borrow())
      |         ^^^^^^^^^^^^^^^---------
      |         |
      |         creates a temporary which is freed while still in use
      |         argument requires that borrow lasts for `'arg`
    9 | }
      | - temporary value is freed at the end of this statement
    
    error[E0716]: temporary value dropped while borrowed
     --> src/lib.rs:8:9
      |
    3 | fn with_nested_refcell<'nested, 'arg> (
      |                                 ---- lifetime `'arg` defined here
    ...
    8 |     f(&*nested.borrow().borrow())
      |     ----^^^^^^^^^^^^^^^^^^^^^^^^-
      |     |   |
      |     |   creates a temporary which is freed while still in use
      |     argument requires that borrow lasts for `'arg`
    9 | }
      | - temporary value is freed at the end of this statement
    

  2. So Rust considers that generic lifetime parameters represent lifetimes that span beyond the end of a function's body (c.f. the previous error message).

    Or in other words, since generic parameters, including generic lifetime parameters, are chosen by the caller, and since the body of a callee is opaque to the caller, it is impossible for the caller to provide / choose a lifetime that is able to match an internal scope.

  3. Because of that, in order for a callback to be able to use a borrow with some existential but unnameable lifetime, only one solution remains:

    That the caller provide a callback able to handle all the lifetimes / any lifetime possible.

    • Imaginary syntax:

      fn with_nested_refcell<'nested> (
          nested: &'nested RefCell<RefCell<str>>,
          f: fn<'arg>(&'arg str),
      )
      {
          f(&*nested.borrow().borrow())
      }

      the difference then being:

      - fn with_nested_refcell<'nested, 'arg> (
      + fn with_nested_refcell<'nested> (
            nested: &'nested RefCell<RefCell<str>>,
      -     f: fn(&'arg str),
      +     f: fn<'arg>(&'arg str),
        )
      

This is actually a real property with real syntax that can be expressed in Rust, and thus that Rust can check, and is precisely the mechanism that enables to have closures (and other objects involving custom traits with lifetimes, such as Deserialize<'de>) operate on borrow over callee locals.

The real syntax for this, is:

fn with_nested_refcell<'nested> (
    nested: &'nested RefCell<RefCell<str>>,
    f: for<'arg> fn(&'arg str), // ≈ fn<'arg> (&'arg str),
)
{
    f(&*nested.borrow().borrow())
}
  • For function pointer types (fn):

    for<'lifetimes...> fn(args...) -> Ret
  • For trait bounds:

    for<'lifetimes...>
        Type : Bounds
    ,
    // e.g.,
    for<'arg>
        F : Fn(&'arg str)
    ,

    or:

    Type : for<'lifetimes...> Bounds
    // e.g.,
    F : for<'arg> Fn(&'arg str)

Back to the ::serde::Deserialize example, it can be interesting to observe that DeserializeOwned is defined as the following simple trait alias:

DeserializeOwned = for<'any> Deserialize<'any>

This whole thing is called Higher-Rank Trait Bounds (HRTB) or higher-order lifetimes.


Ok, time to go back to the question at hand, that of using the custom_dyn! macro in the context of StackBoxes:

So the problem is that the currently defined types and impls in this crate do not support higher-order lifetimes. Indeed, although it is possible to write an impl that supports a specific higher-order signature, it is currently impossible in Rust to write generic code that covers all the possible higher-order signatures:

  1. For instance, given an impl<A> Trait for fn(A) { … }, we won't have fn(&str) implementing Trait, since fn(&str) = for<'any> fn(&'any str) ≠ fn(A).

  2. And even if we wrote impl<A : ?Sized> Trait for fn(&A) { … }, we won't have fn(&&str) implementing Trait, since

    fn(&&str) = for<'a, 'b> fn(&'a &'b str) != for<'c> fn(&'c A) = fn(&A)

That's where custom_dyn! comes into play:

  • I, the crate author, cannot know which higher-order signature(s) you, the user of the library, will be using, and sadly cannot cover that with a generic impl.

  • But since you know which kind of signature you need, I can "let you impl yourself". Sadly, the actual impl is complex and error-prone (involves unsafe and VTables!), so instead I provide you this handy macro that will take care of all the nitty-gritty details for you.

In a way, since I am hitting a limitation of the too-early-type-checked Rust metaprogramming tool (generics), I am falling back to the duck-typed / only-type-checked-when-instanced metaprogramming tool (macros), thus acting as a C++ template of sorts, we could say 😄

Example: dyn FnOnce(&str) = dyn for<'any> FnOnce(&'any str)

The following example fails to compile:

This example deliberately fails to compile
use ::stackbox::prelude::*;

//                       `f: StackBox<dyn FnOnce(&'arg str) -> ()>`
fn call_with_local<'arg> (f: StackBoxDynFnOnce_1<&'arg str, ()>)
{
    let local = format!("...");
    f.call(&local)
}

fn main ()
{
    stackbox!(let f = |_: &str| ());
    call_with_local(f.into_dyn());
}
Error message
error[E0597]: `local` does not live long enough
 --> src/some/file.rs:8:12
  |
5 | fn call_with_local<'arg> (f: StackBoxDynFnOnce_1<&'arg str, ()>)
  |                    ---- lifetime `'arg` defined here
...
8 |     f.call(&local)
  |     -------^^^^^^-
  |     |      |
  |     |      borrowed value does not live long enough
  |     argument requires that `local` is borrowed for `'arg`
9 | }
  | - `local` dropped here while still borrowed

This is exactly the same problem we had when I was explaining higher-order signatures and we had fn nested_refcells<'nested, 'arg>...: the lifetime of the parameter is an outer (fixed) generic lifetime parameter, and it can thus not work with a local / callee-specific lifetime.


The solution is to define a new DynFnOnce... trait, which involves a higher-order lifetime in the signature:

use ::stackbox::prelude::*;

custom_dyn! {
    pub
    dyn FnOnceStr : FnOnce(&str) {
        fn call (self: Self, arg: &str)
        {
            self(arg)
        }
    }
}
//                 `f: StackBox<dyn FnOnce(&str) -> ()>`
fn call_with_local (f: StackBoxDynFnOnceStr)
{
    let local = format!("...");
    f.call(&local)
}

fn main ()
{
    stackbox!(let f = |_: &str| ());
    call_with_local(f.into_dyn());
}