Skip to main content

feldera_ijson/
string.rs

1//! Functionality relating to the JSON string type
2
3use std::alloc::{alloc, dealloc, Layout, LayoutError};
4use std::borrow::Borrow;
5use std::cmp::Ordering;
6use std::fmt::{self, Debug, Display, Formatter};
7use std::hash::Hash;
8use std::ops::Deref;
9use std::ptr::{copy_nonoverlapping, NonNull};
10use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
11
12use dashmap::{DashSet, SharedValue};
13use lazy_static::lazy_static;
14
15use crate::thin::{ThinMut, ThinMutExt, ThinRef, ThinRefExt};
16
17use super::value::{IValue, TypeTag};
18
19#[repr(C)]
20#[repr(align(4))]
21struct Header {
22    rc: AtomicUsize,
23    // We use 48 bits for the length and 16 bits for the shard index.
24    len_lower: u32,
25    len_upper: u16,
26    shard_index: u16,
27}
28
29trait HeaderRef<'a>: ThinRefExt<'a, Header> {
30    fn len(&self) -> usize {
31        (u64::from(self.len_lower) | (u64::from(self.len_upper) << 32)) as usize
32    }
33    fn shard_index(&self) -> usize {
34        self.shard_index as usize
35    }
36    fn str_ptr(&self) -> *const u8 {
37        // Safety: pointers to the end of structs are allowed
38        unsafe { self.ptr().add(1).cast() }
39    }
40    fn bytes(&self) -> &'a [u8] {
41        // Safety: Header `len` must be accurate
42        unsafe { std::slice::from_raw_parts(self.str_ptr(), self.len()) }
43    }
44    fn str(&self) -> &'a str {
45        // Safety: UTF-8 enforced on construction
46        unsafe { std::str::from_utf8_unchecked(self.bytes()) }
47    }
48}
49
50trait HeaderMut<'a>: ThinMutExt<'a, Header> {
51    fn str_ptr_mut(mut self) -> *mut u8 {
52        // Safety: pointers to the end of structs are allowed
53        unsafe { self.ptr_mut().add(1).cast() }
54    }
55}
56
57impl<'a, T: ThinRefExt<'a, Header>> HeaderRef<'a> for T {}
58impl<'a, T: ThinMutExt<'a, Header>> HeaderMut<'a> for T {}
59
60lazy_static! {
61    static ref STRING_CACHE: DashSet<WeakIString> = DashSet::new();
62}
63
64// Eagerly initialize the string cache during tests or when the
65// `ctor` feature is enabled.
66#[cfg(any(test, feature = "ctor"))]
67#[ctor::ctor]
68fn ctor_init_cache() {
69    lazy_static::initialize(&STRING_CACHE);
70}
71
72#[doc(hidden)]
73pub fn init_cache() {
74    lazy_static::initialize(&STRING_CACHE);
75}
76
77struct WeakIString {
78    ptr: NonNull<Header>,
79}
80
81unsafe impl Send for WeakIString {}
82unsafe impl Sync for WeakIString {}
83impl PartialEq for WeakIString {
84    fn eq(&self, other: &Self) -> bool {
85        **self == **other
86    }
87}
88impl Eq for WeakIString {}
89impl Hash for WeakIString {
90    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
91        (**self).hash(state);
92    }
93}
94
95impl Deref for WeakIString {
96    type Target = str;
97    fn deref(&self) -> &str {
98        self.borrow()
99    }
100}
101
102impl Borrow<str> for WeakIString {
103    fn borrow(&self) -> &str {
104        self.header().str()
105    }
106}
107impl WeakIString {
108    fn header(&self) -> ThinRef<Header> {
109        // Safety: pointer is always valid
110        unsafe { ThinRef::new(self.ptr.as_ptr()) }
111    }
112    fn upgrade(&self) -> IString {
113        unsafe {
114            self.ptr.as_ref().rc.fetch_add(1, AtomicOrdering::Relaxed);
115            IString(IValue::new_ptr(
116                self.ptr.as_ptr().cast::<u8>(),
117                TypeTag::StringOrNull,
118            ))
119        }
120    }
121}
122
123/// The `IString` type is an interned, immutable string, and is where this crate
124/// gets its name.
125///
126/// Cloning an `IString` is cheap, and it can be easily converted from `&str` or
127/// `String` types. Comparisons between `IString`s is a simple pointer
128/// comparison.
129///
130/// The memory backing an `IString` is reference counted, so that unlike many
131/// string interning libraries, memory is not leaked as new strings are interned.
132/// Interning uses `DashSet`, an implementation of a concurrent hash-set, allowing
133/// many strings to be interned concurrently without becoming a bottleneck.
134///
135/// Given the nature of `IString` it is better to intern a string once and reuse
136/// it, rather than continually convert from `&str` to `IString`.
137#[repr(transparent)]
138#[derive(Clone, size_of::SizeOf)]
139pub struct IString(pub(crate) IValue);
140
141impl Display for IString {
142    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
143        write!(f, "{}", self.as_str())
144    }
145}
146
147value_subtype_impls!(IString, into_string, as_string, as_string_mut);
148
149static EMPTY_HEADER: Header = Header {
150    len_lower: 0,
151    len_upper: 0,
152    shard_index: 0,
153    rc: AtomicUsize::new(0),
154};
155
156impl IString {
157    fn layout(len: usize) -> Result<Layout, LayoutError> {
158        Ok(Layout::new::<Header>()
159            .extend(Layout::array::<u8>(len)?)?
160            .0
161            .pad_to_align())
162    }
163
164    fn alloc(s: &str, shard_index: usize) -> *mut Header {
165        assert!((s.len() as u64) < (1 << 48));
166        assert!(shard_index < (1 << 16));
167        unsafe {
168            let ptr = alloc(Self::layout(s.len()).unwrap()).cast::<Header>();
169            ptr.write(Header {
170                len_lower: s.len() as u32,
171                len_upper: ((s.len() as u64) >> 32) as u16,
172                shard_index: shard_index as u16,
173                rc: AtomicUsize::new(0),
174            });
175            let hd = ThinMut::new(ptr);
176            copy_nonoverlapping(s.as_ptr(), hd.str_ptr_mut(), s.len());
177            ptr
178        }
179    }
180
181    fn dealloc(ptr: *mut Header) {
182        unsafe {
183            let hd = ThinRef::new(ptr);
184            let layout = Self::layout(hd.len()).unwrap();
185            dealloc(ptr.cast::<u8>(), layout);
186        }
187    }
188
189    /// Converts a `&str` to an `IString` by interning it in the global string cache.
190    #[must_use]
191    pub fn intern(s: &str) -> Self {
192        if s.is_empty() {
193            return Self::new();
194        }
195        let cache = &*STRING_CACHE;
196        let shard_index = cache.determine_map(s);
197
198        // Safety: `determine_map` should only return valid shard indices
199        let shard = unsafe { cache.shards().get_unchecked(shard_index) };
200        let mut guard = shard.write();
201        if let Some((k, _)) = guard.get_key_value(s) {
202            k.upgrade()
203        } else {
204            let k = unsafe {
205                WeakIString {
206                    ptr: NonNull::new_unchecked(Self::alloc(s, shard_index)),
207                }
208            };
209            let res = k.upgrade();
210            guard.insert(k, SharedValue::new(()));
211            res
212        }
213    }
214
215    fn header(&self) -> ThinRef<Header> {
216        unsafe { ThinRef::new(self.0.ptr().cast()) }
217    }
218
219    /// Returns the length (in bytes) of this string.
220    #[must_use]
221    pub fn len(&self) -> usize {
222        self.header().len()
223    }
224
225    /// Returns `true` if this is the empty string "".
226    #[must_use]
227    pub fn is_empty(&self) -> bool {
228        self.len() == 0
229    }
230
231    /// Obtains a `&str` from this `IString`. This is a cheap operation.
232    #[must_use]
233    pub fn as_str(&self) -> &str {
234        self.header().str()
235    }
236
237    /// Obtains a byte slice from this `IString`. This is a cheap operation.
238    #[must_use]
239    pub fn as_bytes(&self) -> &[u8] {
240        self.header().bytes()
241    }
242
243    /// Returns the empty string.
244    #[must_use]
245    pub fn new() -> Self {
246        unsafe { IString(IValue::new_ref(&EMPTY_HEADER, TypeTag::StringOrNull)) }
247    }
248
249    pub(crate) fn clone_impl(&self) -> IValue {
250        if self.is_empty() {
251            Self::new().0
252        } else {
253            self.header().rc.fetch_add(1, AtomicOrdering::Relaxed);
254            unsafe { self.0.raw_copy() }
255        }
256    }
257    pub(crate) fn drop_impl(&mut self) {
258        if !self.is_empty() {
259            let hd = self.header();
260
261            // If the reference count is greater than 1, we can safely decrement it without
262            // locking the string cache.
263            let mut rc = hd.rc.load(AtomicOrdering::Relaxed);
264            while rc > 1 {
265                match hd.rc.compare_exchange_weak(
266                    rc,
267                    rc - 1,
268                    AtomicOrdering::Relaxed,
269                    AtomicOrdering::Relaxed,
270                ) {
271                    Ok(_) => return,
272                    Err(new_rc) => rc = new_rc,
273                }
274            }
275
276            // Slow path: we observed a reference count of 1, so we need to lock the string cache
277            let cache = &*STRING_CACHE;
278            // Safety: the number of shards is fixed
279            let shard = unsafe { cache.shards().get_unchecked(hd.shard_index()) };
280            let mut guard = shard.write();
281            if hd.rc.fetch_sub(1, AtomicOrdering::Relaxed) == 1 {
282                // Reference count reached zero, free the string
283                assert!(guard.remove(hd.str()).is_some());
284
285                // Shrink the shard if it's mostly empty.
286                // The second condition is necessary because `HashMap` sometimes
287                // reports a capacity of zero even when it's still backed by an
288                // allocation.
289                if guard.len() * 3 < guard.capacity() || guard.is_empty() {
290                    guard.shrink_to_fit();
291                }
292                drop(guard);
293
294                Self::dealloc(unsafe { self.0.ptr().cast() });
295            }
296        }
297    }
298}
299
300impl Deref for IString {
301    type Target = str;
302    fn deref(&self) -> &str {
303        self.as_str()
304    }
305}
306
307impl Borrow<str> for IString {
308    fn borrow(&self) -> &str {
309        self.as_str()
310    }
311}
312
313impl From<&str> for IString {
314    fn from(other: &str) -> Self {
315        Self::intern(other)
316    }
317}
318
319impl From<&mut str> for IString {
320    fn from(other: &mut str) -> Self {
321        Self::intern(other)
322    }
323}
324
325impl From<String> for IString {
326    fn from(other: String) -> Self {
327        Self::intern(other.as_str())
328    }
329}
330
331impl From<&String> for IString {
332    fn from(other: &String) -> Self {
333        Self::intern(other.as_str())
334    }
335}
336
337impl From<&mut String> for IString {
338    fn from(other: &mut String) -> Self {
339        Self::intern(other.as_str())
340    }
341}
342
343impl From<IString> for String {
344    fn from(other: IString) -> Self {
345        other.as_str().into()
346    }
347}
348
349impl PartialEq for IString {
350    fn eq(&self, other: &Self) -> bool {
351        self.0.raw_eq(&other.0)
352    }
353}
354
355impl PartialEq<str> for IString {
356    fn eq(&self, other: &str) -> bool {
357        self.as_str() == other
358    }
359}
360
361impl PartialEq<IString> for str {
362    fn eq(&self, other: &IString) -> bool {
363        self == other.as_str()
364    }
365}
366
367impl PartialEq<String> for IString {
368    fn eq(&self, other: &String) -> bool {
369        self.as_str() == other
370    }
371}
372
373impl PartialEq<IString> for String {
374    fn eq(&self, other: &IString) -> bool {
375        self == other.as_str()
376    }
377}
378
379impl Default for IString {
380    fn default() -> Self {
381        Self::new()
382    }
383}
384
385impl Eq for IString {}
386impl Ord for IString {
387    fn cmp(&self, other: &Self) -> Ordering {
388        if self == other {
389            Ordering::Equal
390        } else {
391            self.as_str().cmp(other.as_str())
392        }
393    }
394}
395impl PartialOrd for IString {
396    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
397        Some(self.cmp(other))
398    }
399}
400impl Hash for IString {
401    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
402        self.0.raw_hash(state);
403    }
404}
405
406impl Debug for IString {
407    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
408        Debug::fmt(self.as_str(), f)
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415
416    #[mockalloc::test]
417    fn can_intern() {
418        let x = IString::intern("foo");
419        let y = IString::intern("bar");
420        let z = IString::intern("foo");
421
422        assert_eq!(x.as_ptr(), z.as_ptr());
423        assert_ne!(x.as_ptr(), y.as_ptr());
424        assert_eq!(x.as_str(), "foo");
425        assert_eq!(y.as_str(), "bar");
426    }
427
428    #[mockalloc::test]
429    fn default_interns_string() {
430        let x = IString::intern("");
431        let y = IString::new();
432        let z = IString::intern("foo");
433
434        assert_eq!(x.as_ptr(), y.as_ptr());
435        assert_ne!(x.as_ptr(), z.as_ptr());
436    }
437}