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}