Understanding lifetimes of generic Fn trait objects

I am under the impression that rust uses the term "subtype" very carefully. When we say that A is a subtype of B, it means that any value of type A is also of type B. I.e. all capabilities of B are also capabilities of type A.

But it's not a more specific type. It's a different type. You could impl Iterator<Item=i32> for one and Iterator<Item=bool> for the other.

That's why I call it a coercion... but perhaps this type of coercion is allowed in more places than most? (I guess it kind of follows the rules of variance, rather than needing to be the outermost type constructor? Kind of like... subtyping? Aaaahhhh!!!!!)

...now I'm mentally running around in circles trying to figure out what to call things again!

1 Like

I mean filing it as a new issue at Issues · rust-lang/rust · GitHub. Personally, I'd love for someone in the know to indicate whether what's discussed in this thread is a quirk of today's implementation, something that will be (potentially) changed in the future, a bug, or something else entirely :slight_smile:.

If I put a layman's hat on, I would expect your original code to work, particularly given the 'static object bound. I suspect @ExpHP's explanation is close to the truth of why it doesn't work, but then I'm curious if that's deliberate/intentional or just falls out from how (in)variance in traits is handled.

It's still not using HRTBs there. This is evidenced by what happens if you try to call it twice:

type BoxFn<I, O> = *mut(dyn Fn(I) -> O + 'static);

fn bare(x: &i32) -> i32 {
    2 * *x
}

pub fn use_generic_ptr() -> i32 {
    let generic: BoxFn<&i32, i32> = Box::into_raw(Box::new(bare));
    
    let z = {
        let f = unsafe{Box::from_raw(generic)};
        let x = 42;
        let r = f(&x);
        Box::leak(f);
        r
    };
    let z = {
        let f = unsafe{Box::from_raw(generic)};
        let x = 42;
        let r = f(&x);
        Box::leak(f);
        r
    };
    z
}
error[E0597]: `x` does not live long enough
  --> src/lib.rs:13:19
   |
13 |         let r = f(&x);
   |                   ^^ borrowed value does not live long enough
...
16 |     };
   |     - `x` dropped here while still borrowed
17 |     let z = {
18 |         let f = unsafe{Box::from_raw(generic)};

I haven't thought too hard about in what ways or why you might be seeing different results here versus what you've already been trying before.

I've always looked at subtyping in Rust as "substitutability" - if A is a subtype of B and something expects B, I can give it A - I suppose thinking in terms of capabilities is throwing a similar light onto it. As you know, normally the variance/subtyping stuff is really seen only in the context of lifetimes (i.e. &'static i32 is a subtype of &'a i32 or SomeVariantStruct<'static> is a subtype of SomeVariantStruct<'a>). I do agree this one seems a bit different, and I also find it surprising that you can impl a trait for them like:

trait Foo {}

impl<'a, I, O> Foo for Box<dyn Fn(&'a I) -> O> {}
impl<I, O> Foo for Box<dyn for<'a> Fn(&'a I) -> O> {}

I'm not sure how to reconcile that, and is part of the reason I'm suggesting @asomers creates a github issue :slight_smile:. Hopefully this will go some way towards enhancing the language reference around these points cause otherwise ... good luck to anyone wanting to create a new Rust frontend.

1 Like

Ok, now this is getting weird. The error message you got when you tried using the pointer twice doesn't make much sense. But it makes even less sense if I clone the pointer and use the clone only once:

type BoxFn<I, O> = *mut(dyn Fn(I) -> O + 'static);

fn bare(x: &i32) -> i32 {
    2 * *x
}

pub fn use_generic_ptr() -> i32 {
    let generic: BoxFn<&i32, i32> = Box::into_raw(Box::new(bare));
    let generic2 = generic.clone();
    let _z = {
        let f = unsafe{Box::from_raw(generic)};
        let x = 42;
        let r = f(&x);
        Box::leak(f);
        r
    };
    let z = {
        let f = unsafe{Box::from_raw(generic2)};
        let x = 42;
        let r = f(&x);
        Box::leak(f);
        r
    };
    z
}
   Compiling playground v0.0.1 (/playground)
error[E0597]: `x` does not live long enough
  --> src/lib.rs:13:19
   |
13 |         let r = f(&x);
   |                   ^^ borrowed value does not live long enough
...
16 |     };
   |     - `x` dropped here while still borrowed
17 |     let z = {
18 |         let f = unsafe{Box::from_raw(generic2)};
   |                                      -------- borrow later used here

error: aborting due to previous error

The compiler complains that generic2 is borrowed "later", even though it's only borrowed once! And if I replace BoxFn with the plain pointer type, then everything works. That type alias is definitely removing information of some kind.

There might be two separate issues here:

  1. The specific Fn(&'a I) -> O is different than the HRTB for<'a> Fn(&'a I) -> O.
  2. The type alias is removing some kind of lifetime information.

Are you suggesting I create a Rust issue for both of those?

Agreed, there are several questions here. At its core, for me, is that I (and likely many others) would expect your original code to work as-is because of the 'static bound. So in some sense, the github issue would be a restatement of your first post here :slight_smile:. Of course, we’ve now discussed it some and so linking to this discussion from there would be helpful.

As for type aliases, I believe there’re known issues/limitations with how the compiler does region inference when closures are in play, so I can see how putting slight type “indirection” shakes the apple cart a bit too much.

The subtyping vs different types (in terms of allowing trait impls) for HRTB vs not is also interesting, and perhaps deserves its own issue, but I’d just mention it in the same issue as for above for now.

1 Like

I have a new theory: that this is a dropck bug. The error message, after all, is the regular dropck error message. But Box::drop does use may_dangle, so it shouldn't be a problem. However, it appears that the may_dangle isn't working when invoked through the type alias. But before I file a bug about may_dangle, I need to understand why I can't get it to work even when I use it directly. The linked code contains a new struct BoxFnStruct that wraps Fn and manually implements Drop with may_dangle. Yet it doesn't compile. Do either of you know why?