From 26f53f752c5ffc2dd468a9c5ecac600cb967c5a8 Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Wed, 16 Sep 2026 17:35:54 +0200 Subject: [PATCH 1/7] into/from_part --- src/lib.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 69d5e16..e82d108 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -701,7 +701,7 @@ impl ThinVec { unsafe { if !empty_header_is_aligned && self.header().cap() == 0 { - NonNull::dangling().as_ptr() + ptr::dangling_mut() } else { // This could technically result in overflow, but padding // would have to be absurdly large for this to occur. @@ -712,6 +712,78 @@ impl ThinVec { } } + /// Decomposes a `ThinVec` into its raw components: `(pointer, length, capacity)`. + /// + /// Returns the raw pointer to the underlying data, the length of + /// the vector (in elements), and it's capacity (also in elements). + /// + /// After calling this function, the caller is responsible for the + /// memory previously managed by the `ThinVec`. It is highly recommended that one does + /// this by converting the raw pointer and length back + /// into a `ThinVec` with the [`from_raw_parts`] function, + /// since the given pointer is offsetted from the actual allocation pointer. + /// + /// [`from_raw_parts`]: ThinVec::from_raw_parts + #[must_use = "losing the pointer will leak memory"] + pub fn into_parts(self) -> (NonNull, usize, usize) { + let data_ptr = unsafe { NonNull::new_unchecked(self.data_raw()) }; + let len = self.len(); + let cap = self.capacity(); + mem::forget(self); + (data_ptr, len, cap) + } + + /// Creates a `ThinVec` directly from a pointer, a length, and a capacity. + /// + /// # Safety + /// + /// This is highly unsafe, due to the number of invariants that aren't + /// checked: + /// + /// * If `T` is not a zero-sized type and the capacity is nonzero, `ptr` must have + /// been acquired via [`ThinVec::into_parts`] + /// * `length` needs to be less than or equal to `capacity`. + /// * The first `length` values must be properly initialized values of type `T`. + /// * `capacity` needs to be the capacity that the pointer was acquired with. + /// * If `T` is not a zero-sized type and the capacity is nonzero, + /// `T` must have the same layout as the `T` when `ptr` was acquired + /// + /// The ownership of `ptr` is effectively transferred to the + /// `ThinVec` which may then deallocate, reallocate or change the + /// contents of memory pointed to by the pointer at will. Ensure + /// that nothing else uses the pointer after calling this + /// function. + /// + /// [`ThinVec::into_parts`]: ThinVec::into_parts + pub unsafe fn from_parts(ptr: NonNull, len: usize, capacity: usize) -> Self { + // `padding` contains ~static assertions against types that are + // incompatible with the current feature flags. We also call it to + // invoke these assertions when creating a `ThinVec`, even if we don't need the result. + let padding = padding::(); + + if Self::is_zst() { + return unsafe { + ThinVec { + ptr: len_to_ptr_unchecked(len + 1), + boo: PhantomData, + } + }; + } + + unsafe { + if capacity == 0 { + Self::new() + } else { + let header_size = mem::size_of::
(); + let ptr = ptr.byte_sub(header_size + padding); + ThinVec { + ptr: ptr.cast(), + boo: PhantomData, + } + } + } + } + /// # Safety /// /// This is unsafe when the header is EMPTY_HEADER or when T is a ZST. From a21e35ee0f2dcf7070f89d54dfa8d0b64b2c2401 Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Wed, 16 Sep 2026 17:50:01 +0200 Subject: [PATCH 2/7] reword the documentation for from_parts --- src/lib.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e82d108..fbc957b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -740,13 +740,16 @@ impl ThinVec { /// This is highly unsafe, due to the number of invariants that aren't /// checked: /// - /// * If `T` is not a zero-sized type and the capacity is nonzero, `ptr` must have - /// been acquired via [`ThinVec::into_parts`] + /// * The raw pointer must have been previously returned by a call to + /// [`ThinVec::into_parts`], it is undefined behavior if not. + /// * `U` must have the same layout as `T`. This is trivially true if `U` is `T`. + /// * Note that if `U` is not `T` but has the same size + /// and alignment, this is basically like transmuting references of + /// different types. See [`mem::transmute`] for more information + /// on what restrictions apply in this case. /// * `length` needs to be less than or equal to `capacity`. /// * The first `length` values must be properly initialized values of type `T`. - /// * `capacity` needs to be the capacity that the pointer was acquired with. - /// * If `T` is not a zero-sized type and the capacity is nonzero, - /// `T` must have the same layout as the `T` when `ptr` was acquired + /// * `capacity` needs to be the exact same capacity that the pointer was acquired with. /// /// The ownership of `ptr` is effectively transferred to the /// `ThinVec` which may then deallocate, reallocate or change the @@ -754,7 +757,8 @@ impl ThinVec { /// that nothing else uses the pointer after calling this /// function. /// - /// [`ThinVec::into_parts`]: ThinVec::into_parts + /// [`ThinVec::into_parts`]: ThinVec::into_parts + /// [`mem::transmute`]: core::mem::transmute pub unsafe fn from_parts(ptr: NonNull, len: usize, capacity: usize) -> Self { // `padding` contains ~static assertions against types that are // incompatible with the current feature flags. We also call it to From 479e8277b1ae7408147d4a0c43278a77faba39d4 Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Wed, 16 Sep 2026 17:53:16 +0200 Subject: [PATCH 3/7] fixed docs --- src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fbc957b..1507325 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -744,8 +744,7 @@ impl ThinVec { /// [`ThinVec::into_parts`], it is undefined behavior if not. /// * `U` must have the same layout as `T`. This is trivially true if `U` is `T`. /// * Note that if `U` is not `T` but has the same size - /// and alignment, this is basically like transmuting references of - /// different types. See [`mem::transmute`] for more information + /// and alignment, this is basically like transmuting different types. See [`mem::transmute`] for more information /// on what restrictions apply in this case. /// * `length` needs to be less than or equal to `capacity`. /// * The first `length` values must be properly initialized values of type `T`. From 54ef80002d6979c33ecd7832bf3bab7597d355a1 Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Wed, 16 Sep 2026 17:55:25 +0200 Subject: [PATCH 4/7] fix doc references --- src/lib.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1507325..7833164 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -718,12 +718,10 @@ impl ThinVec { /// the vector (in elements), and it's capacity (also in elements). /// /// After calling this function, the caller is responsible for the - /// memory previously managed by the `ThinVec`. It is highly recommended that one does - /// this by converting the raw pointer and length back - /// into a `ThinVec` with the [`from_raw_parts`] function, - /// since the given pointer is offsetted from the actual allocation pointer. + /// memory previously managed by the `ThinVec`. one does this by converting the raw pointer and length back + /// into a `ThinVec` with the [`from_parts`] function, since the given pointer is offsetted from the actual allocation pointer. /// - /// [`from_raw_parts`]: ThinVec::from_raw_parts + /// [`from_parts`]: ThinVec::from_parts #[must_use = "losing the pointer will leak memory"] pub fn into_parts(self) -> (NonNull, usize, usize) { let data_ptr = unsafe { NonNull::new_unchecked(self.data_raw()) }; From 566e1b0c3ae4c96aadea47b32514912b5ace4150 Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Mon, 21 Sep 2026 15:32:39 +0200 Subject: [PATCH 5/7] change into_parts to into_raw, now just return a pointer, and ZSTs naturaly produce aligned pointers --- src/lib.rs | 146 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 100 insertions(+), 46 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7833164..ba0800e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -174,9 +174,7 @@ use malloc_size_of::{MallocShallowSizeOf, MallocSizeOf, MallocSizeOfOps}; #[cfg(not(feature = "gecko-ffi"))] mod impl_details { pub type SizeType = usize; - // for ZSTs, store the length in the the NonNull as a NonZero, - // the length is thus off by one and can only reach usize::MAX - 1 - pub const MAX_CAP: usize = usize::MAX - 1; + pub const MAX_CAP: usize = usize::MAX; #[inline(always)] pub fn assert_size(x: usize) -> SizeType { @@ -549,7 +547,7 @@ impl ThinVec { if Self::is_zst() { unsafe { ThinVec { - ptr: len_to_ptr_unchecked(1), + ptr: len_to_ptr_unchecked(align_of::()), boo: PhantomData, } } @@ -631,7 +629,7 @@ impl ThinVec { if Self::is_zst() { unsafe { return ThinVec { - ptr: len_to_ptr_unchecked(1), + ptr: len_to_ptr_unchecked(align_of::()), boo: PhantomData, }; } @@ -666,7 +664,7 @@ impl ThinVec { fn data_raw(&self) -> *mut T { if Self::is_zst() { - return ptr::dangling_mut(); + return self.ptr.cast().as_ptr(); } // `padding` contains ~static assertions against types that are @@ -701,7 +699,7 @@ impl ThinVec { unsafe { if !empty_header_is_aligned && self.header().cap() == 0 { - ptr::dangling_mut() + ptr::without_provenance_mut(align_of::()) } else { // This could technically result in overflow, but padding // would have to be absurdly large for this to occur. @@ -712,26 +710,40 @@ impl ThinVec { } } - /// Decomposes a `ThinVec` into its raw components: `(pointer, length, capacity)`. - /// - /// Returns the raw pointer to the underlying data, the length of - /// the vector (in elements), and it's capacity (also in elements). + /// Consumme the `ThinVec` and returns the raw pointer to the underlying data /// /// After calling this function, the caller is responsible for the /// memory previously managed by the `ThinVec`. one does this by converting the raw pointer and length back - /// into a `ThinVec` with the [`from_parts`] function, since the given pointer is offsetted from the actual allocation pointer. + /// into a `ThinVec` with the [`from_raw`] function, since the given pointer is offsetted from the actual allocation pointer. + /// + /// # Examples + /// + /// ``` + /// use thin_vec::ThinVec; /// - /// [`from_parts`]: ThinVec::from_parts + /// let v: ThinVec = thin_vec::thin_vec![-1, 0, 1]; + /// + /// let ptr = v.into_raw(); + /// + /// let rebuilt = unsafe { + /// // We can now make changes to the components, such as + /// // transmuting the raw pointer to a compatible type. + /// let ptr = ptr.cast::(); + /// + /// ThinVec::from_raw(ptr) + /// }; + /// assert_eq!(rebuilt, [4294967295, 0, 1]); + /// ``` + /// + /// [`from_raw`]: ThinVec::from_raw #[must_use = "losing the pointer will leak memory"] - pub fn into_parts(self) -> (NonNull, usize, usize) { + pub fn into_raw(self) -> NonNull { let data_ptr = unsafe { NonNull::new_unchecked(self.data_raw()) }; - let len = self.len(); - let cap = self.capacity(); mem::forget(self); - (data_ptr, len, cap) + data_ptr } - /// Creates a `ThinVec` directly from a pointer, a length, and a capacity. + /// Creates a `ThinVec` directly from a pointer. /// /// # Safety /// @@ -739,14 +751,11 @@ impl ThinVec { /// checked: /// /// * The raw pointer must have been previously returned by a call to - /// [`ThinVec::into_parts`], it is undefined behavior if not. + /// [`ThinVec::into_raw`], it is undefined behavior if not. /// * `U` must have the same layout as `T`. This is trivially true if `U` is `T`. /// * Note that if `U` is not `T` but has the same size /// and alignment, this is basically like transmuting different types. See [`mem::transmute`] for more information /// on what restrictions apply in this case. - /// * `length` needs to be less than or equal to `capacity`. - /// * The first `length` values must be properly initialized values of type `T`. - /// * `capacity` needs to be the exact same capacity that the pointer was acquired with. /// /// The ownership of `ptr` is effectively transferred to the /// `ThinVec` which may then deallocate, reallocate or change the @@ -754,25 +763,55 @@ impl ThinVec { /// that nothing else uses the pointer after calling this /// function. /// - /// [`ThinVec::into_parts`]: ThinVec::into_parts + /// # Examples + /// + /// ``` + /// use std::ptr; + /// use thin_vec::ThinVec; + /// + /// let v = thin_vec::thin_vec![1, 2, 3]; + /// + /// // Deconstruct the vector into parts. + /// let len = v.len(); + /// let p = v.into_raw(); + /// + /// unsafe { + /// // Overwrite memory with 4, 5, 6 + /// for i in 0..len { + /// p.add(i).write(4 + i); + /// } + /// + /// // Put everything back together into a ThinVec + /// let rebuilt = ThinVec::from_raw(p); + /// assert_eq!(rebuilt, [4, 5, 6]); + /// } + /// ``` + /// + /// [`ThinVec::into_raw`]: ThinVec::into_raw /// [`mem::transmute`]: core::mem::transmute - pub unsafe fn from_parts(ptr: NonNull, len: usize, capacity: usize) -> Self { + pub unsafe fn from_raw(ptr: NonNull) -> Self { // `padding` contains ~static assertions against types that are // incompatible with the current feature flags. We also call it to // invoke these assertions when creating a `ThinVec`, even if we don't need the result. let padding = padding::(); if Self::is_zst() { - return unsafe { - ThinVec { - ptr: len_to_ptr_unchecked(len + 1), - boo: PhantomData, - } + return ThinVec { + ptr: ptr.cast(), + boo: PhantomData, }; } + // See `data_raw`, it mirrors the math + + let empty_header_is_aligned = if cfg!(feature = "gecko-ffi") { + true + } else { + mem::align_of::
() >= mem::align_of::() && padding == 0 + }; + unsafe { - if capacity == 0 { + if !empty_header_is_aligned && ptr.addr().get() == align_of::() { Self::new() } else { let header_size = mem::size_of::
(); @@ -807,7 +846,8 @@ impl ThinVec { /// ``` pub fn len(&self) -> usize { if Self::is_zst() { - (self.ptr.as_ptr() as usize) - 1 + let as_int = self.ptr.as_ptr() as usize; + (as_int / align_of::()) - 1 } else { unsafe { self.header().len() } } @@ -843,7 +883,7 @@ impl ThinVec { /// ``` pub fn capacity(&self) -> usize { if Self::is_zst() { - MAX_CAP + (MAX_CAP / align_of::()) - 1 } else { unsafe { self.header().cap() } } @@ -948,15 +988,17 @@ impl ThinVec { /// For internal use only, when setting the length and it's known that T is a ZST. /// # Safety /// - This is unsafe when T is not a ZST. - /// - len must be < usize::MAX + /// - len must be < usize::MAX / align_of:: #[inline] unsafe fn set_len_zst(&mut self, len: usize) { debug_assert!(Self::is_zst()); debug_assert!( - len <= MAX_CAP, - "invalid set_len(usize::MAX) on ZST ThinVec (max cap is usize::MAX - 1)" + len < usize::MAX / align_of::(), + "invalid set_len({}) on ZST ThinVec, max capacity is {}", + len, + (usize::MAX / align_of::()) - 1 ); - unsafe { self.ptr = len_to_ptr_unchecked(len + 1) } + unsafe { self.ptr = len_to_ptr_unchecked((len + 1) * align_of::()) } } /// For internal use only, when setting the length and it's known that the header is owned. @@ -1299,7 +1341,7 @@ impl ThinVec { if min_cap <= old_cap { return; } - // only way to get here is if min_cap == usize::MAX, which we can't handle. + // ZSTs are already at max cap if Self::is_zst() { capacity_overflow(); } @@ -1382,7 +1424,7 @@ impl ThinVec { let new_cap = self.len().checked_add(additional).unwrap_cap_overflow(); let old_cap = self.capacity(); if new_cap > old_cap { - // only way to get here is if new_cap == usize::MAX, which we can't handle. + // ZSTs are already at max cap if Self::is_zst() { capacity_overflow() } @@ -4302,19 +4344,31 @@ mod std_tests { #[test] #[cfg(not(feature = "gecko-ffi"))] fn test_drain_max_vec_size() { - let mut v = ThinVec::<()>::with_capacity(MAX_CAP); + let mut v = ThinVec::<()>::new(); + let cap = v.capacity(); unsafe { - v.set_len(MAX_CAP); + v.set_len(cap); } - for _ in v.drain(MAX_CAP - 1..) {} - assert_eq!(v.len(), MAX_CAP - 1); + for _ in v.drain(cap - 1..) {} + assert_eq!(v.len(), cap - 1); - let mut v = ThinVec::<()>::with_capacity(MAX_CAP); + let mut v = ThinVec::<()>::with_capacity(cap); unsafe { - v.set_len(MAX_CAP); + v.set_len(cap); } - for _ in v.drain(MAX_CAP - 1..=MAX_CAP - 1) {} - assert_eq!(v.len(), MAX_CAP - 1); + for _ in v.drain(cap - 1..=cap - 1) {} + assert_eq!(v.len(), cap - 1); + } + + #[test] + #[should_panic = "capacity overflow"] + fn test_zst_cap_overflow() { + let mut v = ThinVec::<()>::new(); + let cap = v.capacity(); + unsafe { + v.set_len(cap); + } + v.push(()); } #[test] From 3c05d025d6936ca5067fcecad3a0b0b28f71584a Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Mon, 21 Sep 2026 15:55:38 +0200 Subject: [PATCH 6/7] fix len overflow --- src/lib.rs | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ba0800e..1906a50 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -268,6 +268,7 @@ mod impl_details { } #[inline] + #[cfg_attr(debug_assertions, track_caller)] pub fn assert_size(x: usize) -> SizeType { if x > MAX_CAP as usize { panic!("nsTArray size may not exceed the capacity of a 32-bit sized int"); @@ -333,6 +334,7 @@ impl Header { } #[inline] + #[cfg_attr(debug_assertions, track_caller)] fn set_len(&mut self, len: usize) { self._len = assert_size(len); } @@ -483,6 +485,10 @@ const unsafe fn len_to_ptr_unchecked(len: usize) -> NonNull { unsafe { mem::transmute(NonZeroUsize::new_unchecked(len)) } } +const fn zst_max_cap() -> usize { + (usize::MAX / align_of::()) - align_of::() +} + /// See the crate's top level documentation for a description of this type. #[repr(C)] pub struct ThinVec { @@ -883,7 +889,7 @@ impl ThinVec { /// ``` pub fn capacity(&self) -> usize { if Self::is_zst() { - (MAX_CAP / align_of::()) - 1 + zst_max_cap::() } else { unsafe { self.header().cap() } } @@ -975,6 +981,7 @@ impl ThinVec { /// /// Normally, here, one would use [`clear`] instead to correctly drop /// the contents and thus not leak memory. + #[cfg_attr(debug_assertions, track_caller)] pub unsafe fn set_len(&mut self, len: usize) { if self.is_singleton() { // A prerequisite of `Vec::set_len` is that `new_len` must be @@ -987,32 +994,35 @@ impl ThinVec { /// For internal use only, when setting the length and it's known that T is a ZST. /// # Safety - /// - This is unsafe when T is not a ZST. - /// - len must be < usize::MAX / align_of:: + /// - This is UB when T is not a ZST. + /// - len must be <= zst_max_cap::() #[inline] + #[cfg_attr(debug_assertions, track_caller)] unsafe fn set_len_zst(&mut self, len: usize) { debug_assert!(Self::is_zst()); debug_assert!( - len < usize::MAX / align_of::(), + len <= zst_max_cap::(), "invalid set_len({}) on ZST ThinVec, max capacity is {}", len, - (usize::MAX / align_of::()) - 1 + zst_max_cap::() ); unsafe { self.ptr = len_to_ptr_unchecked((len + 1) * align_of::()) } } /// For internal use only, when setting the length and it's known that the header is owned. /// # Safety - /// This is unsafe when the header is EMPTY_HEADER or when T is a ZST. + /// This is UB when the header is EMPTY_HEADER or when T is a ZST. #[inline] + #[cfg_attr(debug_assertions, track_caller)] unsafe fn set_header_len(&mut self, len: usize) { unsafe { self.header_mut().set_len(len) } } /// For internal use only, when setting the length and it's known to be the non-singleton or T is a ZST. /// # Safety - /// This is unsafe when the header is EMPTY_HEADER. + /// This is UB when the header is EMPTY_HEADER. #[inline(always)] + #[cfg_attr(debug_assertions, track_caller)] unsafe fn set_len_non_singleton(&mut self, len: usize) { debug_assert!(!self.is_singleton()); if Self::is_zst() { @@ -3331,7 +3341,7 @@ impl std::io::Write for ThinVec { #[cfg(test)] mod tests { - use super::{MAX_CAP, ThinVec}; + use super::ThinVec; use crate::alloc::{string::ToString, vec}; #[test] @@ -3480,12 +3490,13 @@ mod tests { should_panic = "ThinVec cannot bridge to nsTArray when T is zero-sized" )] fn test_drain_max_vec_size() { - let mut v = ThinVec::<()>::with_capacity(MAX_CAP); + let zst_max_cap = usize::MAX - 1; + let mut v = ThinVec::<()>::with_capacity(zst_max_cap); unsafe { - v.set_len(MAX_CAP); + v.set_len(zst_max_cap); } - for _ in v.drain(MAX_CAP - 1..) {} - assert_eq!(v.len(), MAX_CAP - 1); + for _ in v.drain(zst_max_cap - 1..) {} + assert_eq!(v.len(), zst_max_cap - 1); } #[test] From 13752d57a4116c2f535e7a8db97e644b71f5a7fd Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Mon, 21 Sep 2026 16:00:25 +0200 Subject: [PATCH 7/7] fix wrong reason for a test when gecko-ffi is enabled --- src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 1906a50..1d822cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4372,7 +4372,11 @@ mod std_tests { } #[test] - #[should_panic = "capacity overflow"] + #[cfg_attr( + feature = "gecko-ffi", + should_panic = "ThinVec cannot bridge to nsTArray when T is zero-sized" + )] + #[cfg_attr(not(feature = "gecko-ffi"), should_panic = "capacity overflow")] fn test_zst_cap_overflow() { let mut v = ThinVec::<()>::new(); let cap = v.capacity();