Skip to main content

string_cache/
atom.rs

1// Copyright 2014 The Servo Project Developers. See the COPYRIGHT
2// file at the top-level directory of this distribution.
3//
4// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7// option. This file may not be copied, modified, or distributed
8// except according to those terms.
9
10use crate::dynamic_set::{Entry, dynamic_set};
11use crate::static_sets::StaticAtomSet;
12use debug_unreachable::debug_unreachable;
13
14use std::borrow::Cow;
15use std::cmp::Ordering::{self, Equal};
16use std::fmt;
17use std::hash::{Hash, Hasher};
18use std::marker::PhantomData;
19use std::mem;
20use std::num::NonZeroU64;
21use std::ops;
22use std::slice;
23use std::str;
24use std::sync::atomic::Ordering::SeqCst;
25
26const DYNAMIC_TAG: u8 = 0b_00;
27const INLINE_TAG: u8 = 0b_01; // len in upper nybble
28const STATIC_TAG: u8 = 0b_10;
29const TAG_MASK: u64 = 0b_11;
30
31/// With alignment, a `*const Entry` pointer always has zeroes in its lowest `TAG_BITS` bits
32const _: () = assert!(mem::align_of::<Entry>() >= TAG_MASK.next_power_of_two() as usize);
33
34const LEN_OFFSET: u64 = 4;
35const LEN_MASK: u64 = 0xF0;
36
37const MAX_INLINE_LEN: usize = 7;
38const STATIC_SHIFT_BITS: usize = 32;
39
40/// Represents a string that has been interned.
41///
42/// While the type definition for `Atom` indicates that it generic on a particular
43/// implementation of an atom set, you don't need to worry about this.  Atoms can be static
44/// and come from a `StaticAtomSet` generated by the `string_cache_codegen` crate, or they
45/// can be dynamic and created by you on an `EmptyStaticAtomSet`.
46///
47/// `Atom` implements `Clone` but not `Copy`, since internally atoms are reference-counted;
48/// this means that you may need to `.clone()` an atom to keep copies to it in different
49/// places, or when passing it to a function that takes an `Atom` rather than an `&Atom`.
50///
51/// ## Creating an atom at runtime
52///
53/// If you use `string_cache_codegen` to generate a precomputed list of atoms, your code
54/// may then do something like read data from somewhere and extract tokens that need to be
55/// compared to the atoms.  In this case, you can use `Atom::from(&str)` or
56/// `Atom::from(String)`.  These create a reference-counted atom which will be
57/// automatically freed when all references to it are dropped.
58///
59/// This means that your application can safely have a loop which tokenizes data, creates
60/// atoms from the tokens, and compares the atoms to a predefined set of keywords, without
61/// running the risk of arbitrary memory consumption from creating large numbers of atoms —
62/// as long as your application does not store clones of the atoms it creates along the
63/// way.
64///
65/// For example, the following is safe and will not consume arbitrary amounts of memory:
66///
67/// ```ignore
68/// let untrusted_data = "large amounts of text ...";
69///
70/// for token in untrusted_data.split_whitespace() {
71///     let atom = Atom::from(token); // interns the string
72///
73///     if atom == Atom::from("keyword") {
74///         // handle that keyword
75///     } else if atom == Atom::from("another_keyword") {
76///         // handle that keyword
77///     } else {
78///         println!("unknown keyword");
79///     }
80/// } // atom is dropped here, so it is not kept around in memory
81/// ```
82///
83/// ## Internal representation
84///
85/// An `Atom` is always 64 bits / 8 bytes.
86/// The least-significant two bits form a tag to distinguish three different representations:
87///
88/// * `0b01`: A short string up to 7 bytes, stored inline in most-significant 56 bits.
89///   Bits #4 to #7 (the upper nibble of the lower byte) are the length of the string.
90/// * `0b10`: A string part of a statically-known indexed set with [perfect hashing].
91///   The most-significant 32 bits are the index in the set.
92/// * `0b00`: For other cases, the entire 64 bits are a heap-allocated pointer
93///   to an entry in a global hash map.
94///   Alignment of the allocation ensures the tag bits are indeed zero.
95///   The entry is atomically reference-counted.
96///   It is removed from the map and deallocated when its last `Atom` is dropped.
97///   The map exists so that interning the same string again gives another pointer to the same entry.
98///
99/// In all cases, shallow 64-bit equality is equivalent to string equality.
100///
101/// [perfect hashing]: https://docs.rs/phf/latest/phf/
102#[derive(PartialEq, Eq)]
103// NOTE: Deriving PartialEq requires that a given string must always be interned the same way.
104pub struct Atom<Static> {
105    unsafe_data: NonZeroU64,
106    phantom: PhantomData<Static>,
107}
108
109/// Static and inline atoms don’t have any allocated space. Dynamic atoms do but
110/// are accounted for by [`malloc_size_of_dynamic_set`][crate::malloc_size_of_dynamic_set].
111#[cfg(feature = "malloc_size_of")]
112impl<Static: StaticAtomSet> malloc_size_of::MallocSizeOf for Atom<Static> {
113    fn size_of(&self, _ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
114        0
115    }
116}
117
118// FIXME: bound removed from the struct definition before of this error for pack_static:
119// "error[E0723]: trait bounds other than `Sized` on const fn parameters are unstable"
120// https://github.com/rust-lang/rust/issues/57563
121impl<Static> Atom<Static> {
122    /// For the atom!() macros
123    #[inline(always)]
124    #[doc(hidden)]
125    pub const fn pack_static(n: u32) -> Self {
126        Self {
127            unsafe_data: unsafe {
128                // STATIC_TAG ensures this is non-zero
129                NonZeroU64::new_unchecked((STATIC_TAG as u64) | ((n as u64) << STATIC_SHIFT_BITS))
130            },
131            phantom: PhantomData,
132        }
133    }
134
135    /// For the atom!() macros
136    #[inline(always)]
137    #[doc(hidden)]
138    pub const fn pack_inline(mut n: u64, len: u8) -> Self {
139        if cfg!(target_endian = "big") {
140            // Reverse order of top 7 bytes.
141            // Bottom 8 bits of `n` are zero, and we need that to remain so.
142            // String data is stored in top 7 bytes, tag and length in bottom byte.
143            n = n.to_le() << 8;
144        }
145
146        let data: u64 = (INLINE_TAG as u64) | ((len as u64) << LEN_OFFSET) | n;
147        Self {
148            // INLINE_TAG ensures this is never zero
149            unsafe_data: unsafe { NonZeroU64::new_unchecked(data) },
150            phantom: PhantomData,
151        }
152    }
153
154    fn tag(&self) -> u8 {
155        (self.unsafe_data.get() & TAG_MASK) as u8
156    }
157
158    /// Assuming DYNAMIC_TAG, return the pointer to `Entry`
159    fn dynamic_ptr(&self) -> *const Entry {
160        std::ptr::with_exposed_provenance(self.unsafe_data.get() as usize)
161    }
162}
163
164impl<Static: StaticAtomSet> Atom<Static> {
165    /// Return the internal representation. For testing.
166    #[doc(hidden)]
167    pub fn unsafe_data(&self) -> u64 {
168        self.unsafe_data.get()
169    }
170
171    /// Return true if this is a static Atom. For testing.
172    #[doc(hidden)]
173    pub fn is_static(&self) -> bool {
174        self.tag() == STATIC_TAG
175    }
176
177    /// Return true if this is a dynamic Atom. For testing.
178    #[doc(hidden)]
179    pub fn is_dynamic(&self) -> bool {
180        self.tag() == DYNAMIC_TAG
181    }
182
183    /// Return true if this is an inline Atom. For testing.
184    #[doc(hidden)]
185    pub fn is_inline(&self) -> bool {
186        self.tag() == INLINE_TAG
187    }
188
189    fn static_index(&self) -> u64 {
190        self.unsafe_data.get() >> STATIC_SHIFT_BITS
191    }
192
193    /// Returns a hash of the string
194    ///
195    /// For static or dynamic atoms, it is a pre-computed high-quality hash.
196    ///
197    /// For inline atoms however (short strings 7 bytes or less),
198    /// the returned value is the literal inline representation
199    /// with string bytes packed directly in the `u64` value,
200    /// which makes it a relatively poor-quality hash if used directly.
201    pub fn get_hash(&self) -> u64 {
202        match self.tag() {
203            DYNAMIC_TAG => {
204                let entry = self.dynamic_ptr();
205                unsafe { (*entry).hash }
206            }
207            STATIC_TAG => Static::get().hashes[self.static_index() as usize],
208            INLINE_TAG => self.unsafe_data.get(),
209            _ => unsafe { debug_unreachable!() },
210        }
211    }
212
213    pub fn try_static(string_to_add: &str) -> Option<Self> {
214        Self::try_static_internal(string_to_add).ok()
215    }
216
217    fn try_static_internal(string_to_add: &str) -> Result<Self, phf_shared::Hashes> {
218        let static_set = Static::get();
219        let hash = phf_shared::hash(string_to_add, &static_set.key);
220        let index = phf_shared::get_index(&hash, static_set.disps, static_set.atoms.len());
221
222        if static_set.atoms[index as usize] == string_to_add {
223            Ok(Self::pack_static(index))
224        } else {
225            Err(hash)
226        }
227    }
228
229    /// Get a reference to the underlying str.
230    #[inline]
231    pub fn as_str(&self) -> &str {
232        self // auto-deref
233    }
234
235    /// Get a reference to the bytes of the underlying str.
236    #[inline]
237    pub fn as_bytes(&self) -> &[u8] {
238        self.as_str().as_bytes()
239    }
240}
241
242impl<Static: StaticAtomSet> Default for Atom<Static> {
243    #[inline]
244    fn default() -> Self {
245        Atom::pack_inline(0, 0)
246    }
247}
248
249impl<Static: StaticAtomSet> Hash for Atom<Static> {
250    #[inline]
251    fn hash<H>(&self, state: &mut H)
252    where
253        H: Hasher,
254    {
255        state.write_u64(self.get_hash())
256    }
257}
258
259impl<'a, Static: StaticAtomSet> From<Cow<'a, str>> for Atom<Static> {
260    fn from(string_to_add: Cow<'a, str>) -> Self {
261        let len = string_to_add.len();
262        if len <= MAX_INLINE_LEN {
263            let mut data: u64 = (INLINE_TAG as u64) | ((len as u64) << LEN_OFFSET);
264            {
265                let dest = inline_atom_slice_mut(&mut data);
266                dest[..len].copy_from_slice(string_to_add.as_bytes());
267            }
268            Atom {
269                // INLINE_TAG ensures this is never zero
270                unsafe_data: unsafe { NonZeroU64::new_unchecked(data) },
271                phantom: PhantomData,
272            }
273        } else {
274            Self::try_static_internal(&string_to_add).unwrap_or_else(|hash| {
275                // Reconstitute 64-bit `Hash128::h1`
276                // https://docs.rs/phf_shared/0.14.0/src/phf_shared/lib.rs.html#45-54
277                let hash = (hash.g as u64) << 32 | (hash.f1 as u64);
278                let ptr: std::ptr::NonNull<Entry> = dynamic_set().insert(string_to_add, hash);
279                let data = ptr.as_ptr().expose_provenance() as u64;
280                debug_assert!(0 == data & TAG_MASK);
281                Atom {
282                    // The address of a ptr::NonNull is non-zero
283                    unsafe_data: unsafe { NonZeroU64::new_unchecked(data) },
284                    phantom: PhantomData,
285                }
286            })
287        }
288    }
289}
290
291impl<Static: StaticAtomSet> Clone for Atom<Static> {
292    #[inline(always)]
293    fn clone(&self) -> Self {
294        if self.tag() == DYNAMIC_TAG {
295            let entry = self.dynamic_ptr();
296            // SAFETY: `self` is a valid Atom, meaning its `unsafe_data` points to a live `Entry`
297            // kept alive by `self`'s reference count. We can safely dereference it.
298            if unsafe { &*entry }.ref_count.fetch_add(1, SeqCst) == isize::MAX {
299                std::process::abort();
300            }
301        }
302        Atom { ..*self }
303    }
304}
305
306impl<Static> Drop for Atom<Static> {
307    #[inline]
308    fn drop(&mut self) {
309        if self.tag() == DYNAMIC_TAG {
310            let entry = self.dynamic_ptr();
311            if unsafe { &*entry }.ref_count.fetch_sub(1, SeqCst) == 1 {
312                drop_slow(self)
313            }
314        }
315
316        // Out of line to guide inlining.
317        #[cold]
318        fn drop_slow<Static>(this: &mut Atom<Static>) {
319            dynamic_set().remove(this.dynamic_ptr().cast_mut());
320        }
321    }
322}
323
324impl<Static: StaticAtomSet> ops::Deref for Atom<Static> {
325    type Target = str;
326
327    #[inline]
328    fn deref(&self) -> &str {
329        unsafe {
330            match self.tag() {
331                DYNAMIC_TAG => {
332                    let entry = self.dynamic_ptr();
333                    &(*entry).string
334                }
335                INLINE_TAG => {
336                    let len = (self.unsafe_data() & LEN_MASK) >> LEN_OFFSET;
337                    debug_assert!(len as usize <= MAX_INLINE_LEN);
338                    let src = inline_atom_slice(&self.unsafe_data);
339                    str::from_utf8_unchecked(src.get_unchecked(..(len as usize)))
340                }
341                STATIC_TAG => Static::get().atoms[self.static_index() as usize],
342                _ => debug_unreachable!(),
343            }
344        }
345    }
346}
347
348impl<Static: StaticAtomSet> fmt::Debug for Atom<Static> {
349    #[inline]
350    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
351        let ty_str = unsafe {
352            match self.tag() {
353                DYNAMIC_TAG => "dynamic",
354                INLINE_TAG => "inline",
355                STATIC_TAG => "static",
356                _ => debug_unreachable!(),
357            }
358        };
359
360        write!(f, "Atom('{}' type={})", self, ty_str)
361    }
362}
363
364impl<Static: StaticAtomSet> PartialOrd for Atom<Static> {
365    #[inline]
366    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
367        Some(self.cmp(other))
368    }
369}
370
371impl<Static: StaticAtomSet> Ord for Atom<Static> {
372    #[inline]
373    fn cmp(&self, other: &Self) -> Ordering {
374        if self.unsafe_data == other.unsafe_data {
375            return Equal;
376        }
377        self.as_str().cmp(other.as_ref())
378    }
379}
380
381// AsciiExt requires mutating methods, so we just implement the non-mutating ones.
382// We don't need to implement is_ascii because there's no performance improvement
383// over the one from &str.
384impl<Static: StaticAtomSet> Atom<Static> {
385    fn from_mutated_str<F: FnOnce(&mut str)>(s: &str, f: F) -> Self {
386        let mut buffer = [const { mem::MaybeUninit::<u8>::uninit() }; 64];
387
388        if let Some(buffer_prefix) = buffer.get_mut(..s.len()) {
389            let buffer_ptr = buffer_prefix.as_mut_ptr().cast::<u8>();
390            // SAFETY: `buffer_ptr` points to the `MaybeUninit` array.
391            // We use `copy_nonoverlapping` to write valid data into it,
392            // and then create a slice covering ONLY the initialized portion.
393            let as_str = unsafe {
394                buffer_ptr.copy_from_nonoverlapping(s.as_ptr(), s.len());
395                let buffer_slice = slice::from_raw_parts_mut(buffer_ptr, s.len());
396                std::str::from_utf8_unchecked_mut(buffer_slice)
397            };
398            f(as_str);
399            Atom::from(&*as_str)
400        } else {
401            let mut string = s.to_owned();
402            f(&mut string);
403            Atom::from(string)
404        }
405    }
406
407    /// Like [`to_ascii_uppercase`].
408    ///
409    /// [`to_ascii_uppercase`]: https://doc.rust-lang.org/std/ascii/trait.AsciiExt.html#tymethod.to_ascii_uppercase
410    pub fn to_ascii_uppercase(&self) -> Self {
411        for (i, b) in self.bytes().enumerate() {
412            if let b'a'..=b'z' = b {
413                return Atom::from_mutated_str(self, |s| s[i..].make_ascii_uppercase());
414            }
415        }
416        self.clone()
417    }
418
419    /// Like [`to_ascii_lowercase`].
420    ///
421    /// [`to_ascii_lowercase`]: https://doc.rust-lang.org/std/ascii/trait.AsciiExt.html#tymethod.to_ascii_lowercase
422    pub fn to_ascii_lowercase(&self) -> Self {
423        for (i, b) in self.bytes().enumerate() {
424            if let b'A'..=b'Z' = b {
425                return Atom::from_mutated_str(self, |s| s[i..].make_ascii_lowercase());
426            }
427        }
428        self.clone()
429    }
430
431    /// Like [`eq_ignore_ascii_case`].
432    ///
433    /// [`eq_ignore_ascii_case`]: https://doc.rust-lang.org/std/ascii/trait.AsciiExt.html#tymethod.eq_ignore_ascii_case
434    pub fn eq_ignore_ascii_case(&self, other: &Self) -> bool {
435        (self == other) || self.eq_str_ignore_ascii_case(other)
436    }
437
438    /// Like [`eq_ignore_ascii_case`], but takes an unhashed string as `other`.
439    ///
440    /// [`eq_ignore_ascii_case`]: https://doc.rust-lang.org/std/ascii/trait.AsciiExt.html#tymethod.eq_ignore_ascii_case
441    pub fn eq_str_ignore_ascii_case(&self, other: &str) -> bool {
442        self.as_str().eq_ignore_ascii_case(other)
443    }
444}
445
446#[inline(always)]
447fn inline_atom_slice(x: &NonZeroU64) -> &[u8] {
448    let x: *const NonZeroU64 = x;
449    let mut data = x as *const u8;
450    // All except the lowest byte, which is first in little-endian, last in big-endian.
451    if cfg!(target_endian = "little") {
452        data = unsafe { data.offset(1) };
453    }
454    let len = 7;
455    unsafe { slice::from_raw_parts(data, len) }
456}
457
458#[inline(always)]
459fn inline_atom_slice_mut(x: &mut u64) -> &mut [u8] {
460    let x: *mut u64 = x;
461    let mut data = x as *mut u8;
462    // All except the lowest byte, which is first in little-endian, last in big-endian.
463    if cfg!(target_endian = "little") {
464        data = unsafe { data.offset(1) };
465    }
466    let len = 7;
467    unsafe { slice::from_raw_parts_mut(data, len) }
468}