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}