From c4195bdbf1fb379d63e0bef7111d9f1f79a30987 Mon Sep 17 00:00:00 2001 From: Frank King Date: Mon, 22 Dec 2025 17:50:17 +0800 Subject: [PATCH 1/7] Thin pointers with inline metadata --- ...3895-thin-pointers-with-inline-metadata.md | 334 ++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 text/3895-thin-pointers-with-inline-metadata.md diff --git a/text/3895-thin-pointers-with-inline-metadata.md b/text/3895-thin-pointers-with-inline-metadata.md new file mode 100644 index 00000000000..5767f38f850 --- /dev/null +++ b/text/3895-thin-pointers-with-inline-metadata.md @@ -0,0 +1,334 @@ +- Feature Name: (`thin_pointers`) +- Start Date: 2025-12-16 +- RFC PR: [rust-lang/rfcs#3894](https://github.com/rust-lang/rfcs/pull/3894) +- Rust Issue: + [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) + +# Summary +[summary]: #summary + +This RFC adds `Thin` that wraps `T`'s metadata inline, which makes `Thin` thin even if +`T` is `!Sized`. + +# Motivation +[motivation]: #motivation + +Pointers of dynamically sized types (DSTs) are fat pointers, and they are not FFI-compatible, +which obstables some common types like `&str`, `&[T]`, and `&dyn Trait` from being passed across +the FFI boundaries. + +## 1. Passing pointers of DSTs accross FFI-boundary is hard +Currently, it's difficult to using DSTs in FFI-compatible functions (even by-pointer). +For example, it is not allowed to use `&str`, `&[T]`, or `&dyn Trait` types in an +`extern "C"` function. +```rust +extern "C" fn foo( + str_slice: &str, //~ ERROR not FFI-compatible + int_slice: &[i32], //~ ERROR not FFI-compatible + opaque_obj: &dyn std::any::Any, //~ ERROR not FFI-compatible +) { /* ... */ } +``` + +Instead, users have to wrap these types in `#[repr(C)]` structs: +```rust +/// FFI-compatible wrapper struct of `&[T]` +#[repr(C)] +pub struct Slice<'a, T> { + len: usize, + ptr: NonNull<()>, + _marker: PhantomData<&'a [T]>, +} + +/// FFI-compatible wrapper struct of `&str` +#[repr(C)] +pub struct StrSlice<'a> { + len: usize, + bytes: NonNull, + _marker: PhantomData<&'a str>, +} + +/// FFI-compatible wrapper of `&dyn Trait` +#[repr(C)] +pub struct DynTrait<'a> { + vtable: NonNull<()>, + ptr: NonNull<()>, + _marker: PhantomData<&'a dyn Trait>, +} +``` + +Luckily, the [`abi_stable`] crate provides a series of FFI-compatible types like [`RSlice<'a, T>`], +[`RSliceMut`], [`RStr<'a>`], and an attribute macro [`sabi_trait`] that makes ABI-stable trait +objects (which are also FFI-compatible). + +However, that is tedious and and non-exhaustive because the library writer cannot enumerate all +compound DSTs (e.g. ADTs with a DST field) exhaustively. + +[`abi_stable`]: https://crates.io/crates/abi_stable +[`RSlice<'a, T>`]: https://docs.rs/abi_stable/latest/abi_stable/std_types/struct.RSlice.html +[`RSliceMut`]: https://docs.rs/abi_stable/latest/abi_stable/std_types/struct.RSliceMut.html +[`RStr<'a>`]: https://docs.rs/abi_stable/latest/abi_stable/std_types/struct.RStr.html +[`sabi_trait`]: https://docs.rs/abi_stable/latest/abi_stable/attr.sabi_trait.html + +## 2. Slices cannot be unsized to trait objects + +Suppose there is a `dyn`-safe trait `MyTrait`, and it is implemented for `[T]`. However, it is +not possible to convert an `&[T]` to an `&dyn MyTrait` because `[T]` doesn't impl `Sized`. +```rust +trait MyTrait { + fn foo(&self); +} + +impl MyTrait for [T] { + fn foo(&self) { /* ... */ } +} + +fn as_my_trait(x: &[T]) -> &dyn MyTrait { + x //~ ERROR the size for values of type `[T]` cannot be known at compilation time +} +``` + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation +To overcome the obstacles above, we introduce a `Thin` wrapper that stores the metadata and +a (sized) value inside and thus keeps pointers of `Thin` thin. + +## Passing DST pointers accross the FFI boundraries +```rust +extern "C" fn foo( + str_slice: &Thin, // ok because `&Thin` is thin + int_slice: &Thin<[i32]>, // ok because `&Thin<[i32]>` is thin + opaque_obj: &Thin, // ok because `&Thin` is thin +} { /* ... */ } + +// Construct the values of DSTs on stack +let str_slice: &Thin = thin_str!("something"); +let int_slice: &Thin<[i32]> = &Thin::new_unsized([1, 2, 3]); +let opaque_obj: &Thin = &Thin::new_unsized(String::from("hello")); +// Pass the thin DSTs accross FFI boundary +unsafe { + foo(str_slice, int_slice, opaque_obj); +} +``` +## Making trait objects of slices +```rust +trait MyTrait { + fn foo(&self); +} + +impl MyTrait for Thin<[T]> { + fn foo(&self) { /* ... */ } +} + +// Construct a thin `Thin<[i32]>` on stack +let value: &Thin<[i32]> = &Thin::new_unsized([1, 2, 3]); +// Coerce it to a trait object +// where `+ ValueSized` is needed to indicate that the size of this trait object +// are calculated from its value. +let dyn_value: &dyn MyTrait + ValueSized = value; // ok because `Thin<[i32]>` is thin +// Calls ` as dyn MyTrait>::foo` +dyn_value.foo(); +``` +## Unify normal and thin containers +Given that: +* [`List`] in rustc that is a thin `[T]` with the metadata (length) on the head; +* [`ThinVec`] that put the length and capacity components together with its contents on the heap; +* [`ThinBox`] like `Box` but put the metadata together on the heap; +* [`thin_trait_object`], an attribute macro that makes a thin trait object (by manually + constructing the vtable). + +Now they can be rewritten as: +- `List` -> `&Thin<[T]>` +- `ThinVec`, technically `Box<(usize, Thin<[MaybeUninit]>)>` (in representation) +- `ThinBox` -> `Box>` +- `BoxedTrait` -> `Box>` + +where much less boilerplate codes are needed. + +[`List`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/type.List.html +[`ThinVec`]: https://docs.rs/thin-vec/latest/thin_vec/struct.ThinVec.html +[`ThinBox`]: https://doc.rust-lang.org/std/boxed/struct.ThinBox.html +[`thin_trait_object`]: https://docs.rs/thin_trait_object/latest/thin_trait_object/attr.thin_trait_object.html + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation +## Add `ValueSized` to the sized hierarchy + +Regarding [sized hierarchy], `Thin` is more than `PointeeSized` but not `MetaSized`: +- it is not `MetaSized` because the metadata is not carried by the pointer itself; +- it is more than `PointeeSized` because we actually know its size by reading the metadata + stored inside. + +We need to add a new stuff to the sized hierarchy, named `ValueSized`, to indicate a value of +which the size is known by reading its value, as mentioned in [RFC 3729 (comments)]. + +```rust +// mod core::marker; + +/// Indicates that a type's size is known from reading its value. +/// +/// Different from `MetaSized`, this requires pointer dereferences. +#[lang_item = "value_sized"] +pub trait ValueSized: PointeeSized {} + +// Change the bound of `MetaSized: PointeeSized` to `MetaSized: ValueSized` +#[lang_item = "meta_sized"] +pub trait MetaSized: ValueSized {} +``` + +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, +instead of a compile-time constant value. + + +[sized hierarchy]: https://github.com/rust-lang/rust/issues/144404 +[RFC 3729 (comments)]: https://github.com/davidtwco/rfcs/blob/sized-hierarchy/text/3729-sized-hierarchy.md#user-content-fn-9-ebb4bca70c758b473dca6f49c3bb3cbc + +## Public APIs + +The public APIs of `Thin` consist of 2 parts: +- `Thin`, which is a (maybe unsized) value of `T` with the metadata type of `U` carried on. + Typically, `U = T` or `U` is some type that `T: Unsize`. +- `EraseMetadata` which is a wrapper of (maybe unsized) `T`, which ignores the metadata of `T`. + E.g., both `&ErasedMetadata` and `&ErasedMetadata<[u8]>` has the same size as a + thin pointer `&()`. +```rust +// mod core::thin; + +/// Wrapping a DST `T` with its metadata inlined, +/// then the pointers of `Thin` are thin. +/// +/// the generic type `U` is for two-stage construction of +/// `Thin`, i.e., `Thin where T: Unsize` must be +/// constructed first, then coerced (unsized) to `Thin` +/// (aka `Thin`) +#[repr(C)] +pub struct Thin { + metadata: U::Metadata, + data: EraseMetadata, +} + +// The size is known via reading its metadata. +impl ValueSized for Thin {} + +/// A wrapper that ignores the metadata of a type. +#[lang = "erase_metadata"] +#[repr(transparent)] +pub struct EraseMetadata(T); + +// The size is unknown because the metadata is erased. +impl PointeeSized for EraseMetadata {} + +// Value accesses +impl ops::Deref for Thin { + type Target = U; + fn deref(&self) -> &U; +} +impl ops::DerefMut for Thin { + fn deref_mut(&mut self) -> &mut U; +} +``` + +## Value constructions +For a sized typed `Thin`, it is able to construct with `Thin::::new`. +For an unsized (`MetaSized`) typed `Thin`, in general, it requires 3 steps to construct +a `Thin` on stack or on heap: +- construct a sized value of `Thin` via `Thin::::new_unsized` (where `T: Unsize`). +- obtain a pointer (i.e., `&`, `&mut`, `Box`, `Rc`, `Arc`, etc.) of `Thin` via + their constructors. +- coerce the pointer of `Thin` to the pointer of `Thin`. + +Here are the APIs related to value constructions mentioned above: +```rust +impl Thin { + /// Create a sized `Thin` value, which is a simple wrapper of `T` + pub fn new(value: T) -> Thin { + Self { + metadata: (), // Sized type `T` has an empty metadata + data: ErasedMetadata(value), + } + } +} + +impl Thin { + /// Create a sized `Thin` value with metadata of unsized type `U`, + /// which can be coerced (unsized) to `Thin` + pub fn new_unsized(value: T) -> Self + where + T: Unsize, + { + Self { + metadata: ptr::metadata(&value as &U), + data: ErasedMetadata(value), + } + } + /// Consume the `Thin` and return the inner wrapped value of `T` + pub fn into_inner(self) -> T { + self.data.0 + } +} + +/// `Thin` has the same layout as `Thin`, so that it can be coerced +/// (unsized) to `Thin` +impl Unsize> for Thin +where + T: Unsize +{} +``` + +# Drawbacks +[drawbacks]: #drawbacks + +## `Thin` is confusing with existing concepts +The term `Thin` has a different meaning with a previous term: the trait `core::ptr::Thin` +(that means types with `Metadata = ()`). + +## `ValueSized` doesn't fit `Thin` 100% +Generally, the size of `ValueSized` type is calculated from its value. For instance, the size of +a *real* C string (Not `core::ffi::CStr`) is determined by counting the bytes until a `\0` +is reached. In general, `ValueSized` types are `Freeze`, or else the size can change after +calculating from its value. Hence, `CStrV2` (the real C string type) is `ValueSized` but +`UnsafeCell` is not. + +However, for `Thin`, we are definitely sure that `::Metadata` is +`Freeze`, then even if `T: !Freeze`, `Thin: ValueSized` still holds. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +Don't introduce `Thin`, but use the extern type instead, then `Thin` can be represented by: +```rust +pub struct Thin { + metadata: U::Metadata, + marker: PhantomData, + extern_type: ThinExtra, +} + +extern "C" { + type ThinExtra; +} +``` +However, it is hard to construct values on stack. + +# Prior art +[prior-art]: #prior-art + +- [RFC 2594: Custom DSTs] introduced a more general way to define a DST. This RFC + focus on how to "thinify" a pointer to an existing DST via inlining the metadata, + which is a convenient helper for some common situations. +- [RFC 3536: Trait for `!Sized` thin pointers] was very similar to this RFC. + This RFC doesn't change the semantics of existing DSTs like `[T]`, which can avoid + potential breaking changes of composed DSTs. + +[RFC 2594: Custom DSTs]: https://github.com/rust-lang/rfcs/pull/2594 +[RFC 3536: Trait for `!Sized` thin pointers]: https://github.com/rust-lang/rfcs/pull/3536 + +# Unresolved Questions +[unresolved-questions]: #unresolved-questions + +- Should the trait `ValueSized` provide a user-implementable method `size`? (In this RFC, + such `size` method can only be generated by compiler.) + +## Future possibilities +[future-possibilities]: #future-possibilities + +None yet. \ No newline at end of file From 0146ef0a3b3f9a2654c60586384f8aad0bf167b9 Mon Sep 17 00:00:00 2001 From: Frank King Date: Mon, 22 Dec 2025 19:25:41 +0800 Subject: [PATCH 2/7] Update the RFC number --- ...e-metadata.md => 3898-thin-pointers-with-inline-metadata.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename text/{3895-thin-pointers-with-inline-metadata.md => 3898-thin-pointers-with-inline-metadata.md} (99%) diff --git a/text/3895-thin-pointers-with-inline-metadata.md b/text/3898-thin-pointers-with-inline-metadata.md similarity index 99% rename from text/3895-thin-pointers-with-inline-metadata.md rename to text/3898-thin-pointers-with-inline-metadata.md index 5767f38f850..95b0d92a37c 100644 --- a/text/3895-thin-pointers-with-inline-metadata.md +++ b/text/3898-thin-pointers-with-inline-metadata.md @@ -1,6 +1,6 @@ - Feature Name: (`thin_pointers`) - Start Date: 2025-12-16 -- RFC PR: [rust-lang/rfcs#3894](https://github.com/rust-lang/rfcs/pull/3894) +- RFC PR: [rust-lang/rfcs#3898](https://github.com/rust-lang/rfcs/pull/3898) - Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) From 176545bdb23495e04d03e7151d8343f495f9b53e Mon Sep 17 00:00:00 2001 From: Frank King Date: Mon, 22 Dec 2025 20:28:41 +0800 Subject: [PATCH 3/7] Fix typos --- ...3898-thin-pointers-with-inline-metadata.md | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/text/3898-thin-pointers-with-inline-metadata.md b/text/3898-thin-pointers-with-inline-metadata.md index 95b0d92a37c..645fd63fedb 100644 --- a/text/3898-thin-pointers-with-inline-metadata.md +++ b/text/3898-thin-pointers-with-inline-metadata.md @@ -14,11 +14,11 @@ This RFC adds `Thin` that wraps `T`'s metadata inline, which makes `Thin` [motivation]: #motivation Pointers of dynamically sized types (DSTs) are fat pointers, and they are not FFI-compatible, -which obstables some common types like `&str`, `&[T]`, and `&dyn Trait` from being passed across +which prevents some common types like `&str`, `&[T]`, and `&dyn Trait` from being passed across the FFI boundaries. -## 1. Passing pointers of DSTs accross FFI-boundary is hard -Currently, it's difficult to using DSTs in FFI-compatible functions (even by-pointer). +## 1. Passing pointers of DSTs across FFI-boundaries is hard +Currently, it's difficult to use DSTs in FFI-compatible functions (even by-pointer). For example, it is not allowed to use `&str`, `&[T]`, or `&dyn Trait` types in an `extern "C"` function. ```rust @@ -60,7 +60,7 @@ Luckily, the [`abi_stable`] crate provides a series of FFI-compatible types like [`RSliceMut`], [`RStr<'a>`], and an attribute macro [`sabi_trait`] that makes ABI-stable trait objects (which are also FFI-compatible). -However, that is tedious and and non-exhaustive because the library writer cannot enumerate all +However, that is tedious and non-exhaustive because the library writer cannot enumerate all compound DSTs (e.g. ADTs with a DST field) exhaustively. [`abi_stable`]: https://crates.io/crates/abi_stable @@ -72,7 +72,7 @@ compound DSTs (e.g. ADTs with a DST field) exhaustively. ## 2. Slices cannot be unsized to trait objects Suppose there is a `dyn`-safe trait `MyTrait`, and it is implemented for `[T]`. However, it is -not possible to convert an `&[T]` to an `&dyn MyTrait` because `[T]` doesn't impl `Sized`. +not possible to convert an `&[T]` to an `&dyn MyTrait` because `[T]` doesn't implement `Sized`. ```rust trait MyTrait { fn foo(&self); @@ -89,10 +89,10 @@ fn as_my_trait(x: &[T]) -> &dyn MyTrait { # Guide-level explanation [guide-level-explanation]: #guide-level-explanation -To overcome the obstacles above, we introduce a `Thin` wrapper that stores the metadata and +To overcome the obstacles above, we introduce a `Thin` wrapper that stores the metadata and a (sized) value inside and thus keeps pointers of `Thin` thin. -## Passing DST pointers accross the FFI boundraries +## Passing DST pointers across the FFI boundaries ```rust extern "C" fn foo( str_slice: &Thin, // ok because `&Thin` is thin @@ -104,7 +104,7 @@ extern "C" fn foo( let str_slice: &Thin = thin_str!("something"); let int_slice: &Thin<[i32]> = &Thin::new_unsized([1, 2, 3]); let opaque_obj: &Thin = &Thin::new_unsized(String::from("hello")); -// Pass the thin DSTs accross FFI boundary +// Pass the thin DSTs across FFI boundaries unsafe { foo(str_slice, int_slice, opaque_obj); } @@ -123,7 +123,7 @@ impl MyTrait for Thin<[T]> { let value: &Thin<[i32]> = &Thin::new_unsized([1, 2, 3]); // Coerce it to a trait object // where `+ ValueSized` is needed to indicate that the size of this trait object -// are calculated from its value. +// is calculated from its value. let dyn_value: &dyn MyTrait + ValueSized = value; // ok because `Thin<[i32]>` is thin // Calls ` as dyn MyTrait>::foo` dyn_value.foo(); @@ -142,7 +142,7 @@ Now they can be rewritten as: - `ThinBox` -> `Box>` - `BoxedTrait` -> `Box>` -where much less boilerplate codes are needed. +where much less boilerplate code is needed. [`List`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/type.List.html [`ThinVec`]: https://docs.rs/thin-vec/latest/thin_vec/struct.ThinVec.html @@ -158,7 +158,7 @@ Regarding [sized hierarchy], `Thin` is more than `PointeeSized` but not `MetaSiz - it is more than `PointeeSized` because we actually know its size by reading the metadata stored inside. -We need to add a new stuff to the sized hierarchy, named `ValueSized`, to indicate a value of +We need to add new stuff to the sized hierarchy, named `ValueSized`, to indicate a value of which the size is known by reading its value, as mentioned in [RFC 3729 (comments)]. ```rust @@ -189,7 +189,7 @@ The public APIs of `Thin` consist of 2 parts: - `Thin`, which is a (maybe unsized) value of `T` with the metadata type of `U` carried on. Typically, `U = T` or `U` is some type that `T: Unsize`. - `EraseMetadata` which is a wrapper of (maybe unsized) `T`, which ignores the metadata of `T`. - E.g., both `&ErasedMetadata` and `&ErasedMetadata<[u8]>` has the same size as a + E.g., both `&EraseMetadata` and `&EraseMetadata<[u8]>` have the same size as a thin pointer `&()`. ```rust // mod core::thin; @@ -197,7 +197,7 @@ The public APIs of `Thin` consist of 2 parts: /// Wrapping a DST `T` with its metadata inlined, /// then the pointers of `Thin` are thin. /// -/// the generic type `U` is for two-stage construction of +/// The generic type `U` is for two-stage construction of /// `Thin`, i.e., `Thin where T: Unsize` must be /// constructed first, then coerced (unsized) to `Thin` /// (aka `Thin`) @@ -217,6 +217,8 @@ pub struct EraseMetadata(T); // The size is unknown because the metadata is erased. impl PointeeSized for EraseMetadata {} +// `EraseMetadata` is a simple wrapper for sized types. +impl Sized for EraseMetadata {} // Value accesses impl ops::Deref for Thin { @@ -229,8 +231,8 @@ impl ops::DerefMut for Thin { ``` ## Value constructions -For a sized typed `Thin`, it is able to construct with `Thin::::new`. -For an unsized (`MetaSized`) typed `Thin`, in general, it requires 3 steps to construct +For a sized type `Thin`, it can be constructed with `Thin::::new`. +For an unsized (`MetaSized`) type `Thin`, in general, it requires 3 steps to construct a `Thin` on stack or on heap: - construct a sized value of `Thin` via `Thin::::new_unsized` (where `T: Unsize`). - obtain a pointer (i.e., `&`, `&mut`, `Box`, `Rc`, `Arc`, etc.) of `Thin` via @@ -243,8 +245,8 @@ impl Thin { /// Create a sized `Thin` value, which is a simple wrapper of `T` pub fn new(value: T) -> Thin { Self { - metadata: (), // Sized type `T` has an empty metadata - data: ErasedMetadata(value), + metadata: (), // Sized type `T` has empty metadata + data: EraseMetadata(value), } } } @@ -258,7 +260,7 @@ impl Thin { { Self { metadata: ptr::metadata(&value as &U), - data: ErasedMetadata(value), + data: EraseMetadata(value), } } /// Consume the `Thin` and return the inner wrapped value of `T` @@ -279,12 +281,12 @@ where [drawbacks]: #drawbacks ## `Thin` is confusing with existing concepts -The term `Thin` has a different meaning with a previous term: the trait `core::ptr::Thin` +The term `Thin` has a different meaning from a previous term: the trait `core::ptr::Thin` (that means types with `Metadata = ()`). ## `ValueSized` doesn't fit `Thin` 100% Generally, the size of `ValueSized` type is calculated from its value. For instance, the size of -a *real* C string (Not `core::ffi::CStr`) is determined by counting the bytes until a `\0` +a *real* C string (not `core::ffi::CStr`) is determined by counting the bytes until a `\0` is reached. In general, `ValueSized` types are `Freeze`, or else the size can change after calculating from its value. Hence, `CStrV2` (the real C string type) is `ValueSized` but `UnsafeCell` is not. @@ -313,7 +315,7 @@ However, it is hard to construct values on stack. [prior-art]: #prior-art - [RFC 2594: Custom DSTs] introduced a more general way to define a DST. This RFC - focus on how to "thinify" a pointer to an existing DST via inlining the metadata, + focuses on how to "thinify" a pointer to an existing DST via inlining the metadata, which is a convenient helper for some common situations. - [RFC 3536: Trait for `!Sized` thin pointers] was very similar to this RFC. This RFC doesn't change the semantics of existing DSTs like `[T]`, which can avoid @@ -326,7 +328,7 @@ However, it is hard to construct values on stack. [unresolved-questions]: #unresolved-questions - Should the trait `ValueSized` provide a user-implementable method `size`? (In this RFC, - such `size` method can only be generated by compiler.) + such a `size` method can only be generated by compiler.) ## Future possibilities [future-possibilities]: #future-possibilities From 9c0740590ae826079b749ad1c4a8979d8dd72fb5 Mon Sep 17 00:00:00 2001 From: Frank King Date: Mon, 22 Dec 2025 20:56:48 +0800 Subject: [PATCH 4/7] Add more descriptions of `EraseMetadata` where `T: Sized` --- ...3898-thin-pointers-with-inline-metadata.md | 65 ++++++++++++++----- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/text/3898-thin-pointers-with-inline-metadata.md b/text/3898-thin-pointers-with-inline-metadata.md index 645fd63fedb..aac91e5fb34 100644 --- a/text/3898-thin-pointers-with-inline-metadata.md +++ b/text/3898-thin-pointers-with-inline-metadata.md @@ -185,12 +185,12 @@ instead of a compile-time constant value. ## Public APIs -The public APIs of `Thin` consist of 2 parts: -- `Thin`, which is a (maybe unsized) value of `T` with the metadata type of `U` carried on. - Typically, `U = T` or `U` is some type that `T: Unsize`. -- `EraseMetadata` which is a wrapper of (maybe unsized) `T`, which ignores the metadata of `T`. - E.g., both `&EraseMetadata` and `&EraseMetadata<[u8]>` have the same size as a - thin pointer `&()`. +The public APIs of `Thin` consist of 2 parts. + +### `Thin` +`Thin` is a (maybe unsized) value of `T` with the metadata type of `U` carried on. + +Typically, `U = T` or `U` is some type that `T: Unsize`. ```rust // mod core::thin; @@ -210,16 +210,6 @@ pub struct Thin { // The size is known via reading its metadata. impl ValueSized for Thin {} -/// A wrapper that ignores the metadata of a type. -#[lang = "erase_metadata"] -#[repr(transparent)] -pub struct EraseMetadata(T); - -// The size is unknown because the metadata is erased. -impl PointeeSized for EraseMetadata {} -// `EraseMetadata` is a simple wrapper for sized types. -impl Sized for EraseMetadata {} - // Value accesses impl ops::Deref for Thin { type Target = U; @@ -230,6 +220,49 @@ impl ops::DerefMut for Thin { } ``` +### `EraseMetadata` +`EraseMetadata` is a wrapper of (maybe unsized) `T`, which ignores the metadata of `T`. + +E.g., both `&EraseMetadata` and `&EraseMetadata<[u8]>` have the same size as a thin pointer `&()`. + +```rust +/// A wrapper that ignores the metadata of a type. +#[lang = "erase_metadata"] +#[repr(transparent)] +pub struct EraseMetadata(T); + +// For sized types, `EraseMetadata` is a simple wrapper. +impl Sized for EraseMetadata {} +impl ops::Deref for EraseMetadata { + type Target = T; + fn deref(&self) -> &T { + &self.0 + } +} +impl ops::DerefMut for EraseMetadata { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl EraseMetadata { + /// Wrap a sized value into an `EraseMetadata`. + pub fn new(inner: T) -> EraseMetadata { + EraseMetadata(inner) + } + /// Unwrap a sized value from an `EraseMetadata`. + pub fn into_inner(self) -> T { + self.0 + } +} + +// For unsized types, `EraseMetadata` is completely opaque because it is unsafe +// to read the inner value without the metadata. + +// The size is unknown because the metadata is erased. +impl PointeeSized for EraseMetadata {} +``` + ## Value constructions For a sized type `Thin`, it can be constructed with `Thin::::new`. For an unsized (`MetaSized`) type `Thin`, in general, it requires 3 steps to construct From 43eabe84339d633871f33d33b107735f61e07852 Mon Sep 17 00:00:00 2001 From: Frank King Date: Mon, 22 Dec 2025 21:05:19 +0800 Subject: [PATCH 5/7] Improving words. --- text/3898-thin-pointers-with-inline-metadata.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/text/3898-thin-pointers-with-inline-metadata.md b/text/3898-thin-pointers-with-inline-metadata.md index aac91e5fb34..53564ca5a4c 100644 --- a/text/3898-thin-pointers-with-inline-metadata.md +++ b/text/3898-thin-pointers-with-inline-metadata.md @@ -199,8 +199,8 @@ Typically, `U = T` or `U` is some type that `T: Unsize`. /// /// The generic type `U` is for two-stage construction of /// `Thin`, i.e., `Thin where T: Unsize` must be -/// constructed first, then coerced (unsized) to `Thin` -/// (aka `Thin`) +/// constructed first, then coerced (unsized) to `Thin` +/// (aka `Thin`) #[repr(C)] pub struct Thin { metadata: U::Metadata, @@ -223,7 +223,8 @@ impl ops::DerefMut for Thin { ### `EraseMetadata` `EraseMetadata` is a wrapper of (maybe unsized) `T`, which ignores the metadata of `T`. -E.g., both `&EraseMetadata` and `&EraseMetadata<[u8]>` have the same size as a thin pointer `&()`. +For example, both `&EraseMetadata` and `&EraseMetadata<[u8]>` have the same size as a thin +pointer `&()`. ```rust /// A wrapper that ignores the metadata of a type. From 80875526f97ccd5f500c78237c389d274e735a55 Mon Sep 17 00:00:00 2001 From: Frank King Date: Fri, 30 Jan 2026 19:28:11 +0800 Subject: [PATCH 6/7] Revision based on the review - add motivation for more thin containers/pointers - add adjustments to the layout entries in vtable - add the layout assumption question of thin pointers to unresolved questions --- ...3898-thin-pointers-with-inline-metadata.md | 63 ++++++++++++++++--- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/text/3898-thin-pointers-with-inline-metadata.md b/text/3898-thin-pointers-with-inline-metadata.md index 53564ca5a4c..bf89729e173 100644 --- a/text/3898-thin-pointers-with-inline-metadata.md +++ b/text/3898-thin-pointers-with-inline-metadata.md @@ -17,7 +17,7 @@ Pointers of dynamically sized types (DSTs) are fat pointers, and they are not FF which prevents some common types like `&str`, `&[T]`, and `&dyn Trait` from being passed across the FFI boundaries. -## 1. Passing pointers of DSTs across FFI-boundaries is hard +## 1. Passing pointers of DSTs across FFI boundaries is hard Currently, it's difficult to use DSTs in FFI-compatible functions (even by-pointer). For example, it is not allowed to use `&str`, `&[T]`, or `&dyn Trait` types in an `extern "C"` function. @@ -87,6 +87,16 @@ fn as_my_trait(x: &[T]) -> &dyn MyTrait { } ``` +## 3. Better performance due to more cache-friendly memory layout +Thin pointers are useful for: +- reduced memory consumption, when there are many copies of the pointer, which happens with `Rc` and `Arc`. +- reduced cache consumption, which is particularly useful when the pointer points to cold data. +- atomic operations: much easier to atomically modify a thin pointer than a fat one. +- having a single-pointer-sized error handler (i.e. `Box>`). + +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. + # Guide-level explanation [guide-level-explanation]: #guide-level-explanation To overcome the obstacles above, we introduce a `Thin` wrapper that stores the metadata and @@ -98,7 +108,7 @@ extern "C" fn foo( str_slice: &Thin, // ok because `&Thin` is thin int_slice: &Thin<[i32]>, // ok because `&Thin<[i32]>` is thin opaque_obj: &Thin, // ok because `&Thin` is thin -} { /* ... */ } +) { /* ... */ } // Construct the values of DSTs on stack let str_slice: &Thin = thin_str!("something"); @@ -144,6 +154,9 @@ Now they can be rewritten as: where much less boilerplate code is needed. +Future possibilities are: +- `Rc>` and `Arc>` + [`List`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/type.List.html [`ThinVec`]: https://docs.rs/thin-vec/latest/thin_vec/struct.ThinVec.html [`ThinBox`]: https://doc.rust-lang.org/std/boxed/struct.ThinBox.html @@ -175,14 +188,43 @@ pub trait ValueSized: PointeeSized {} pub trait MetaSized: ValueSized {} ``` -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, -instead of a compile-time constant value. - - [sized hierarchy]: https://github.com/rust-lang/rust/issues/144404 [RFC 3729 (comments)]: https://github.com/davidtwco/rfcs/blob/sized-hierarchy/text/3729-sized-hierarchy.md#user-content-fn-9-ebb4bca70c758b473dca6f49c3bb3cbc +### Adjustments to the common vtable entries +Now that `dyn Trait` contains an implicit bound `MetaSized` (i.e. `dyn Trait` is short for +`dyn Trait + MetaSized`), we can opt out that bound by explicitly writing `dyn Trait + ValueSized` +or `dyn Trait + PointeeSized`, which loosens the `MetaSized` bound to `ValueSized` or `PointeeSized`. + +Previous layout of a vtable is like: +```rust +struct Vtable { + drop_in_place: unsafe fn(*mut ()), + align: ptr::Alignment, + size: usize, + // function entries +} +``` +It suits well for `dyn Trait + MetaSized`. But for `dyn Trait + ValueSized`, the size is no longer +a constant, but a runtime value that can be calculated by the value. + +We can exploit the niches in `align` and extend the `Value` struct to: +```rust +struct VtableV2 { + drop_in_place: unsafe fn(*mut ()), // unchanged + layout: VtableLayout, +} + +enum VtableLayout { + /// For a `PointeeSized` type, there's nothing known about its layout + PointeeSized, + /// For a `PointeeSized` type, its layout can be computed by invoking a function + ValueSized(fn(*const ()) -> alloc::Layout), + /// For a `MetaSized` type, its layout is known as a certain value. + MetaSized(alloc::Layout), +} +``` + ## Public APIs The public APIs of `Thin` consist of 2 parts. @@ -208,7 +250,7 @@ pub struct Thin { } // The size is known via reading its metadata. -impl ValueSized for Thin {} +impl ValueSized for Thin {} // Value accesses impl ops::Deref for Thin { @@ -293,6 +335,9 @@ impl Thin { T: Unsize, { Self { + // This depends on the constraint that `ptr::metadata` should + // always return the same value, independent of the address of + // `value`. metadata: ptr::metadata(&value as &U), data: EraseMetadata(value), } @@ -363,6 +408,8 @@ However, it is hard to construct values on stack. - Should the trait `ValueSized` provide a user-implementable method `size`? (In this RFC, such a `size` method can only be generated by compiler.) +- 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 than the metadata, so we could pass `&Thin<[u8]>` directly and write the bytes in C without knowing the metadata size. ## Future possibilities [future-possibilities]: #future-possibilities From 915ee3ed757c0678375b8dbcc691b9368bb1f1a2 Mon Sep 17 00:00:00 2001 From: Frank King Date: Fri, 30 Jan 2026 20:10:26 +0800 Subject: [PATCH 7/7] Add future possibilities of custom DSTs --- ...3898-thin-pointers-with-inline-metadata.md | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/text/3898-thin-pointers-with-inline-metadata.md b/text/3898-thin-pointers-with-inline-metadata.md index bf89729e173..ced1cc8c308 100644 --- a/text/3898-thin-pointers-with-inline-metadata.md +++ b/text/3898-thin-pointers-with-inline-metadata.md @@ -408,10 +408,28 @@ However, it is hard to construct values on stack. - Should the trait `ValueSized` provide a user-implementable method `size`? (In this RFC, such a `size` method can only be generated by compiler.) -- 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 than the metadata, so we could pass `&Thin<[u8]>` directly and write the bytes in C without knowing the metadata size. +- 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 than the metadata, +so we could pass `&Thin<[u8]>` directly and write the bytes in C without knowing the metadata size. ## Future possibilities [future-possibilities]: #future-possibilities -None yet. \ No newline at end of file +### Custom DSTs with any-sized metadata +Currently, fat pointers carry pointer-sized metadata only, so there is no way to implement a +partially initialized slice like a `Vec` in a more generic way (i.e., `Box>`), +because `Vec` has one more metadata field (i.e. the `len`) than a slice (the `RawVec`). + +Should the metadata be restricted to be pointer-sized? It is risky to break that constraint +as it may break some existing code based on that assumption. + +`Thin` makes it more practical to implement custom DSTs. + +Take the `PartiallyInitSlice` for example, we can wrap the "inner" metadata (the `len`) +in the `Thin`, and keep the "outer" metadata (the `capacity`) in +` as Pointee>::Metadata`. In this way, we get a custom DST (which +is `MetaSized`) without breaking the pointer-sized metadata assumption. + +Similarly, the `Matrix` can be made by assigning the total length (`rows` * `cols`) to +the outer metadata, and wrapping the length per chunk in the `Thin`.