generic_statics/
lib.rs

1#![feature(asm_const)]
2
3//! A "workaround" for missing generic statics in Rust.
4//!
5//! **This crate is experimental and might not be fully sound. Use at your own risk.**
6//!
7//! The core functionality is provided by [`Namespace::generic_static`].
8//!
9//! ```rust
10//! use std::sync::atomic::{AtomicPtr, Ordering};
11//! use generic_statics::{define_namespace, Namespace};
12//!
13//! define_namespace!(Test);
14//!
15//! let a = Test::generic_static::<AtomicPtr<usize>>().load(Ordering::Relaxed);
16//! ```
17//!
18//! ## Caveats and Limitations
19//!
20//! This crate is nightly only and relies on `#![feature(asm_const)]`.
21//!
22//! The generic statics provided by this crate use static allocation
23//! (i.e. no dynamic allocation will occur at runtime) and is almost zero-cost
24//! (aside from some inline asm instructions for computing the static address).
25//!
26//! However, this crate is **only best-effort** (i.e. best-effort stable addresses):
27//!
28//! ```rust
29//! use generic_statics::{define_namespace, Namespace};
30//!
31//! define_namespace!(Test);
32//!
33//! // This is *not* guaranteed, but in usual cases this will work just fine.
34//! assert_eq!(
35//!     Test::generic_static::<usize>() as *const _,
36//!     Test::generic_static::<usize>() as *const _
37//! );
38//! ```
39//!
40//! The used approach relies on inline assembly to instantiate/reserve static data for each
41//! monomorphized variant of the function.
42//! Unfortunately inlining will return a different version of the data and thus will not return
43//! stable addresses.
44//! However, [`Namespace::generic_static`] is marked `#[inline(never)]` which should provide stable
45//! addresses in most situations
46//! (Note that `#[inline(never)]` is just a hint to the compiler and doesn't guarantee anything).
47//!
48//! Only "zeroable" types are allowed for now due to inline asm restrictions.
49//!
50//! This crate only supports these targets for now:
51//!
52//! - macOS `x86_64`, `aarch64`
53//! - Linux `x86_64`, `aarch64`
54//! - FreeBSD `x86_64`, `aarch64`
55//! - Windows `x86_64`
56//!
57
58mod zeroable;
59
60use std::{any::TypeId, mem, ptr};
61
62pub use zeroable::Zeroable;
63
64const fn cmp_max(a: usize, b: usize) -> usize {
65    if a > b {
66        a
67    } else {
68        b
69    }
70}
71
72/// A namespace for generic statics.
73///
74/// # Safety
75///
76/// Implementing this trait is not unsafe per-se but you should use the [`define_namespace`]
77/// instead.
78pub unsafe trait Namespace: 'static + Send + Sync + Copy + Clone {
79    /// The returned reference points to the static namespaced global variable for each
80    /// generic `T` (but are lifetime erased). The static's value is zero-initialized.
81    ///
82    /// For caveats and limitations, refer to [top-module](crate#caveats-and-limitations).
83    #[inline(never)]
84    #[must_use]
85    fn generic_static<T: 'static + Zeroable>() -> &'static T {
86        #[allow(unused_assignments)]
87        let mut addr: *const () = ptr::null();
88
89        // HACK: We have to "use" the generic `T` in some way to force the compiler to emit every
90        // instatiation of this function, otherwise rustc might be smart and merge instantiations.
91        let type_id = TypeId::of::<(Self, T)> as *const ();
92
93        #[cfg(all(
94            target_arch = "aarch64",
95            any(target_os = "macos", target_os = "ios", target_os = "tvos")
96        ))]
97        unsafe {
98            std::arch::asm!(
99                "/* {type_id} */",
100                "adrp {x}, 1f@PAGE",
101                "add {x}, {x}, 1f@PAGEOFF",
102                ".pushsection __DATA,__data",
103                ".p2align {align}, 0",
104                "1: .zero {size}",
105                ".popsection",
106                size = const { cmp_max(mem::size_of::<T>(), 1) },
107                align = const { mem::align_of::<T>().ilog2() },
108                type_id = in(reg) type_id,
109                x = out(reg) addr,
110                options(nostack)
111            );
112        }
113
114        #[cfg(all(
115            target_arch = "aarch64",
116            any(target_os = "none", target_os = "linux", target_os = "freebsd")
117        ))]
118        unsafe {
119            std::arch::asm!(
120                "/* {type_id} */",
121                "adrp {x}, 1f",
122                "add {x}, {x}, :lo12:1f",
123                ".pushsection .bss.generic_statics,\"aw\",@nobits",
124                ".p2align {align}, 0",
125                "1: .zero {size}",
126                ".popsection",
127                size = const { cmp_max(mem::size_of::<T>(), 1) },
128                align = const { mem::align_of::<T>().ilog2() },
129                type_id = in(reg) type_id,
130                x = out(reg) addr,
131                options(nostack)
132            );
133        }
134
135        #[cfg(all(
136            target_arch = "x86_64",
137            any(target_os = "macos", target_os = "ios", target_os = "tvos")
138        ))]
139        unsafe {
140            std::arch::asm!(
141                "/* {type_id} */",
142                "lea {x}, [rip + 1f]",
143                ".pushsection __DATA,__data",
144                ".p2align {align}, 0",
145                "1: .zero {size}",
146                ".popsection",
147                size = const { cmp_max(mem::size_of::<T>(), 1) },
148                align = const { mem::align_of::<T>().ilog2() },
149                type_id = in(reg) type_id,
150                x = out(reg) addr,
151                options(nostack)
152            );
153        }
154
155        #[cfg(all(
156            target_arch = "x86_64",
157            any(target_os = "none", target_os = "linux", target_os = "freebsd")
158        ))]
159        unsafe {
160            std::arch::asm!(
161                "/* {type_id} */",
162                "lea {x}, [rip + 1f]",
163                ".pushsection .bss.generic_statics,\"aw\",@nobits",
164                ".p2align {align}, 0",
165                "1: .zero {size}",
166                ".popsection",
167                size = const { cmp_max(mem::size_of::<T>(), 1) },
168                align = const { mem::align_of::<T>().ilog2() },
169                type_id = in(reg) type_id,
170                x = out(reg) addr,
171                options(nostack)
172            );
173        }
174
175        #[cfg(all(target_arch = "x86_64", target_os = "windows"))]
176        unsafe {
177            std::arch::asm!(
178                "/* {type_id} */",
179                "lea {x}, [rip + 1f]",
180                ".pushsection .bss.generic_statics,\"bw\"",
181                ".p2align {align}, 0",
182                "1: .zero {size}",
183                ".popsection",
184                size = const { cmp_max(mem::size_of::<T>(), 1) },
185                align = const { mem::align_of::<T>().ilog2() },
186                type_id = in(reg) type_id,
187                x = out(reg) addr,
188                options(nostack)
189            );
190        }
191
192        #[cfg(not(any(
193            target_os = "none",
194            target_os = "linux",
195            target_os = "freebsd",
196            target_os = "macos",
197            target_os = "ios",
198            target_os = "tvos",
199            target_os = "windows",
200        )))]
201        std::compile_error!("static-generics is not supported on this platform");
202
203        assert!(!addr.is_null(), "unsupported platform");
204
205        unsafe { &*addr.cast::<T>() }
206    }
207}
208
209#[macro_export]
210macro_rules! define_namespace {
211    ($vis:vis $name:ident) => {
212        #[derive(Debug, Copy, Clone)]
213        $vis struct $name;
214
215        unsafe impl $crate::Namespace for $name {}
216    };
217}
218
219#[cfg(test)]
220mod tests {
221    use std::{
222        assert_ne,
223        marker::PhantomData,
224        sync::atomic::{AtomicIsize, AtomicPtr, AtomicUsize, Ordering},
225    };
226
227    use super::Namespace;
228
229    define_namespace!(pub Test);
230
231    #[test]
232    fn stable_addr() {
233        let a = Test::generic_static::<*const ()>() as *const _;
234        let b = Test::generic_static::<*const ()>() as *const _;
235        assert_eq!(a, b);
236
237        let d = Test::generic_static::<(AtomicUsize, AtomicUsize, AtomicUsize)>() as *const _;
238        let e = Test::generic_static::<(AtomicUsize, AtomicUsize, AtomicUsize)>() as *const _;
239        assert_eq!(d, e);
240
241        assert_ne!(a as *const (), d as *const _ as *const ());
242    }
243
244    #[test]
245    fn unique_address() {
246        let a = Test::generic_static::<AtomicUsize>() as *const _ as *const ();
247        let b = Test::generic_static::<AtomicIsize>() as *const _ as *const ();
248        let c = Test::generic_static::<usize>() as *const _ as *const ();
249        let d = Test::generic_static::<AtomicPtr<()>>() as *const _ as *const ();
250
251        assert_ne!(a, b);
252        assert_ne!(a, c);
253        assert_ne!(a, d);
254        assert_ne!(b, c);
255        assert_ne!(b, d);
256        assert_ne!(c, d);
257    }
258
259    #[test]
260    fn unique_address_dyn() {
261        trait Foo<A: 'static> {}
262
263        let a = Test::generic_static::<PhantomData<dyn Foo<usize>>>() as *const _ as *const ();
264        let b = Test::generic_static::<PhantomData<dyn Foo<isize>>>() as *const _ as *const ();
265        let c = Test::generic_static::<PhantomData<dyn Foo<()>>>() as *const _ as *const ();
266
267        assert_ne!(a, b);
268        assert_ne!(a, c);
269        assert_ne!(b, c);
270    }
271
272    #[test]
273    fn mutation() {
274        let a = Test::generic_static::<AtomicUsize>();
275        assert_eq!(a.load(Ordering::Relaxed), 0);
276        a.store(42, Ordering::Relaxed);
277
278        let b = Test::generic_static::<AtomicUsize>();
279        assert_eq!(b.load(Ordering::Relaxed), 42);
280
281        let a2 = Test::generic_static::<AtomicIsize>();
282        assert_eq!(a2.load(Ordering::Relaxed), 0);
283    }
284}