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