[−][src]Macro stackbox::custom_dyn
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?
-
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 compilefn 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
-
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.
-
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 StackBox
es:
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:
-
For instance, given an
impl<A> Trait for fn(A) { … }
, we won't havefn(&str)
implementingTrait
, sincefn(&str) = for<'any> fn(&'any str) ≠ fn(A)
. -
And even if we wrote
impl<A : ?Sized> Trait for fn(&A) { … }
, we won't havefn(&&str)
implementingTrait
, sincefn(&&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:
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()); }