generic_static_cache/
lib.rs

1#![feature(asm_const)]
2#![cfg_attr(
3    not(any(target_arch = "x86_64", target_arch = "aarch64")),
4    feature(const_collections_with_hasher)
5)]
6// TODO: Properly test data for same type from different compilation units
7// TODO: More platforms
8// TODO: Benches
9
10//! Quoting the [Rust Reference](https://doc.rust-lang.org/reference/items/static-items.html):
11//!
12//! "A static item defined in a generic scope (for example in a blanket or default implementation)
13//! will result in exactly one static item being defined, as if the static definition was pulled
14//! out of the current scope into the module. There will not be one item per monomorphization."
15//!
16//! One way to work around this is to use a `HashMap<TypeId,Data>`. This is a simple & usually the best solution.
17//! If lookup performance is important, you can skip hashing the `TypeId` for minor gains as it
18//! [already contains](https://github.com/rust-lang/rust/blob/eeff92ad32c2627876112ccfe812e19d38494087/library/core/src/any.rs#L645)
19//! a good-quality hash. This is implemented in `TypeIdMap`.
20//!
21//! This crate aims to further fully remove the lookup by allocating the storage using inline
22//! assembly.
23//!
24//! Supported targets are **x86-64** and **aarch64**. On other targets, the `generic_static` macro
25//! falls back to a hashmap and most other functionality is unavailable.
26//!
27//! Additionally, different compilation units may access different instances of the data!
28//!
29//! This crate requires the following unstable features: `asm_const` and
30//! (on unsupported targets) `const_collections_with_hasher`.
31//!
32//! # Examples
33//! Static variables in a generic context:
34//! ```
35//! #![feature(const_collections_with_hasher)]
36//! # use std::sync::atomic::{AtomicI32, Ordering};
37//! # use generic_static_cache::generic_static;
38//! fn get_and_inc<T>() -> i32 {
39//!     generic_static!{
40//!         static COUNTER: &AtomicI32 = &AtomicI32::new(1);
41//!     }
42//!     COUNTER.fetch_add(1, Ordering::Relaxed)
43//! }
44//! assert_eq!(get_and_inc::<bool>(), 1);
45//! assert_eq!(get_and_inc::<bool>(), 2);
46//! assert_eq!(get_and_inc::<String>(), 1);
47//! assert_eq!(get_and_inc::<bool>(), 3);
48//! ```
49//! Associating data with a type:
50//! ```
51//! # #[derive(Debug)]
52//! ##[derive(Copy, Clone, Eq, PartialEq)]
53//! struct Metadata(&'static str);
54//!
55//! struct Cat;
56//! struct Bomb;
57//!
58//! use generic_static_cache::{get, init};
59//! init::<Cat, _>(Metadata("nya!")).unwrap();
60//! init::<Bomb, _>(Metadata("boom!")).unwrap();
61//!
62//! assert_eq!(get::<Cat, _>(), Some(Metadata("nya!")));
63//! assert_eq!(get::<Bomb, _>(), Some(Metadata("boom!")));
64//! ```
65
66use std::any::TypeId;
67use std::collections::HashMap;
68use std::hash::{BuildHasher, Hasher};
69use std::sync::atomic::AtomicPtr;
70
71use bytemuck::Zeroable;
72
73#[derive(Debug)]
74pub struct AlreadyInitialized;
75
76/// Wrapper to prevent interfering with the user's `direct` calls
77struct Heap<T>(AtomicPtr<T>);
78
79unsafe impl<T> Zeroable for Heap<T> {}
80
81/// Initialize the `Data`-storage of type `Type`.
82/// Each `Type` can hold data for multiple different instantiations of `Data`.
83///
84/// If called multiple times, only the first call will succeed.
85///
86/// Only available on supported targets.
87#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
88pub fn init<Type: 'static, Data: Copy + 'static>(data: Data) -> Result<(), AlreadyInitialized> {
89    use std::sync::atomic::Ordering;
90    let boxed = Box::into_raw(Box::new(data));
91    match direct::<Type, Heap<Data>>().0.compare_exchange(
92        std::ptr::null_mut(),
93        boxed,
94        Ordering::SeqCst,
95        Ordering::SeqCst,
96    ) {
97        Ok(_) => Ok(()),
98        Err(_) => {
99            unsafe {
100                drop(Box::from_raw(boxed));
101            }
102            Err(AlreadyInitialized)
103        }
104    }
105}
106
107/// Access the `Data`-storage of type `Type`.
108/// Each `Type` can hold data for multiple different instantiations of `Data`.
109///
110/// Only available on supported targets.
111#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
112pub fn get<Type: 'static, Data: Copy + 'static>() -> Option<Data> {
113    use std::sync::atomic::Ordering;
114    let data = direct::<Type, Heap<Data>>().0.load(Ordering::SeqCst);
115    if data.is_null() {
116        None
117    } else {
118        Some(unsafe { *data })
119    }
120}
121
122/// Initialize & access the `Data`-storage of type `Type`.
123/// Each `Type` can hold data for multiple different instantiations of `Data`.
124///
125/// Only available on supported targets.
126#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
127pub fn get_or_init<Type: 'static, Data: Copy + 'static>(cons: impl Fn() -> Data) -> Data {
128    use std::sync::atomic::Ordering;
129    let data = direct::<Type, Heap<Data>>().0.load(Ordering::SeqCst);
130    if data.is_null() {
131        let _ = init::<Type, _>(cons());
132        get::<Type, _>().unwrap()
133    } else {
134        unsafe { *data }
135    }
136}
137
138/// Declare a static variable that is not shared across different monomorphizations
139/// of the containing functions. Its type must be a shared reference to a type
140/// that implements Sync.
141///
142/// If this is executed for the first time in multiple threads simultaneously,
143/// the initializing expression may get executed multiple times.
144///
145/// On unsupported targets, this falls back to a hashmap and generic types
146/// from outer items may not be used.
147///
148/// On supported targets, the type annotation can be ommited.
149///
150/// # Example
151/// ```
152/// # use std::sync::Mutex;
153/// # use generic_static_cache::generic_static;
154/// generic_static!{
155///     static NAME: &Mutex<String> = &Mutex::new("Ferris".to_string());
156/// }
157/// ```
158#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
159#[macro_export]
160macro_rules! generic_static {
161    {static $ident:ident $(: &$type:ty)? = &$init:expr;} => {
162        #[allow(non_snake_case)]
163        let $ident $(: &'static $type)? = {
164            let init = ||$init;
165            fn assert_sync<T: Sync>(_: &impl FnOnce() -> T) {}
166            assert_sync(&init);
167
168            // Use use empty closure to create a new type to use as a unique key,
169            // use reference to initializer to infer type of static data
170            fn make<Key: 'static, Value>(_: Key, _: &impl FnOnce()->Value)
171            -> &'static std::sync::atomic::AtomicPtr<Value> {
172                $crate::direct::<Key, std::sync::atomic::AtomicPtr<Value>>()
173            }
174            let ptr = make(||(), &init);
175
176            let data = ptr.load(std::sync::atomic::Ordering::SeqCst);
177            if data.is_null() {
178                // Need to call initializer
179                // This can be called multiple times if executed for the first time
180                // in multiple threads simultaneously!
181                let boxed = Box::into_raw(Box::new(init()));
182                if ptr
183                    .compare_exchange(
184                        std::ptr::null_mut(),
185                        boxed,
186                        std::sync::atomic::Ordering::SeqCst,
187                        std::sync::atomic::Ordering::SeqCst,
188                    )
189                    .is_err()
190                {
191                    // Was simultaneously initialized by another thread
192                    unsafe {
193                        drop(Box::from_raw(boxed));
194                    }
195                }
196                unsafe { &*boxed }
197            } else {
198                unsafe { &*data }
199            }
200        };
201    };
202}
203
204/// Declare a static variable that is not shared across different monomorphizations
205/// of the containing functions. Its type must be a shared reference to a type
206/// that implements Sync.
207///
208/// If this is executed for the first time in multiple threads simultaneously,
209/// the initializing expression may get executed multiple times.
210///
211/// On unsupported targets, this falls back to a hashmap.
212///
213/// On supported targets, the type annotation can be ommited.
214///
215/// # Example
216/// ```
217/// # use std::sync::Mutex;
218/// # use generic_static_cache::generic_static;
219/// generic_static!{
220///     static NAME: &Mutex<String> = &Mutex::new("Ferris".to_string());
221/// }
222/// ```
223#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
224#[macro_export]
225macro_rules! generic_static {
226    {static $ident:ident: &$type:ty = &$init:expr;} => {
227        #[allow(non_snake_case)]
228        let $ident: &'static $type = {
229            static MAP: std::sync::Mutex<$crate::TypeIdMap<&'static $type>> =
230                std::sync::Mutex::new($crate::TypeIdMap::with_hasher(
231                    $crate::NoOpTypeIdBuildHasher,
232                ));
233            MAP.lock()
234                .unwrap()
235                .entry({
236                    fn id<T: 'static>(_: T) -> std::any::TypeId {
237                        std::any::TypeId::of::<T>()
238                    }
239                    id(||())
240                })
241                .or_insert_with(|| Box::leak(Box::new((|| $init)())))
242        };
243    };
244}
245
246trait Carrier: 'static {
247    unsafe fn storage(&self) -> TypeId;
248}
249
250impl<T: 'static, D: Zeroable + Sync + 'static> Carrier for (T, D) {
251    /// THIS PLACE IS NOT A PLACE OF HONOR.  
252    /// NO HIGHLY ESTEEMED DEED IS COMMEMORATED HERE.  
253    /// NOTHING VALUED IS HERE.  
254    /// WHAT IS HERE WAS DANGEROUS AND REPULSIVE TO US.  
255    /// THE DANGE IS IN A PARTICULAR LOCATION.  
256    /// THE DANGER IS STILL PRESENT, IN YOUR TIME, AS IT WAS IN OURS.  
257    ///
258    /// The useage of sym seems to prevent rustc from optimizing this out
259    /// in release mode, even though it's never called.
260    /// The inline is does the same, but for release mode.
261    ///
262    /// Why? *shrug* Only ð’€­ð’‚—ð’† , the god of knowledge, knows
263    /// and even he needs a minute to figure this out.
264    #[inline(always)]
265    unsafe fn storage(&self) -> TypeId {
266        std::arch::asm!(
267            ".pushsection .data",
268            // TODO: Check at what align the .data section gets loaded,
269            // ensure align is at most that large.
270            // I believe llvm uses 4kb by default (probably more if larger pages are enabled),
271            // but users could mess with the linker script, so add a note to the README?
272            ".balign {align}",
273            "type_data_{id}:",
274            ".skip {size}",
275            ".popsection",
276            id = sym <(T, D) as Carrier>::storage,
277            align = const std::mem::align_of::<D>(),
278            size = const std::mem::size_of::<D>(),
279            options(nomem)
280        );
281        TypeId::of::<(T, D)>()
282    }
283}
284
285/// Access data associated with Type without indirection. Requires interior mutability to be useful.
286/// This data is independent of the data accessed via [`get`]/[`init`]/[`get_or_init`].
287///
288/// Only available on supported targets.
289#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
290pub fn direct<Type: 'static, Data: Zeroable + Sync + 'static>() -> &'static Data {
291    unsafe {
292        // Tested both with position-independent code and with -C relocation-model=static
293        let addr: usize;
294        #[cfg(target_arch = "x86_64")]
295        {
296            std::arch::asm!(
297                "lea {addr}, [rip+type_data_{id}]",
298                addr = out(reg) addr,
299                id = sym <(Type, Data) as Carrier>::storage,
300                options(pure, nomem)
301            );
302        }
303        #[cfg(target_arch = "aarch64")]
304        {
305            atd::arch::asm!(
306                "adrp {addr}, type_data_{id}",
307                "add {addr}, {addr}, :lo12:type_data_{id}",
308                addr = out(reg) addr,
309                id = sym <(Type, Data) as Carrier>::storage,
310                options(pure, nomem)
311            );
312        }
313        &*(addr as *const _)
314    }
315}
316
317/// Fast type map suitable for all platforms.
318pub type TypeIdMap<T> = HashMap<TypeId, T, NoOpTypeIdBuildHasher>;
319
320#[doc(hidden)]
321#[derive(Default)]
322pub struct NoOpTypeIdBuildHasher;
323
324impl BuildHasher for NoOpTypeIdBuildHasher {
325    type Hasher = NoOpTypeIdHasher;
326
327    fn build_hasher(&self) -> Self::Hasher {
328        NoOpTypeIdHasher(0)
329    }
330}
331
332#[doc(hidden)]
333#[derive(Default)]
334pub struct NoOpTypeIdHasher(u64);
335
336impl Hasher for NoOpTypeIdHasher {
337    fn finish(&self) -> u64 {
338        self.0
339    }
340
341    fn write(&mut self, _bytes: &[u8]) {
342        unimplemented!()
343    }
344
345    fn write_u64(&mut self, i: u64) {
346        self.0 = i
347    }
348}
349
350#[cfg(test)]
351#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
352#[test]
353fn test_heapless() {
354    use crate::direct;
355    use std::sync::atomic::{AtomicI64, Ordering};
356
357    let a = direct::<Option<bool>, AtomicI64>();
358    let b = direct::<Option<()>, AtomicI64>();
359    assert_eq!(a.load(Ordering::Relaxed), 0);
360    a.store(69, Ordering::Relaxed);
361    assert_eq!(a.load(Ordering::Relaxed), 69);
362    assert_eq!(b.load(Ordering::Relaxed), 0);
363    assert_eq!(*direct::<Option<()>, i64>(), 0);
364
365    std::hint::black_box(direct::<Option<bool>, AtomicI64>());
366}
367
368#[cfg(test)]
369#[test]
370fn test_macro() {
371    use std::sync::atomic::{AtomicI32, Ordering};
372    fn get_and_inc<T: 'static>() -> i32 {
373        generic_static!(
374            static BLUB: &AtomicI32 = &AtomicI32::new(1);
375        );
376        let value = BLUB.load(Ordering::Relaxed);
377        BLUB.fetch_add(1, Ordering::Relaxed);
378        value
379    }
380    assert_eq!(get_and_inc::<bool>(), 1);
381    assert_eq!(get_and_inc::<bool>(), 2);
382    assert_eq!(get_and_inc::<String>(), 1);
383    assert_eq!(get_and_inc::<bool>(), 3);
384
385    generic_static!(
386        static FOO_1: &AtomicI32 = &AtomicI32::new(0);
387    );
388    generic_static!(
389        static FOO_2: &AtomicI32 = &AtomicI32::new(69);
390    );
391    assert_eq!(FOO_1.load(Ordering::Relaxed), 0);
392    assert_eq!(FOO_2.load(Ordering::Relaxed), 69);
393    FOO_1.store(1, Ordering::Relaxed);
394    FOO_2.store(2, Ordering::Relaxed);
395    assert_eq!(FOO_1.load(Ordering::Relaxed), 1);
396    assert_eq!(FOO_2.load(Ordering::Relaxed), 2);
397}
398
399#[cfg(test)]
400#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
401#[test]
402fn test_macro_type_inference() {
403    generic_static! {
404        static _FOO = &();
405    }
406}