Skip to content

Conversation

@frank-king
Copy link

@frank-king frank-king commented Dec 22, 2025

This RFC introduces a Thin wrapper which thinifies a fat pointer of a DST by inlining its metadata.

Rendered.

Pre-RFC on Rust Internal Forum.

Important

When responding to RFCs, try to use inline review comments (it is possible to leave an inline review comment for the entire file at the top) instead of direct comments for normal comments and keep normal comments for procedural matters like starting FCPs.

This keeps the discussion more organized.


Now they can be rewritten as:
- `List<T>` -> `&Thin<[T]>`
- `ThinVec<T>`, technically `Box<(usize, Thin<[MaybeUninit<T>]>)>` (in representation)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is unfortunate that we can't fit Vec into this representation, but I feel like custom DSTs could eventually make it work with Thin, so, I'm not too upset about it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean making std::vec::Vec<T> a thin pointer? What's the advantage compared to the current representation?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, specifically the fact that there's no way to represent a partially initialised slice like a Vec in a generic way. It would be cool if Vec<T> were Box<PartiallyInitSlice<T>> and ThinVec<T> were Box<Thin<PartiallyInitSlice<T>>.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I understand the PartiallyInitSlice<T> as a [MaybeUninit<T>] with an extra metadata inited_len: uszie indicating the number of initialized elements in this slice? Sounds cool!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that requires DST of mutable metadata, or it is not possible to implement a push method for an &mut PartiallyInitSlice<T>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, under this situation, you'd still need to be implementing stuff for &mut Box directly, but at least the types are compatible with Thin.

I don't treat any of this as a set-in-stone proposal; my main point is that Thin not working for Vec now doesn't mean it can't necessarily work in the future, just, it would require extra stuff we don't have yet. And so, it not working for Vec shouldn't mean Thin as-is needs any particular changes to account for that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably I can add this point to the "Future possibility" section?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, feel free to reword any amount of what I said and add it to the RFC!

I think that maybe making the capacity of a vec part of the metadata could be a useful step toward allowing some kind of ArrayVec solution in libstd, or at least making array::IntoIter less redundant, but there are a lot of different paths and we're not sure what path we'll take. But in terms of generalising ThinVec, it seems promising.

@jieyouxu jieyouxu added T-lang Relevant to the language team, which will review and decide on the RFC. T-types Relevant to the types team, which will review and decide on the RFC. labels Dec 23, 2025
}

// The size is known via reading its metadata.
impl<U: Pointee> ValueSized for Thin<U> {}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this requires U: MetaSized: there are !MetaSized types where you can't work out their size. You plausibly could get away with U: ValueSized but it would have weird/surprising semantics - I think it would ignore the metadata and call the value's size function.

```

For `dyn Trait + ValueSized` types, the `MetadataSize` entry of the common vtable entries
will be a method of `fn size(&self) -> usize` which computes the value's size at runtime,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's more like fn size(*const ()), this also makes me nervous, this single choice feels like it probably implies a bunch of semantics but I can't quite work out what those semantics are. For example, the same change should probably be done for alignment too, because we probably don't want ValueSized: const Sized.

T: Unsize<U>,
{
Self {
metadata: ptr::metadata(&value as &U),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this puts constraints on what metadata can be, ptr::metadata(&value as &U) has to always return the same value, so it has to be pure, not depend on address etc. This is plausibly fine but definitely needs to be documented.

pub trait MetaSized: ValueSized {}
```

