Skip to main content

ext_php_rs/zend/
bailout_guard.rs

1//! Drops a value even when a PHP bailout skips its frame.
2//!
3//! A bailout is a `longjmp` that the engine makes on a fatal error or on `memory_limit`
4//! exhaustion. The jump skips the Rust frames between the error and the catch point, so
5//! Rust does not run their destructors. `BailoutGuard` moves the value to the heap and
6//! registers a cleanup entry with the innermost [`try_catch`](crate::zend::try_catch)
7//! frame. That frame drops the value when it catches the bailout.
8//!
9//! # Ownership
10//!
11//! Every [`try_catch`](crate::zend::try_catch) call records the depth of the cleanup stack
12//! on entry. The wrapper of each exported PHP function is such a call. When the frame
13//! catches a bailout, it drops the guards that were created inside its closure, newest
14//! first. The guards that were created before the frame are not changed. You can still
15//! use them.
16//!
17//! Do not move a guard out of a `try_catch` closure through shared mutable state, for
18//! example `AssertUnwindSafe(&mut Option<_>)`. If that closure bails out, the frame drops
19//! the value and the moved guard points at freed memory.
20//!
21//! `BailoutGuard` is `!Send`. The cleanup stack belongs to the thread that created the
22//! guard.
23//!
24//! # Example
25//!
26//! ```ignore
27//! use ext_php_rs::zend::BailoutGuard;
28//!
29//! #[php_function]
30//! pub fn my_function(callback: ZendCallable) {
31//!     let resource = BailoutGuard::new(ExpensiveResource::new());
32//!
33//!     // BailoutGuard implements Deref and DerefMut
34//!     resource.do_something();
35//!
36//!     // The resource is dropped even if the callback hits a fatal error
37//!     let _ = callback.try_call(vec![]);
38//! }
39//! ```
40//!
41//! A guard cannot leave its thread:
42//!
43//! ```compile_fail
44//! use ext_php_rs::zend::BailoutGuard;
45//!
46//! let guard = BailoutGuard::new(42_u64);
47//! std::thread::spawn(move || drop(guard));
48//! ```
49
50use std::cell::RefCell;
51use std::ops::{Deref, DerefMut};
52
53/// A cleanup entry. `None` marks an entry whose guard was released out of LIFO
54/// order; it is popped once every newer entry is gone.
55type CleanupEntry = Option<Box<dyn FnOnce()>>;
56
57thread_local! {
58    static CLEANUP_STACK: RefCell<Vec<CleanupEntry>> = const { RefCell::new(Vec::new()) };
59}
60
61/// A guard that drops its value even if a PHP bailout skips its frame.
62///
63/// `BailoutGuard::new` moves the value to the heap and registers a cleanup entry with
64/// the innermost [`try_catch`](crate::zend::try_catch) frame. If that frame catches a
65/// bailout, it drops the value. If you drop the guard, the guard releases the entry and
66/// drops the value.
67///
68/// # Performance
69///
70/// `BailoutGuard::new` makes one heap allocation. Use it only for values that must be
71/// released: file handles, network connections, locks. Do not wrap simple values.
72pub struct BailoutGuard<T> {
73    value: *mut T,
74    index: usize,
75}
76
77impl<T: 'static> BailoutGuard<T> {
78    /// Wraps `value` in a new guard.
79    ///
80    /// The value moves to the heap. The guard registers a cleanup entry with the
81    /// innermost `try_catch` frame.
82    pub fn new(value: T) -> Self {
83        let ptr = Box::into_raw(Box::new(value));
84        let index = CLEANUP_STACK.with(|stack| {
85            let mut stack = stack.borrow_mut();
86            stack.push(Some(Box::new(move || {
87                // SAFETY: only the bailout path runs this closure. The frames that owned
88                // the guard were jumped over by longjmp, so its Drop never runs and this is
89                // the only release of the allocation.
90                unsafe { drop(Box::from_raw(ptr)) }
91            })));
92            stack.len() - 1
93        });
94
95        Self { value: ptr, index }
96    }
97
98    /// Returns a reference to the wrapped value.
99    #[inline]
100    #[must_use]
101    pub fn get(&self) -> &T {
102        // SAFETY: The pointer is valid as long as self exists.
103        unsafe { &*self.value }
104    }
105
106    /// Returns a mutable reference to the wrapped value.
107    #[inline]
108    pub fn get_mut(&mut self) -> &mut T {
109        // SAFETY: The pointer is valid as long as self exists, and we have &mut self.
110        unsafe { &mut *self.value }
111    }
112
113    /// Consumes the guard and returns the value.
114    ///
115    /// The guard releases its cleanup entry.
116    #[must_use]
117    pub fn into_inner(self) -> T {
118        self.release();
119        // SAFETY: We're consuming self, so no one else can access the pointer.
120        let value = unsafe { *Box::from_raw(self.value) };
121        std::mem::forget(self);
122        value
123    }
124}
125
126impl<T> BailoutGuard<T> {
127    fn release(&self) {
128        CLEANUP_STACK.with(|stack| {
129            let mut stack = stack.borrow_mut();
130            if self.index + 1 == stack.len() {
131                stack.pop();
132                while stack.last().is_some_and(Option::is_none) {
133                    stack.pop();
134                }
135            } else if let Some(entry) = stack.get_mut(self.index) {
136                *entry = None;
137            }
138        });
139    }
140}
141
142impl<T> Deref for BailoutGuard<T> {
143    type Target = T;
144
145    #[inline]
146    fn deref(&self) -> &T {
147        // SAFETY: The pointer is valid as long as self exists.
148        unsafe { &*self.value }
149    }
150}
151
152impl<T> DerefMut for BailoutGuard<T> {
153    #[inline]
154    fn deref_mut(&mut self) -> &mut T {
155        // SAFETY: The pointer is valid as long as self exists, and we have &mut self.
156        unsafe { &mut *self.value }
157    }
158}
159
160impl<T> Drop for BailoutGuard<T> {
161    fn drop(&mut self) {
162        self.release();
163        // SAFETY: We're in Drop, so no one else can access the pointer.
164        unsafe { drop(Box::from_raw(self.value)) }
165    }
166}
167
168/// Current depth of the cleanup stack, recorded by `try_catch` on entry.
169pub(crate) fn cleanup_depth() -> usize {
170    CLEANUP_STACK.with(|stack| stack.borrow().len())
171}
172
173/// Removes the cleanup entries above `depth` and runs them, newest first.
174///
175/// `try_catch` calls this after it catches a bailout. The entries leave the stack before
176/// they run, so a destructor can create new guards or call the engine.
177pub(crate) fn run_cleanups_above(depth: usize) {
178    let entries: Vec<CleanupEntry> = CLEANUP_STACK.with(|stack| {
179        let mut stack = stack.borrow_mut();
180        if depth >= stack.len() {
181            return Vec::new();
182        }
183        stack.drain(depth..).collect()
184    });
185    for cleanup in entries.into_iter().rev().flatten() {
186        cleanup();
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use std::sync::Arc;
194    use std::sync::atomic::{AtomicUsize, Ordering};
195
196    struct DropCounter(Arc<AtomicUsize>);
197    impl Drop for DropCounter {
198        fn drop(&mut self) {
199            self.0.fetch_add(1, Ordering::SeqCst);
200        }
201    }
202
203    fn counter() -> (Arc<AtomicUsize>, impl Fn() -> DropCounter) {
204        let count = Arc::new(AtomicUsize::new(0));
205        let make = {
206            let count = Arc::clone(&count);
207            move || DropCounter(Arc::clone(&count))
208        };
209        (count, make)
210    }
211
212    fn reset() {
213        CLEANUP_STACK.with(|stack| stack.borrow_mut().clear());
214    }
215
216    #[test]
217    fn normal_drop_pops_entry() {
218        reset();
219        let (count, make) = counter();
220        {
221            let _guard = BailoutGuard::new(make());
222            assert_eq!(cleanup_depth(), 1);
223        }
224        assert_eq!(count.load(Ordering::SeqCst), 1);
225        assert_eq!(cleanup_depth(), 0);
226    }
227
228    #[test]
229    fn out_of_order_drop_drains_once_newer_guard_goes() {
230        reset();
231        let (count, make) = counter();
232        let older = BailoutGuard::new(make());
233        let newer = BailoutGuard::new(make());
234        drop(older);
235        assert_eq!(cleanup_depth(), 2);
236        drop(newer);
237        assert_eq!(cleanup_depth(), 0);
238        assert_eq!(count.load(Ordering::SeqCst), 2);
239    }
240
241    #[test]
242    fn cleanups_above_depth_spare_outer_guard() {
243        reset();
244        let (count, make) = counter();
245        let outer = BailoutGuard::new(make());
246        let depth = cleanup_depth();
247        let inner = BailoutGuard::new(make());
248        std::mem::forget(inner);
249
250        run_cleanups_above(depth);
251
252        assert_eq!(count.load(Ordering::SeqCst), 1);
253        assert_eq!(cleanup_depth(), 1);
254        assert_eq!(outer.get().0.load(Ordering::SeqCst), 1);
255        drop(outer);
256        assert_eq!(count.load(Ordering::SeqCst), 2);
257        assert_eq!(cleanup_depth(), 0);
258    }
259
260    struct Record(Arc<std::sync::Mutex<Vec<u8>>>, u8);
261    impl Drop for Record {
262        fn drop(&mut self) {
263            self.0.lock().expect("poisoned").push(self.1);
264        }
265    }
266
267    #[test]
268    fn cleanups_run_newest_first() {
269        reset();
270        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
271        std::mem::forget(BailoutGuard::new(Record(Arc::clone(&order), 1)));
272        std::mem::forget(BailoutGuard::new(Record(Arc::clone(&order), 2)));
273
274        run_cleanups_above(0);
275
276        assert_eq!(*order.lock().expect("poisoned"), vec![2, 1]);
277    }
278
279    #[test]
280    fn into_inner_releases_entry() {
281        reset();
282        let (count, make) = counter();
283        let value = BailoutGuard::new(make()).into_inner();
284        assert_eq!(cleanup_depth(), 0);
285        assert_eq!(count.load(Ordering::SeqCst), 0);
286        drop(value);
287        assert_eq!(count.load(Ordering::SeqCst), 1);
288    }
289
290    #[test]
291    fn deref_and_deref_mut() {
292        let mut guard = BailoutGuard::new(String::from("hello"));
293        guard.push_str(" world");
294        assert_eq!(&*guard, "hello world");
295        assert_eq!(guard.len(), 11);
296    }
297}