DST unions

Currently, unions cannot contain DSTs. This is the cause of MaybeUninit not supporting them. I propose to make this possible.

In general, the alignment/size of a union is the largest of the alignment/sizes of its fields. The pointer metadata of a DST union is the same as its DST field. This metadata is used to calculate the alignment/size of its DST field.

Extern types cannot have their size measured so unions of extern types are not able to do so either.

For trait object unions, it is sound to use any DynMetadata<dyn Foo>, not necessarily the vtable corresponding to the data inside the union. However, it is unsound to get a reference to the dyn Foo within the union unless the vtable is valid for the data inside the union.

Any feedback is appreciated.

1 Like

Since enums can be encoded as a struct with a tag and a union, and structs support DSTs in their last field, wouldn’t such a proposal also effectively introduce enums with unsized fields? (In which case they could also be introduced properly... well, maybe enums are more complicated due to things like niche optimization, so let’s not focus on them too much.)

What’s your take on unsized coercions? If I have a

union Either<Left: ?Sized, Right: ?Sized> {
    left: ManuallyDrop<Left>,
    right: ManuallyDrop<Right>,
}

can I turn Either<[bool; 10], i32> into Either<[bool], dyn Any>? (Probably not, how is Rust supposed to know which one of the vtables to include..)




For the use-case of MaybeUninit you’d only need support for a single T: ?Sized argument, right? I think things only become complicated once you want to support multiple different parameters. I guess, in general, there are union fields that are

  1. always Sized,
  2. always !Sized and their pointer metadata comes from some fixed type dyn Trait or [Type]
  3. potentially unsized, depending on a type parameter T, where T: ?Sized and the type of the pointer metadata comes directly from T; or
  4. potentially unsized, depending one or multiple parameters, but through some indirection (e.g. associated type of traits, etc.)

For structs, similar cases are possible for the last field, but the only really interesting/useful unsized case is just the 3rd one, since otherwise it’s hard to construct the struct anyways (without lots of unsafe code).

Focusing on this case then, we might restrict unsized unions to cases where where every field is either

  • always Sized

or

  • potentially using metadata depending directly from a type parameter T

with the additional restrictions that

  • all of the potentially unsized fields use the same type parameter T; and
  • the type parameter T is not used in any of the always sized fields.

This includes MaybeUninit. AFAICT, in these cases unions should then be able to support Unsize.


Going back to the cases 2. or 4. above; I haven’t thought much about 4., but regarding 2., one might also allow unions where all fields are either always sized or always unsized and all of the metadata types are the same. This kind of union could then, perhaps, be constructed by transmuting a (similarly structured) union that’s of the form described in the previous section. However it is debatable how valuable such unions are after all. For structs, the possibility to do something like creating a newtype around e.g. str is a good use-case; on the other hand a union like

union Foo {
    x: str,
    y: (u8, str),
}

could always be replaced by something like this using a struct:

union FooInner<T: ?Sized> {
    x: ManuallyDrop<T>,
    y: ManuallyDrop<(u8, T)>,
}
struct Foo {
    inner: FooInner<str>
}

Edit: Some more examples on the restrictions I tried to lay out above:

// allowed
union MaybeUninit<T: ?Sized> {
    uninit: (),
    value: ManuallyDrop<T>,
}
// allowed
union FooInner<T: ?Sized> {
    x: ManuallyDrop<T>,
    y: ManuallyDrop<(u8, T)>,
}
// not allowed
union Bar<T: ?Sized> {
    x: ManuallyDrop<Box<T>>,
    y: ManuallyDrop<T>,
}
// allowed
union Bar<S: ?Sized, T: ?Sized> {
    x: ManuallyDrop<Box<S>>,
    y: ManuallyDrop<T>,
}
// not allowed
union Bar<S: ?Sized, T: ?Sized> {
    x: ManuallyDrop<S>,
    y: ManuallyDrop<T>,
}

What's the reasoning behind this? If its used in a Sized field, then it won't affect pointer metadata, so there's not really a reason to ban it

The reasoning behind it is that this way unsized coercion can work for that type parameter T. You can't turn a & union { T, Box<T> } into & union { dyn Trait, Box<dyn Trait> } bcause of the T inside of the Box.

Sorry for the short answer I'm only on mobile.

Thinking about this, I probably need to a restriction in my list that would disallow multiple uses of T in the type of the same conditionally unsized field.

Couldn’t we store the v-table before the DST data? (ie. the layout of DST union would be a pointer + an unspecified amount of bytes for the data itself).

It’s probably even cleaner with enum, since the discriminant can be directly used to index a const array of v-table (assuming that the discriminant is a counter starting from 0).

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.