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}