DST Receivers/Idempotent unsize


#15

Not if it’s already a trait object. &Trait + Markers (or even just &Trait) can be “re-unsized” to &Trait. The problematic case was &slice as &Trait.


#16

It the reason you can convert a trait object ‘Trait + Sync’ to ‘Trait’ is because the built-in marker traits have no dictionary?


#17

Yes (assuming that “dictionary” means vtable). IIRC, this could technically work with custom marker traits (as long as they don’t implement any methods) but rust doesn’t currently allow you to do this).


#18

Wow, yes, you can! Can someone explain what that means? Specifically, given

struct Wrapper<A: ?Sized + Trait>(A);

what’s the binary representation of Wrapper<Trait>? It doesn’t seem to be a wrapped trait object. What exactly does, e.g. Wrapper(1u32) as Wrapper<Trait> create/do?


List of things to be explained in the documentation?
#19

It is a trait object, of sorts. &Wrapper<Trait> is a fat pointer where the vtable points to i32's vtable for Debug.

#![feature(raw)]
use std::fmt::Debug;
use std::raw::TraitObject;

impl Trait for i32 {}
trait Trait {}
struct Struct<T: ?Sized>(T);

fn main() {
    unsafe {
        let value1 = Struct(0i32);
        let value2 = 0i32;
        let obj = &value1 as &Struct<Debug>;
        let obj2 = &value2 as &Debug;
        let raw1: TraitObject = std::mem::transmute(obj);
        let raw2: TraitObject = std::mem::transmute(obj2);
        assert_eq!(raw1.vtable, raw2.vtable);
    }
}

#20

Wow, I love this! It’s like conceptually fmapping trait-object-referencing into the struct. But how general is this? Does this work for any struct parameterized by T that allows T:?Sized? Presumably not: what if the struct contains two instances of T? Is this (and its constraints) documented somewhere?


#21

This means:

struct Wrapper<A>(A)

And thats about it :slight_smile: it also makes you include the generic bounds of “?Sized + Trait” on any function you pass it to. The representation is exactly the same. It is not a trait object it is just a plain instance of type A wrapped in a named tuple.


#22

This is the bit that I found a bit odd. &Debug is a trait object, and I kind of get &Wrapper<Debug>, but what is Wrapper<Debug> ?

Does that mean I can take any parametric type, and substitute traits for the type parameters, take a reference, and it becomes a trait object, I guess I find the mechanics of this a little magical and mysterious when compared with Haskell’s existential types where a type constraint (trait bound) is always syntacticly a type-class and never masquerades as a type. “forall a . (Debug a) => Ref (Wrapper a)” seems syntactically more clean to me. It also loses some identities, for example how do you write “forall a . (Debug a) => Ref (Wrapper a, Wrapper a)”?


#23

That wasn’t my question. I completely understand what struct Wrapper<A:Trait+?Sized>(A) means. The question was what Wrapper<Trait> means given that definition as you also asked in your follow-up:

As far as stebalien@ has described Wrapper<Trait> seems to be just a concept with no binary representation, whereas &Wrapper<Trait> has the binary representation of a trait object fat pointer.
Hence my follow-up question above:

which also relates to your follow-up questions. Still a mystery to me. I would love to understand precisely when &Wrapper<Trait> is allowed for the given definition of Wrapper. E.g. if we defined the Wrapper as a pair:

struct Wrapper<A: ?Sized + Trait>(A, A);

What would &Wrapper<Trait> mean? A tuple of trait objects? Actually, this definition doesn’t even compile (playground):

error: the trait `core::marker::Sized` is not implemented for the type `A` 

which is not particularly elucidating given that the non-pair version compiles. So there are clearly some constraints, but what precisely?


#24

That last one doesn’t compile because a struct can only have one dynamically sized field.


#25

Thanks! I neglected that fact. So this works, which I find even stranger in the case of &Wrapper2<Trait>.

trait Trait {}

impl Trait for i32 {}

struct Wrapper1<A: ?Sized + Trait>(A);
struct Wrapper2<A: ?Sized + Trait>(i32, A);

fn main() {
    println!("{:?}", std::mem::size_of::<&Trait>()); // => 16

    let sized = Wrapper1(0i32);
    let _ = &sized as &Wrapper1<Trait>;
    println!("{:?}", std::mem::size_of::<&Wrapper1<Trait>>()); // => 16

    let sized = Wrapper2(0, 0i32);
    let _ = &sized as &Wrapper2<Trait>;
    println!("{:?}", std::mem::size_of::<&Wrapper2<Trait>>()); // => 16
}

(playground)

So &Wrapper2<Trait> has the same representation as &Trait despite the extra tuple field that Wrapper2 contains.

This is really surprising and I don’t understand why this was added to Rust. I mean, what’s the point of adding the non-reifiable concept of Wrapper<Trait> and a strangely reified &Wrapper<Trait> when one can simply use Wrapper<&Trait> which makes complete sense based on other Rust concepts? In the case where Wrapper<Trait> seems least surprising (the case of a named 1-tuple), &Wrapper1<Trait> has the same binary representation as Wrapper1<&Trait>.

Any insights as to why Wrapper<Trait> (and the &Wrapper<T> as &Wrapper<Trait> conversion) was introduced?


#26

I don’t think they have the same representation, I think you are measuring the size of the reference (fat pointer).

I think what is happening is for Wrapper a trait object is being constructed such that:

&Trait = forall t . (Trait t) => Ref t

