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