Skip to main content

ext_php_rs/zend/
module_globals.rs

1use std::cell::UnsafeCell;
2use std::marker::PhantomData;
3#[cfg_attr(php_zts, allow(unused_imports))]
4use std::mem::MaybeUninit;
5
6/// Trait for types used as PHP module globals.
7///
8/// Requires [`Default`] for initialization. Override [`ginit`](ModuleGlobal::ginit)
9/// and [`gshutdown`](ModuleGlobal::gshutdown) for custom per-thread (ZTS) or
10/// per-module (non-ZTS) lifecycle logic.
11///
12/// # Examples
13///
14/// ```
15/// use ext_php_rs::zend::ModuleGlobal;
16///
17/// #[derive(Default)]
18/// struct MyGlobals {
19///     request_count: i64,
20///     max_depth: i32,
21/// }
22///
23/// impl ModuleGlobal for MyGlobals {
24///     fn ginit(&mut self) {
25///         self.max_depth = 512;
26///     }
27/// }
28/// ```
29pub trait ModuleGlobal: Default + 'static {
30    /// Called after the struct is initialized with [`Default::default()`].
31    ///
32    /// Use for setup that goes beyond what `Default` can express.
33    /// In ZTS mode, called once per thread. In non-ZTS mode, called once at module init.
34    fn ginit(&mut self) {}
35
36    /// Called before the struct is dropped.
37    ///
38    /// Use for cleanup of external resources.
39    /// In ZTS mode, called once per thread. In non-ZTS mode, called once at module shutdown.
40    fn gshutdown(&mut self) {}
41}
42
43unsafe extern "C" {
44    #[cfg(php_zts)]
45    fn ext_php_rs_tsrmg_bulk(id: i32) -> *mut std::ffi::c_void;
46}
47
48/// Thread-safe handle to PHP module globals.
49///
50/// Declare as a `static` and pass to [`ModuleBuilder::globals()`](crate::builders::ModuleBuilder::globals).
51///
52/// In ZTS (thread-safe) builds, PHP's TSRM allocates per-thread storage and
53/// manages the lifetime via GINIT/GSHUTDOWN callbacks. In non-ZTS builds, the
54/// globals live inline in this struct as a plain static.
55///
56/// # Examples
57///
58/// ```
59/// use ext_php_rs::zend::{ModuleGlobal, ModuleGlobals};
60///
61/// #[derive(Default)]
62/// struct MyGlobals {
63///     counter: i64,
64/// }
65///
66/// impl ModuleGlobal for MyGlobals {}
67///
68/// static MY_GLOBALS: ModuleGlobals<MyGlobals> = ModuleGlobals::new();
69/// ```
70pub struct ModuleGlobals<T: ModuleGlobal> {
71    #[cfg(php_zts)]
72    id: UnsafeCell<i32>,
73    #[cfg(not(php_zts))]
74    inner: UnsafeCell<MaybeUninit<T>>,
75    _marker: PhantomData<T>,
76}
77
78// SAFETY: In ZTS mode, TSRM guarantees per-thread access. The `id` field is
79// only written once during single-threaded module init (MINIT).
80// In non-ZTS mode, PHP is single-threaded.
81unsafe impl<T: ModuleGlobal> Sync for ModuleGlobals<T> {}
82
83impl<T: ModuleGlobal> Default for ModuleGlobals<T> {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl<T: ModuleGlobal> ModuleGlobals<T> {
90    /// Creates an uninitialized globals handle.
91    ///
92    /// Must be passed to [`ModuleBuilder::globals()`](crate::builders::ModuleBuilder::globals)
93    /// for PHP to allocate and initialize the storage.
94    #[must_use]
95    pub const fn new() -> Self {
96        Self {
97            #[cfg(php_zts)]
98            id: UnsafeCell::new(0),
99            #[cfg(not(php_zts))]
100            inner: UnsafeCell::new(MaybeUninit::uninit()),
101            _marker: PhantomData,
102        }
103    }
104
105    /// Returns a shared reference to the current thread's globals.
106    ///
107    /// Safe because PHP guarantees single-threaded request processing:
108    /// only one request handler runs per thread at a time, and module
109    /// globals are initialized before any request begins.
110    ///
111    /// # Panics
112    ///
113    /// Debug-asserts that the globals have been registered. In release builds,
114    /// calling this before module init is undefined behavior.
115    pub fn get(&self) -> &T {
116        unsafe {
117            #[cfg(php_zts)]
118            {
119                let id = *self.id.get();
120                debug_assert!(id != 0, "ModuleGlobals accessed before registration");
121                &*ext_php_rs_tsrmg_bulk(id).cast::<T>()
122            }
123            #[cfg(not(php_zts))]
124            {
125                (*self.inner.get()).assume_init_ref()
126            }
127        }
128    }
129
130    /// Returns a mutable reference to the current thread's globals.
131    ///
132    /// # Safety
133    ///
134    /// Caller must ensure exclusive access. Typically safe within `RINIT`/`RSHUTDOWN`
135    /// or from a `#[php_function]` handler (PHP runs one request per thread), but
136    /// NOT from background Rust threads.
137    #[allow(clippy::mut_from_ref)]
138    pub unsafe fn get_mut(&self) -> &mut T {
139        #[cfg(php_zts)]
140        unsafe {
141            let id = *self.id.get();
142            debug_assert!(id != 0, "ModuleGlobals accessed before registration");
143            &mut *ext_php_rs_tsrmg_bulk(id).cast::<T>()
144        }
145        #[cfg(not(php_zts))]
146        unsafe {
147            (*self.inner.get()).assume_init_mut()
148        }
149    }
150
151    /// Returns a raw pointer to the globals for the current thread.
152    ///
153    /// Escape hatch for power users who need direct access without
154    /// lifetime constraints.
155    pub fn as_ptr(&self) -> *mut T {
156        unsafe {
157            #[cfg(php_zts)]
158            {
159                ext_php_rs_tsrmg_bulk(*self.id.get()).cast::<T>()
160            }
161            #[cfg(not(php_zts))]
162            {
163                (*self.inner.get()).as_mut_ptr()
164            }
165        }
166    }
167
168    /// Returns a pointer to the internal ID storage (ZTS) or data storage (non-ZTS).
169    ///
170    /// Used by [`ModuleBuilder::globals()`](crate::builders::ModuleBuilder::globals)
171    /// to wire up the `zend_module_entry`.
172    #[cfg(php_zts)]
173    pub(crate) fn id_ptr(&self) -> *mut i32 {
174        self.id.get()
175    }
176
177    /// Returns a pointer to the internal storage.
178    ///
179    /// Used by [`ModuleBuilder::globals()`](crate::builders::ModuleBuilder::globals)
180    /// to wire up the `zend_module_entry`.
181    #[cfg(not(php_zts))]
182    pub(crate) fn data_ptr(&self) -> *mut std::ffi::c_void {
183        self.inner.get().cast()
184    }
185}
186
187/// GINIT callback invoked by PHP per-thread (ZTS) or once (non-ZTS).
188///
189/// # Safety
190///
191/// `globals` must point to uninitialized memory of at least `size_of::<T>()` bytes.
192/// Called by PHP's module initialization machinery.
193pub(crate) unsafe extern "C" fn ginit_callback<T: ModuleGlobal>(globals: *mut std::ffi::c_void) {
194    unsafe {
195        let ptr = globals.cast::<T>();
196        ptr.write(T::default());
197        (*ptr).ginit();
198    }
199}
200
201/// GSHUTDOWN callback invoked by PHP before freeing globals memory.
202///
203/// # Safety
204///
205/// `globals` must point to a valid, initialized `T`.
206/// Called by PHP's module shutdown machinery.
207pub(crate) unsafe extern "C" fn gshutdown_callback<T: ModuleGlobal>(
208    globals: *mut std::ffi::c_void,
209) {
210    unsafe {
211        let ptr = globals.cast::<T>();
212        (*ptr).gshutdown();
213        std::ptr::drop_in_place(ptr);
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[derive(Default)]
222    struct TestGlobals {
223        value: i32,
224        initialized: bool,
225    }
226
227    impl ModuleGlobal for TestGlobals {
228        fn ginit(&mut self) {
229            self.initialized = true;
230            self.value = 42;
231        }
232
233        fn gshutdown(&mut self) {
234            self.initialized = false;
235        }
236    }
237
238    #[test]
239    fn new_is_const() {
240        static _G: ModuleGlobals<TestGlobals> = ModuleGlobals::new();
241    }
242
243    #[test]
244    fn ginit_callback_initializes() {
245        let mut storage = MaybeUninit::<TestGlobals>::uninit();
246        unsafe {
247            ginit_callback::<TestGlobals>(storage.as_mut_ptr().cast());
248            let globals = storage.assume_init_ref();
249            assert!(globals.initialized);
250            assert_eq!(globals.value, 42);
251            std::ptr::drop_in_place(storage.as_mut_ptr());
252        }
253    }
254
255    #[test]
256    fn gshutdown_callback_cleans_up() {
257        let mut storage = MaybeUninit::<TestGlobals>::uninit();
258        unsafe {
259            ginit_callback::<TestGlobals>(storage.as_mut_ptr().cast());
260            gshutdown_callback::<TestGlobals>(storage.as_mut_ptr().cast());
261        }
262    }
263
264    #[test]
265    #[cfg(not(php_zts))]
266    fn non_zts_get_after_init() {
267        let globals: ModuleGlobals<TestGlobals> = ModuleGlobals::new();
268        unsafe {
269            ginit_callback::<TestGlobals>(globals.data_ptr());
270        }
271        assert!(globals.get().initialized);
272        assert_eq!(globals.get().value, 42);
273        unsafe {
274            gshutdown_callback::<TestGlobals>(globals.data_ptr());
275        }
276    }
277
278    #[test]
279    #[cfg(not(php_zts))]
280    fn non_zts_get_mut() {
281        let globals: ModuleGlobals<TestGlobals> = ModuleGlobals::new();
282        unsafe {
283            ginit_callback::<TestGlobals>(globals.data_ptr());
284            globals.get_mut().value = 99;
285        }
286        assert_eq!(globals.get().value, 99);
287        unsafe {
288            gshutdown_callback::<TestGlobals>(globals.data_ptr());
289        }
290    }
291
292    #[test]
293    #[cfg(not(php_zts))]
294    fn non_zts_as_ptr() {
295        let globals: ModuleGlobals<TestGlobals> = ModuleGlobals::new();
296        unsafe {
297            ginit_callback::<TestGlobals>(globals.data_ptr());
298        }
299        let ptr = globals.as_ptr();
300        assert_eq!(unsafe { (*ptr).value }, 42);
301        unsafe {
302            gshutdown_callback::<TestGlobals>(globals.data_ptr());
303        }
304    }
305
306    #[derive(Default)]
307    struct ZstGlobals;
308    impl ModuleGlobal for ZstGlobals {}
309
310    #[test]
311    fn zst_size_is_zero() {
312        assert_eq!(std::mem::size_of::<ZstGlobals>(), 0);
313    }
314}