ext_php_rs/zend/
try_catch.rs

1use crate::ffi::{
2    ext_php_rs_zend_bailout, ext_php_rs_zend_first_try_catch, ext_php_rs_zend_try_catch,
3};
4use std::ffi::c_void;
5use std::panic::{UnwindSafe, catch_unwind, resume_unwind};
6use std::ptr::null_mut;
7
8/// Error returned when a bailout occurs
9#[derive(Debug)]
10pub struct CatchError;
11
12pub(crate) unsafe extern "C" fn panic_wrapper<R, F: FnOnce() -> R + UnwindSafe>(
13    ctx: *const c_void,
14) -> *const c_void {
15    // we try to catch panic here so we correctly shutdown php if it happens
16    // mandatory when we do assert on test as other test would not run correctly
17    // SAFETY: We read the closure from the pointer and consume it. This is safe because
18    // the closure is only called once.
19    let func = unsafe { std::ptr::read(ctx.cast::<F>()) };
20    let panic = catch_unwind(func);
21
22    Box::into_raw(Box::new(panic)).cast::<c_void>()
23}
24
25/// PHP proposes a try catch mechanism in C using setjmp and longjmp (bailout)
26/// It stores the arg of setjmp into the bailout field of the global executor
27/// If a bailout is triggered, the executor will jump to the setjmp and restore
28/// the previous setjmp
29///
30/// [`try_catch`] allows to use this mechanism
31///
32/// # Returns
33///
34/// * The result of the function
35///
36/// # Errors
37///
38/// * [`CatchError`] - A bailout occurred during the execution
39pub fn try_catch<R, F: FnOnce() -> R + UnwindSafe>(func: F) -> Result<R, CatchError> {
40    do_try_catch(func, false)
41}
42
43/// PHP proposes a try catch mechanism in C using setjmp and longjmp (bailout)
44/// It stores the arg of setjmp into the bailout field of the global executor
45/// If a bailout is triggered, the executor will jump to the setjmp and restore
46/// the previous setjmp
47///
48/// [`try_catch_first`] allows to use this mechanism
49///
50/// This functions differs from [`try_catch`] as it also initialize the bailout
51/// mechanism for the first time
52///
53/// # Returns
54///
55/// * The result of the function
56///
57/// # Errors
58///
59/// * [`CatchError`] - A bailout occurred during the execution
60pub fn try_catch_first<R, F: FnOnce() -> R + UnwindSafe>(func: F) -> Result<R, CatchError> {
61    do_try_catch(func, true)
62}
63
64fn do_try_catch<R, F: FnOnce() -> R + UnwindSafe>(func: F, first: bool) -> Result<R, CatchError> {
65    let mut panic_ptr = null_mut();
66    let has_bailout = unsafe {
67        if first {
68            ext_php_rs_zend_first_try_catch(
69                panic_wrapper::<R, F>,
70                (&raw const func).cast::<c_void>(),
71                &raw mut panic_ptr,
72            )
73        } else {
74            ext_php_rs_zend_try_catch(
75                panic_wrapper::<R, F>,
76                (&raw const func).cast::<c_void>(),
77                &raw mut panic_ptr,
78            )
79        }
80    };
81
82    // Prevent the closure from being dropped here since it was consumed in panic_wrapper
83    std::mem::forget(func);
84
85    let panic = panic_ptr.cast::<std::thread::Result<R>>();
86
87    // can be null if there is a bailout
88    if panic.is_null() || has_bailout {
89        return Err(CatchError);
90    }
91
92    match unsafe { *Box::from_raw(panic.cast::<std::thread::Result<R>>()) } {
93        Ok(r) => Ok(r),
94        Err(err) => {
95            // we resume the panic here so it can be caught correctly by the test framework
96            resume_unwind(err);
97        }
98    }
99}
100
101/// Trigger a bailout
102///
103/// This function will stop the execution of the current script
104/// and jump to the last try catch block
105///
106/// # Safety
107///
108/// This function is unsafe because it can cause memory leaks
109/// Since it will jump to the last try catch block, it will not call the
110/// destructor of the current scope
111///
112/// When using this function you should ensure that all the memory allocated in
113/// the current scope is released
114pub unsafe fn bailout() -> ! {
115    unsafe { ext_php_rs_zend_bailout() };
116}
117
118#[cfg(feature = "embed")]
119#[cfg(test)]
120mod tests {
121    use crate::embed::Embed;
122    use crate::zend::{bailout, try_catch};
123    use std::ptr::null_mut;
124
125    #[test]
126    fn test_catch() {
127        Embed::run(|| {
128            let catch = try_catch(|| {
129                unsafe {
130                    bailout();
131                }
132
133                #[allow(unreachable_code)]
134                #[allow(clippy::assertions_on_constants)]
135                {
136                    assert!(false);
137                }
138            });
139
140            assert!(catch.is_err());
141        });
142    }
143
144    #[test]
145    fn test_no_catch() {
146        Embed::run(|| {
147            let catch = try_catch(|| {
148                #[allow(clippy::assertions_on_constants)]
149                {
150                    assert!(true);
151                }
152            });
153
154            assert!(catch.is_ok());
155        });
156    }
157
158    #[test]
159    fn test_bailout() {
160        Embed::run(|| {
161            unsafe {
162                bailout();
163            }
164
165            #[allow(unreachable_code)]
166            #[allow(clippy::assertions_on_constants)]
167            {
168                assert!(false);
169            }
170        });
171    }
172
173    #[test]
174    #[should_panic(expected = "should panic")]
175    fn test_panic() {
176        Embed::run(|| {
177            let _ = try_catch(|| {
178                panic!("should panic");
179            });
180        });
181    }
182
183    #[test]
184    fn test_return() {
185        let foo = Embed::run(|| {
186            let result = try_catch(|| "foo");
187
188            assert!(result.is_ok());
189
190            #[allow(clippy::unwrap_used)]
191            result.unwrap()
192        });
193
194        assert_eq!(foo, "foo");
195    }
196
197    #[test]
198    fn test_memory_leak() {
199        use std::panic::AssertUnwindSafe;
200
201        Embed::run(|| {
202            let mut ptr = null_mut();
203
204            let _ = try_catch(AssertUnwindSafe(|| {
205                let mut result = "foo".to_string();
206                ptr = &raw mut result;
207
208                unsafe {
209                    bailout();
210                }
211            }));
212
213            // Check that the string is never released
214            let result = unsafe { &*ptr as &str };
215
216            assert_eq!(result, "foo");
217        });
218    }
219}