#Lifetime woes with blanket traits over `FnMut` types

35 messages · Page 1 of 1 (latest)

limpid prawn
#

I'm working on adding dynamic, reflection-based function types for the Bevy game engine. This is meant to allow something like the following:

let mut add: Func = Foo::add.into_func();
let args: ArgList = ArgList::default().push_mut(&mut foo).push_owned(50);
add.call(args).unwrap();
println!("{:?}", foo);

This almost works. The issue is that the lifetimes require me to add a drop(add) just before the println!, since foo is "already borrowed as mutable" and "mutable borrow might be used here, when add is dropped and runs the destructor for type Func<'_>".

Of course, I want to be able to call add multiple times if necessary. And even if not that, I don't want to have to manually drop/scope add just so I can resume using foo.

Here are the relevant types and traits (simplified a bit):

enum Arg<'a> {  
    Owned(Box<dyn Reflect>),  
    Ref(&'a dyn Reflect),  
    Mut(&'a mut dyn Reflect),  
}

/// Used to convert `Arg<'a>` to one of:
/// 1. `T`
/// 2. `&'a T`
/// 3. `&'a mut T`
/// This allows us to avoid manually hundreds of impls with different ownership combinations
trait FromArg<'a>: Sized {
    fn from_arg(arg: Arg<'a>, info: &ArgInfo) -> Result<Self, ArgError>;  
}

struct Func<'a> {    
    func: Box<dyn FnMut(ArgList<'a>, &[ArgInfo]) -> FuncResult>,  
}

trait IntoFunc<'a, T> {  
    fn into_func(self) -> Func<'a>;  
}

And this is the impl that manages to compile:

impl<'a, T0: FromArg<'a>, R: Reflect, F: FnMut(T0) -> R> IntoFunc<'a, fn(T0) -> R> for F {
    fn into_func(mut self) -> Func<'a> {
        Func {
            func: Box::new(move |mut args, info| {
                let output = (self)(args.take_1(info)?);
                to_func_return(output)
            }),
        }
    }
}

I think the problem is because ArgList shares the lifetime 'a with Func. However, other configurations don't seem to work.

#

Note that I tried using HRTBs but those seem to cause issues in the impl itself:

struct Func {
    // 1. Causes `borrowed data escapes outside of closure` unless #2
    func: Box<dyn for<'a> FnMut(ArgList<'a>, &[ArgInfo]) -> FuncResult>,  
}

impl<
    // 2. Causes `no method named `into_func` found for fn item` when used on the `Foo::add` function
    T0: for<'a> FromArg<'a>,
    R: Reflect,
    F: FnMut(T0) -> R
>
    IntoFunc<fn(T0) -> R> for F {
    // ...
}

Any ideas for how I could address this? Are there any safe/unsafe lifetime shenanigans I can pull to make it work?

solid geyser
#

you are correct to think of HRTB here

#

it may not work with one, but it definitely won't work without one

#

but T0: for<'a> FromArg<'a>, won't do because you're trying to convert the lifetime-bearing arg type into a non-lifetime-bearing T0, which is always going to fail (unless that conversion is a borrowed-to-owned conversion), which isn't the case here

#

you need an impl like impl<T, F: for<'a> FnMut(&'a T)> IntoFunc<...> for F

#

and you will have to have a separate (and non-overlapping) impl for owned Ts if you want to support them

limpid prawn
#

Ah so the only solution for this would be to create all combinations of ownership impls then?

#

So for 16 args I think that would be 540 impls (not counting the return type combinations) 😅

#

Wait no

#

A lot more lol

novel reef
#

well, reasonably runnable not including bevy

limpid prawn
#

It includes three main modules:

  1. basic - the one that compiles but requires the drop
  2. hrtb1 - adding HRTB to just Func
  3. hrtb2 - adding HRTB to the IntoFunc impl
limpid prawn
novel reef
#

wow, that's great help for testing ferrisPray

novel reef
limpid prawn
#

No way! I had actually tried to do that but got stuck because F expected T0 and not T0::Item

#

I'll try it out in the actual codebase to see if any issues pop up that weren't caught in my makeshift playground

#

Yeah just testing a bit here and it looks like that adding that second bound on F did the trick! Hopefully it stays that way lol

#

@novel reef do you have a GitHub account? If I make a PR I can be sure to credit you for your help here 🙂

limpid prawn
#

Oh boy looks like the next challenge is going to be getting R to convert into:

pub enum Return<'a> {
    Unit,
    Owned(Box<dyn Reflect>),
    Ref(&'a dyn Reflect),
    Mut(&'a mut dyn Reflect),
}
#

I guess you can't reference lifetimes in the associated Output type of F (FnOnce)

novel reef
novel reef
#

it'd have to be a "real" argument lifetime

#

I'm slightly tempted to just ping Yandros lol

#

I know one of the workarounds is to have a [&'a (); 0] argument, which you just pass as [], but that requires the user to have that in the function's argument list

limpid prawn
#

Yeah and that’s something we’re trying to avoid. We want it to be as frictionless as possible

#

I suppose we could just require that it returns owned data. It’s unfortunate but better than nothing

limpid prawn
limpid prawn
#

Okay @frank quartz explained it to me so I think I understand how that’s supposed to work now