ext_php_rs/zend/
try_catch.rs1use super::bailout_guard::{cleanup_depth, run_cleanups_above};
2use crate::exception::{PhpException, PhpResult};
3use crate::ffi::{
4 ext_php_rs_zend_bailout, ext_php_rs_zend_first_try_catch, ext_php_rs_zend_try_catch,
5};
6use crate::zend::ce;
7use std::any::Any;
8use std::ffi::c_void;
9use std::mem::{ManuallyDrop, MaybeUninit};
10use std::panic::{AssertUnwindSafe, UnwindSafe, catch_unwind};
11use std::ptr::null_mut;
12
13#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum CatchError {
17 #[error("the engine bailed out")]
19 Bailout,
20 #[error("the closure result pointer was null")]
22 NullPanicPtr,
23 #[error("the closure panicked: {0}")]
26 Panic(String),
27}
28
29fn panic_message(payload: &(dyn Any + Send)) -> String {
30 if let Some(s) = payload.downcast_ref::<&str>() {
31 (*s).to_owned()
32 } else if let Some(s) = payload.downcast_ref::<String>() {
33 s.clone()
34 } else {
35 "non-string panic payload".to_owned()
36 }
37}
38
39fn panic_exception(message: &str) -> PhpException {
40 PhpException::new(format!("Rust panic: {message}"), 0, ce::error())
41}
42
43pub fn run_handler<F: FnOnce() + UnwindSafe>(func: F) {
58 match try_catch(func) {
59 Ok(()) => {}
60 Err(CatchError::Bailout) => unsafe { bailout() },
61 Err(CatchError::Panic(message)) => panic_exception(&message).throw(),
62 Err(err @ CatchError::NullPanicPtr) => panic_exception(&err.to_string()).throw(),
63 }
64}
65
66pub(crate) fn catch_panic<R>(func: impl FnOnce() -> PhpResult<R>) -> PhpResult<R> {
70 match catch_unwind(AssertUnwindSafe(func)) {
71 Ok(result) => result,
72 Err(payload) => Err(panic_exception(&panic_message(payload.as_ref()))),
73 }
74}
75
76pub(crate) struct CatchFrame<R, F> {
78 func: ManuallyDrop<F>,
79 result: MaybeUninit<std::thread::Result<R>>,
80}
81
82impl<R, F: FnOnce() -> R + UnwindSafe> CatchFrame<R, F> {
83 pub(crate) fn new(func: F) -> Self {
84 Self {
85 func: ManuallyDrop::new(func),
86 result: MaybeUninit::uninit(),
87 }
88 }
89
90 pub(crate) unsafe fn into_result(self) -> std::thread::Result<R> {
94 unsafe { self.result.assume_init() }
95 }
96}
97
98pub(crate) unsafe extern "C" fn panic_wrapper<R, F: FnOnce() -> R + UnwindSafe>(
102 ctx: *const c_void,
103) -> *const c_void {
104 let frame = unsafe { &mut *ctx.cast_mut().cast::<CatchFrame<R, F>>() };
107 let func = unsafe { ManuallyDrop::take(&mut frame.func) };
108 frame.result.write(catch_unwind(func));
109 ctx
110}
111
112pub fn try_catch<R, F: FnOnce() -> R + UnwindSafe>(func: F) -> Result<R, CatchError> {
136 do_try_catch(func, false)
137}
138
139pub fn try_catch_first<R, F: FnOnce() -> R + UnwindSafe>(func: F) -> Result<R, CatchError> {
158 do_try_catch(func, true)
159}
160
161fn do_try_catch<R, F: FnOnce() -> R + UnwindSafe>(func: F, first: bool) -> Result<R, CatchError> {
162 let mut frame = CatchFrame::new(func);
163 let ctx = (&raw mut frame).cast::<c_void>().cast_const();
164 let mut done = null_mut();
165 let depth = cleanup_depth();
166 let has_bailout = unsafe {
167 if first {
168 ext_php_rs_zend_first_try_catch(panic_wrapper::<R, F>, ctx, &raw mut done)
169 } else {
170 ext_php_rs_zend_try_catch(panic_wrapper::<R, F>, ctx, &raw mut done)
171 }
172 };
173
174 if has_bailout {
175 run_cleanups_above(depth);
176 return Err(CatchError::Bailout);
177 }
178
179 if done.is_null() {
180 return Err(CatchError::NullPanicPtr);
181 }
182
183 match unsafe { frame.into_result() } {
185 Ok(r) => Ok(r),
186 Err(payload) => Err(CatchError::Panic(panic_message(payload.as_ref()))),
187 }
188}
189
190pub unsafe fn bailout() -> ! {
204 unsafe { ext_php_rs_zend_bailout() };
205}
206
207#[cfg(feature = "embed")]
208#[cfg(test)]
209mod tests {
210 use super::CatchError;
211 use crate::embed::Embed;
212 use crate::zend::{BailoutGuard, bailout, try_catch};
213 use std::sync::Arc;
214 use std::sync::atomic::{AtomicUsize, Ordering};
215
216 struct DropCounter(Arc<AtomicUsize>);
217 impl Drop for DropCounter {
218 fn drop(&mut self) {
219 self.0.fetch_add(1, Ordering::SeqCst);
220 }
221 }
222
223 #[test]
224 fn bailout_drops_inner_guard_and_spares_outer() {
225 let outer_drops = Arc::new(AtomicUsize::new(0));
226 let inner_drops = Arc::new(AtomicUsize::new(0));
227 let (caught, outer_alive_after) = Embed::run(|| {
228 let outer = BailoutGuard::new(DropCounter(Arc::clone(&outer_drops)));
229 let inner_drops = Arc::clone(&inner_drops);
230 let caught = try_catch(move || {
231 let _inner = BailoutGuard::new(DropCounter(inner_drops));
232 unsafe { bailout() }
233 });
234 let outer_alive_after = outer.get().0.load(Ordering::SeqCst) == 0;
235 drop(outer);
236 (caught.is_err(), outer_alive_after)
237 });
238
239 assert!(caught);
240 assert!(outer_alive_after);
241 assert_eq!(inner_drops.load(Ordering::SeqCst), 1);
242 assert_eq!(outer_drops.load(Ordering::SeqCst), 1);
243 }
244
245 #[test]
246 fn normal_return_drops_guard_once() {
247 let drops = Arc::new(AtomicUsize::new(0));
248 let ok = Embed::run(|| {
249 let drops = Arc::clone(&drops);
250 try_catch(move || {
251 let _guard = BailoutGuard::new(DropCounter(drops));
252 })
253 .is_ok()
254 });
255
256 assert!(ok);
257 assert_eq!(drops.load(Ordering::SeqCst), 1);
258 }
259
260 #[test]
261 fn test_catch() {
262 let caught = Embed::run(|| {
263 let catch = try_catch(|| unsafe { bailout() });
264 matches!(catch, Err(CatchError::Bailout))
265 });
266
267 assert!(caught);
268 }
269
270 #[test]
271 fn test_no_catch() {
272 let ok = Embed::run(|| try_catch(|| 1).is_ok());
273
274 assert!(ok);
275 }
276
277 #[test]
278 fn test_bailout() {
279 Embed::run(|| {
280 unsafe {
281 bailout();
282 }
283
284 #[allow(unreachable_code)]
285 #[allow(clippy::assertions_on_constants)]
286 {
287 assert!(false);
288 }
289 });
290 }
291
292 #[test]
293 fn test_panic_is_reported_not_resumed() {
294 let message = Embed::run(|| match try_catch(|| panic!("should panic")) {
295 Err(CatchError::Panic(message)) => message,
296 other => format!("unexpected: {other:?}"),
297 });
298
299 assert_eq!(message, "should panic");
300 }
301
302 #[test]
303 fn test_panic_payload_fallback() {
304 let message = Embed::run(|| match try_catch(|| std::panic::panic_any(42)) {
305 Err(CatchError::Panic(message)) => message,
306 other => format!("unexpected: {other:?}"),
307 });
308
309 assert_eq!(message, "non-string panic payload");
310 }
311
312 #[test]
313 fn test_return() {
314 let foo = Embed::run(|| {
315 let result = try_catch(|| "foo");
316
317 assert!(result.is_ok());
318
319 #[allow(clippy::unwrap_used)]
320 result.unwrap()
321 });
322
323 assert_eq!(foo, "foo");
324 }
325
326 #[test]
327 fn test_memory_leak() {
328 let drops = Arc::new(AtomicUsize::new(0));
329 Embed::run(|| {
330 let drops = Arc::clone(&drops);
331 let _ = try_catch(move || {
332 let _unguarded = DropCounter(drops);
333 unsafe { bailout() }
334 });
335 });
336
337 assert_eq!(drops.load(Ordering::SeqCst), 0);
338 }
339}