For `dyn Trait + ValueSized` types, the `MetadataSize` entry of the common vtable entries
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you adding these types? No existing code will be able to accept them (because they're ValueSized), also they're annoyingly incompatible with dyn Trait because converting from dyn Trait to dyn Trait + ValueSized requires replacing the VTable with a new one.

Copy link
Contributor

@zachs18 zachs18 Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

converting from dyn Trait to dyn Trait + ValueSized requires replacing the VTable with a new one.

I think we could make dyn Trait and dyn Trait + ValueSized (and a hypothetical dyn Trait + PointeeSized) be able to share a vtable by encoding whether the concrete type is Sized or not in the alignment field of the metadata, since the alignment must always be a positive power of two for a Sized type.

Specifically:

// expository
struct Vtable {
  drop_in_place: nullable fn ptr,
  align: usize,
  size: union<usize, fn ptr>,
}

(I'm using Thin here as the current unstable meaning in the stdlib of Pointee<Metadata = ()>, not the Thin type proposed in this RFC)

  1. align > 0: the concrete type is Thin + Sized, the align field is the alignment and the size field is the size.
  2. align == 0 and size != 0/null: the concrete type is Thin + !Sized + ValueSized, the size field is a (non-null) function pointer to fn(*comst _) -> Layout (roughly)
  3. align == 0 and size == 0/null: the concrete type is Thin + !ValueSized + PointeeSized: not actually proposed in this RFC, but would allow unsizing extern types to dyn Trait + PointeeSized.

This shouldn't regress performance for normal dyn Trait (i.e. dyn Trait + MetaSized), since the compiler can assume that only the first case needs to be handled.

For dyn Trait + ValueSized, the compiler will check the align field of the vtable to know whether the layout is given inline, or requires calling the function pointer. As a future possibility, there could be a fallible conversion from pointers to dyn Trait + ValueSized to dyn Trait which succeeds if the vtable is in the first class. (Analogous for dyn Trait + PointeeSized).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, how about an (niche-optimized) enum? (Which has better readability)

enum LayoutEntry {
    PointeeSized,
    ValueSized(fn (*const ()) -> Layout),
    MetaSized(Layout),
}

Then

struct VtableHeader {
    drop_in_place: unsafe fn(*mut ()),
    layout_entry: LayoutEntry,
}

- `List<T>` -> `&Thin<[T]>`
- `ThinVec<T>`, technically `Box<(usize, Thin<[MaybeUninit<T>]>)>` (in representation)
- `ThinBox<T>` -> `Box<Thin<T>>`
- `BoxedTrait` -> `Box<Thin<dyn Trait>>`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love the idea of getting a fully functional ThinBox, as the current version is so functionality-starved.

Do I understand correctly that this would also "automatically" give thin versions of Rc and Arc?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand correctly that this would also "automatically" give thin versions of Rc and Arc?

I don't think so, at least not without additional guarantees or restrictions. In order for Weak<T> to work, Arc<T> needs to be able to get the size/align of a T even after it has been dropped (which is possible for T: MetaSized since it only requires the pointer metadata), so we'd have to also require that for ValueSized types, or special-case Thin<_> somehow. Or maybe we could restrict Weak<T> to T: MetaSized, so only Arc<T>/Rc<T> needs to handle T: ValueSized which I think should be feasible (just get the layout before dropping, if we are in a codepath that could deallocate).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Weak also works for Thin<T> where T: MetaSized because the drop for the inlined metadata is a no-op and remains unchanged after Thin<T> being dropped. That metadata can still be readable, though it may need extra modification with the opsem, i.e. a value can be partially initialized after deinitialization where some of its fields are trivially destructible.

This RFC adds `Thin<T>` that wraps `T`'s metadata inline, which makes `Thin<T>` thin even if
`T` is `!Sized`.

# Motivation

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are under-selling the RFC with the current list of motivations :)

Thin pointers -- overall -- are useful for:

  1. Reduced memory consumption, when there's many copies of the pointer, which happens with Rc and Arc.
  2. Reduced cache consumption, which is particularly useful when the pointer points to cold data.
  3. Atomic operations: much easier to atomically modify a thin pointer than a fat one.

Unfortunately, the current standard library only offers scarce support for thin pointers. There's only ThinBox, no ThinRc or ThinArc, and even then... ThinBox is sorely trailing Box in terms of functionality.

The ability to have one Thin wrapper and automatically get both a full-featured ThinBox and full-featured ThinRc and ThinArc would really make thin pointers a LOT more approachable and flexible.

(Ie, my own attempt at getting all those goodies in endor-thin is long-winded, and riddled with unsafe code which I hope I got correct)

Now they can be rewritten as:
- `List<T>` -> `&Thin<[T]>`
- `ThinVec<T>`, technically `Box<(usize, Thin<[MaybeUninit<T>]>)>` (in representation)
- `ThinBox<T>` -> `Box<Thin<T>>`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I perused the code of ThinBox when re-implementing an equivalent, and noted a fairly clever optimization: when the data portion is ZST, the library const-allocates the associated metadata and simply sets the pointer in Box to point to this "static" element.

(see ThinBox::new_unsize_zst)

Do you believe it would be possible to implement the same trick with Box<Thin<T>>?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think doing so would require clawing back one of Box's current guarantees, that (for non-zero-sized pointees) a Box owns an allocation from the global allocator with the correct layout (source). If we make Box::<Thin<[u8]>>::bikeshed_new_unsize_zst([]) point to a "static" allocation, then this guarantee is no longer true (or at least needs to be restricted to T: MetaSized)

Personally, prefer the current guarantee for Box

However, I think something like this could work for Arc, like how impl Default for Arc<[T]> shares a common empty slice ArcInner. Arc::<Thin<_>>::new_unsize_zst could work basically just like how ThinBox::new_unsize_zst works, but the const allocation would be an ArcInner<Thin<_>> that starts with a nonzero refcount, so the code will never attempt to deallocate it (assuming no-one incurs UB by calling Arc::decrement_..._count too many times).

Comment on lines +95 to +101
## Passing DST pointers across the FFI boundaries
```rust
extern "C" fn foo(
str_slice: &Thin<str>, // ok because `&Thin<str>` is thin
int_slice: &Thin<[i32]>, // ok because `&Thin<[i32]>` is thin
opaque_obj: &Thin<dyn std::any::Any>, // ok because `&Thin<dyn std::any::Any>` is thin
} { /* ... */ }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to guarantee anything about the layout that would allow accessing the data on the other side of the FFI boundary?

In theory, &Thin/*const Thin could be made to point at the data field rather then the metadata, so you could pass &Thin<[u8]> directly and write the bytes in C without knowing the metadata size. I'm not sure that's worth the complexity over a .data_ptr() method, however.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-lang Relevant to the language team, which will review and decide on the RFC. T-types Relevant to the types team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants