Skip to main content

boa_string/vtable/
shared.rs

1//! Arc-backed [`JsString`] kind.
2//!
3//! [`SharedJsStr`] owns Latin1 or UTF-16 code units in a buffer held by an
4//! [`Arc`][`std::sync::Arc`]. The value is [`Send`] and [`Sync`].
5//! [`JsString::from_shared`] turns one into a [`JsString`] without copying the
6//! units. Cloning and dropping that `JsString` adjust the same atomic count.
7
8use crate::iter::CodePointsIter;
9use crate::r#type::{InternalStringType, Latin1, Utf16};
10use crate::vtable::JsStringVTable;
11use crate::{JsStr, JsStrVariant, JsString, JsStringKind};
12use std::hash::{Hash, Hasher};
13use std::marker::PhantomData;
14use std::mem::{ManuallyDrop, offset_of};
15use std::ptr::NonNull;
16use std::sync::Arc;
17
18// Distinct from `InternalStringType::KIND`, which selects the non-atomic sequence kind.
19trait SharedType: InternalStringType {
20    const SHARED_KIND: JsStringKind;
21}
22
23impl SharedType for Latin1 {
24    const SHARED_KIND: JsStringKind = JsStringKind::Latin1Shared;
25}
26
27impl SharedType for Utf16 {
28    const SHARED_KIND: JsStringKind = JsStringKind::Utf16Shared;
29}
30
31/// Header stored inside the [`Arc`]. The vtable is the first field so a [`JsString`]
32/// can address this allocation directly.
33///
34/// The code units are a [`Box`], not a trailing slice. [`Arc`] has no public
35/// constructor for a custom dynamically sized header, and the vtable pointer has
36/// to be the `Arc`'s `T` so clone and drop can use [`Arc::increment_strong_count`]
37/// and [`Arc::from_raw`].
38#[repr(C)]
39struct SharedString<T: SharedType> {
40    vtable: JsStringVTable,
41    data: Box<[T::Byte]>,
42    _marker: PhantomData<fn() -> T>,
43}
44
45static_assertions::const_assert!(offset_of!(SharedString<Latin1>, vtable) == 0);
46static_assertions::const_assert!(offset_of!(SharedString<Utf16>, vtable) == 0);
47
48#[derive(Clone)]
49enum SharedInner {
50    Latin1(Arc<SharedString<Latin1>>),
51    Utf16(Arc<SharedString<Utf16>>),
52}
53
54/// Immutable Latin1 or UTF-16 string stored in an [`Arc`][`std::sync::Arc`].
55///
56/// Cloning bumps the atomic reference count and does not copy the code units.
57/// [`JsString::from_shared`] materializes a [`JsString`] that shares the allocation.
58/// Send [`SharedJsStr`] across threads, then call `from_shared` on the thread that
59/// will use the [`JsString`]. [`JsString`] itself is not thread-safe.
60#[derive(Clone)]
61pub struct SharedJsStr {
62    inner: SharedInner,
63}
64
65const _: fn() = || {
66    fn assert<T: Send + Sync>() {}
67    assert::<SharedJsStr>();
68};
69
70impl SharedJsStr {
71    /// Borrows the code units. Does not copy.
72    #[inline]
73    #[must_use]
74    pub fn as_str(&self) -> JsStr<'_> {
75        match &self.inner {
76            SharedInner::Latin1(arc) => JsStr::latin1(&arc.data),
77            SharedInner::Utf16(arc) => JsStr::utf16(&arc.data),
78        }
79    }
80
81    #[inline]
82    fn from_latin1(bytes: &[u8]) -> Self {
83        Self {
84            inner: SharedInner::Latin1(SharedString::<Latin1>::from_units(bytes)),
85        }
86    }
87
88    #[inline]
89    fn from_utf16(units: &[u16]) -> Self {
90        Self {
91            inner: SharedInner::Utf16(SharedString::<Utf16>::from_units(units)),
92        }
93    }
94
95    /// Hands this owner to a [`JsString`] without copying the units.
96    #[inline]
97    #[must_use]
98    pub(crate) fn into_js_string(self) -> JsString {
99        let ptr = match self.inner {
100            SharedInner::Latin1(arc) => vtable_ptr(arc),
101            SharedInner::Utf16(arc) => vtable_ptr(arc),
102        };
103        // SAFETY: `ptr` addresses the vtable of an `Arc` whose strong count this
104        // `JsString` now owns. Later clones and drops go through that vtable.
105        unsafe { JsString::from_ptr(ptr) }
106    }
107}
108
109impl<T: SharedType> SharedString<T> {
110    fn from_units(units: &[T::Byte]) -> Arc<Self> {
111        let data: Box<[T::Byte]> = Box::from(units);
112        let len = data.len();
113        Arc::new(Self {
114            vtable: JsStringVTable {
115                clone: shared_clone::<T>,
116                drop: shared_drop::<T>,
117                as_str: shared_as_str::<T>,
118                code_points: shared_code_points::<T>,
119                refcount: shared_refcount::<T>,
120                len,
121                kind: T::SHARED_KIND,
122            },
123            data,
124            _marker: PhantomData,
125        })
126    }
127}
128
129#[inline]
130fn shared_clone<T: SharedType>(vtable: NonNull<JsStringVTable>) -> JsString {
131    // SAFETY: `vtable` points at a live `SharedString<T>` created by `Arc::into_raw`
132    // (or a later `increment_strong_count` on that pointer). The caller holds one
133    // strong reference, so the allocation is alive. The increment records the clone.
134    // `from_ptr` only copies the address.
135    unsafe {
136        Arc::increment_strong_count(vtable.cast::<SharedString<T>>().as_ptr());
137        JsString::from_ptr(vtable)
138    }
139}
140
141#[inline]
142fn shared_drop<T: SharedType>(vtable: NonNull<JsStringVTable>) {
143    // SAFETY: `vtable` was produced by `Arc::into_raw` or by `shared_clone`.
144    // Rebuilding the `Arc` and dropping it releases exactly one strong reference
145    // and frees the allocation when that was the last owner.
146    unsafe {
147        drop(Arc::from_raw(vtable.cast::<SharedString<T>>().as_ptr()));
148    }
149}
150
151#[inline]
152fn shared_as_str<T: SharedType>(vtable: NonNull<JsStringVTable>) -> JsStr<'static> {
153    // SAFETY: `vtable` is the first field of a live `SharedString<T>` inside an `Arc`.
154    // The `JsString` borrow that called this keeps that `Arc` alive.
155    let this: &SharedString<T> = unsafe { vtable.cast().as_ref() };
156    debug_assert_eq!(this.vtable.len, this.data.len());
157    let ptr = this.data.as_ptr();
158    let len = this.data.len();
159    // SAFETY: `data` is a `Box<[T::Byte]>` of `len` initialized elements, aligned for
160    // `T::Byte`. An empty box still returns a non-null aligned pointer. The `Arc`
161    // owns the buffer for as long as any `JsString` of this kind is borrowed.
162    let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
163    T::str_ctor(slice)
164}
165
166#[inline]
167fn shared_code_points<T: SharedType>(vtable: NonNull<JsStringVTable>) -> CodePointsIter<'static> {
168    CodePointsIter::new(shared_as_str::<T>(vtable))
169}
170
171/// `VTable` function for refcount, need to return an `Option<usize>`.
172#[inline]
173#[allow(clippy::unnecessary_wraps)]
174fn shared_refcount<T: SharedType>(vtable: NonNull<JsStringVTable>) -> Option<usize> {
175    // SAFETY: same live `Arc` as the other vtable functions. `ManuallyDrop` prevents
176    // the temporary `Arc` from decrementing the strong count.
177    let arc =
178        unsafe { ManuallyDrop::new(Arc::from_raw(vtable.cast::<SharedString<T>>().as_ptr())) };
179    Some(Arc::strong_count(&arc))
180}
181
182/// Rebuild an [`Arc`] for a `JsString` whose kind was already checked.
183#[inline]
184fn clone_arc<T: SharedType>(string: &JsString) -> Arc<SharedString<T>> {
185    debug_assert_eq!(string.kind(), T::SHARED_KIND);
186    let ptr = string.ptr.cast::<SharedString<T>>().as_ptr();
187    // SAFETY: `string.kind()` selected `T`, and that kind is only built from
188    // `Arc::<SharedString<T>>::into_raw`. `string` keeps one strong reference.
189    // The increment balances `from_raw`, so `string` keeps its original count.
190    unsafe {
191        Arc::increment_strong_count(ptr);
192        Arc::from_raw(ptr)
193    }
194}
195
196#[inline]
197fn vtable_ptr<T: SharedType>(arc: Arc<SharedString<T>>) -> NonNull<JsStringVTable> {
198    let raw = Arc::into_raw(arc);
199    // SAFETY: `Arc::into_raw` never returns null. `#[repr(C)]` places `vtable` at
200    // offset 0, so this is a valid `JsStringVTable` pointer. The strong count is
201    // transferred, not incremented.
202    unsafe { NonNull::new_unchecked(raw.cast_mut()).cast() }
203}
204
205impl From<&JsString> for SharedJsStr {
206    #[inline]
207    fn from(value: &JsString) -> Self {
208        match value.kind() {
209            JsStringKind::Latin1Shared => Self {
210                inner: SharedInner::Latin1(clone_arc::<Latin1>(value)),
211            },
212            JsStringKind::Utf16Shared => Self {
213                inner: SharedInner::Utf16(clone_arc::<Utf16>(value)),
214            },
215            _ => match value.as_str().variant() {
216                JsStrVariant::Latin1(bytes) => Self::from_latin1(bytes),
217                JsStrVariant::Utf16(units) => Self::from_utf16(units),
218            },
219        }
220    }
221}
222
223impl From<&str> for SharedJsStr {
224    #[inline]
225    fn from(s: &str) -> Self {
226        // Same Latin1 / UTF-16 choice as `From<&str> for JsString`, without interning.
227        // `PartialEq<str>` compares Latin1 against raw bytes, so the cutoff has to match.
228        if s.is_ascii() {
229            return Self::from_latin1(s.as_bytes());
230        }
231        if s.chars().all(|c| c as u32 <= 0xFF) {
232            let bytes: Vec<u8> = s.chars().map(|c| c as u8).collect();
233            return Self::from_latin1(&bytes);
234        }
235        let units = s.encode_utf16().collect::<Vec<_>>();
236        Self::from_utf16(&units)
237    }
238}
239
240impl From<&[u16]> for SharedJsStr {
241    #[inline]
242    fn from(units: &[u16]) -> Self {
243        Self::from_utf16(units)
244    }
245}
246
247impl<const N: usize> From<&[u16; N]> for SharedJsStr {
248    #[inline]
249    fn from(units: &[u16; N]) -> Self {
250        Self::from(&units[..])
251    }
252}
253
254impl PartialEq for SharedJsStr {
255    #[inline]
256    fn eq(&self, other: &Self) -> bool {
257        self.as_str() == other.as_str()
258    }
259}
260
261impl Eq for SharedJsStr {}
262
263impl PartialOrd for SharedJsStr {
264    #[inline]
265    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
266        Some(self.cmp(other))
267    }
268}
269
270impl Ord for SharedJsStr {
271    #[inline]
272    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
273        self.as_str().cmp(&other.as_str())
274    }
275}
276
277impl Hash for SharedJsStr {
278    #[inline]
279    fn hash<H: Hasher>(&self, state: &mut H) {
280        self.as_str().hash(state);
281    }
282}
283
284impl PartialEq<JsString> for SharedJsStr {
285    #[inline]
286    fn eq(&self, other: &JsString) -> bool {
287        self.as_str() == other.as_str()
288    }
289}
290
291impl PartialEq<SharedJsStr> for JsString {
292    #[inline]
293    fn eq(&self, other: &SharedJsStr) -> bool {
294        self.as_str() == other.as_str()
295    }
296}
297
298impl PartialEq<str> for SharedJsStr {
299    #[inline]
300    fn eq(&self, other: &str) -> bool {
301        self.as_str() == other
302    }
303}
304
305impl PartialEq<&str> for SharedJsStr {
306    #[inline]
307    fn eq(&self, other: &&str) -> bool {
308        self.as_str() == *other
309    }
310}
311
312impl PartialEq<SharedJsStr> for str {
313    #[inline]
314    fn eq(&self, other: &SharedJsStr) -> bool {
315        other == self
316    }
317}
318
319impl std::fmt::Debug for SharedJsStr {
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        let units = self.as_str();
322        let content = units.display_lossy().to_string();
323        f.debug_struct("SharedJsStr")
324            .field("len", &units.len())
325            .field("latin1", &units.is_latin1())
326            .field("content", &content)
327            .finish_non_exhaustive()
328    }
329}