&Wrapper = forall t . (Trait t) => Ref (Wrapper t)

where Ref is a special type constructor unique for each set of trait object bounds. So the fat pointer for both has a pointer to the “Trait” dictionary/vtable and the data which in one case is just the value of type "t’ and in the other is the value wrapped with the value-constructors, and in the case of Wrapper2, a tuple with an Int and the value of type “t”.


#27

My point was that the representation for both is a fat pointer, which wasn’t particularly obvious for &Wrapper<Trait> since nothing like this is defined anywhere. As for whether the representations are identical, I explored this and discovered what I believe a bug in the compiler:

match sized2 {
    Wrapper2(_, ref num) => {
        let unwrapped = &*num as &Trait;
        let obj: TraitObject = unsafe { std::mem::transmute(unwrapped) };
        println!("unwrapped: data: {:?}  vtable: {:?}", obj.data, obj.vtable);
        println!("  wrapped: foo() = {:?}", unwrapped.foo());
    }
}

let obj: TraitObject = unsafe { std::mem::transmute(wrapped2) };
println!("  wrapped: data: {:?}  vtable: {:?}", obj.data, obj.vtable);

// SEGFAULTS!
// match wrapped2 {
//     &Wrapper2(_, ref num) =>
//         println!("  wrapped: foo() = {:?}", (num as &Trait).foo())
// }

(playground)

So, the output is

16
16
unwrapped: data: 0x7fff3fea10a4  vtable: 0x7f1bdbfc88c0
  wrapped: foo() = 42
  wrapped: data: 0x7fff3fea10a0  vtable: 0x7f1bdbfc88c0

showing that there is clearly an offset in the Wrapped version, but they point to the same vtable. However, I am not able to call the foo method without segfaulting (bug).

@stebalien Since you are already using this concept in your code, could you possibly enlighten me as to how you invoke trait methods on &Wrapped<Trait>?


#28

Also, interestingly, the size seems kinda off when the wrapper contains T only as PhantomData:

trait Trait {}

impl Trait for i32 {}

struct Wrapper2<A: ?Sized + Trait>(i32, A);
struct Wrapper3<A: ?Sized + Trait>(i32, i32, i32, i32, std::marker::PhantomData<A>);

fn main() {
    println!("{:?}", std::mem::size_of::<&Trait>()); // => 16

    let sized2 = Wrapper2(0, 42i32);
    let _ = &sized2 as &Wrapper2<Trait>;
    println!("{:?}", std::mem::size_of::<&Wrapper2<Trait>>()); // => 16

    let _sized3 = Wrapper3(0, 0, 0, 0, std::marker::PhantomData::<i32>);
    // error: non-scalar cast: `&Wrapper3<i32>` as `&Wrapper3<Trait>`
    //let _ = &_sized3 as &Wrapper3<Trait>;
    println!("{:?}", std::mem::size_of::<&Wrapper3<Trait>>()); // => 8
}

(playground)

Specifically, std::mem::size_of::<&Wrapper3<Trait>>() gives 8. But it gives a confusing error, preventing me from creating a &Wrapper3<Trait> object to begin with. So the fundamental question remains: when precisely is &t as &Wrapped<Trait> technically allowed? Only for named tuples where the type-parametrized item is at the end of the tuple?


#29

You don’t invoke trait methods on &Wrapped<Trait> but on the contained trait object:

#![feature(unsize)]

use std::marker::Unsize;

trait WhatAmI {
    fn whatami(&self);
}

impl WhatAmI for i32 {
    fn whatami(&self) {
        println!("I am an i32");
    }
}

struct Wrapper<T: ?Sized + WhatAmI>(T);

impl<T: ?Sized + WhatAmI> Wrapper<T> {
    fn whatihold<'a>(&'a self)
        where Self: Unsize<Wrapper<WhatAmI + 'a>>
    {
        self.0.whatami();
        // Or, to be explicit.
        let inner: &WhatAmI = &(self as &Wrapper<WhatAmI>).0;
        inner.whatami();
    }
}
fn main() {
    Wrapper(0).whatihold();
    (&Wrapper(0) as &Wrapper<WhatAmI>).whatihold();
}

#30

That’s precisely what I was trying to do with the following which segfaulted.

But yeah, I totally overlooked that I could do the more straightforward

println!("  wrapped: foo() = {:?}", wrapped2.1.foo())

Thanks for the explanation! The mechanics of the whole Wrapped<Trait> makes more sense to me now and I can see how it might be implemented underneath (modulo the wording of the error for &Wrapped3<Trait> I mentioned above. This feature probably ought to be documented somewhere. Summon @steveklabnik? :wink:

Now that you’ve brought me up to speed, I have a question on your proposal. Specifically, regarding

is it possible to have an unsized T satisfying these constraints where T is not Trait and Wrapper<T> is valid? A few attempts of mine failed since either (1) I end up getting a sized type or (2) for SubTrait: Trait, &Subtrait is not castable to &Trait to begin with. If there isn’t (and there’s no danger of it being added in the future), then your proposal seems to make sense.


#31

impl Trait for str?


#32

What @Dr_Emann said, or


#33

Thanks, I should have re-read the thread after things clicked for me. Indeed, the unstable Unsize marker trait you discovered does what you want. Perhaps From should be implemented for it, so that you could just do View(self.into()).


Specifying that trait should be object-safe for purpose of casting to object
#34

Actually, for some reason adding the Self: Unsize<Trait> clause removes the need for the cast. That is, View(self) just works™.