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::panic::{AssertUnwindSafe, UnwindSafe, catch_unwind};
10use std::ptr::null_mut;
11
12#[derive(Debug, thiserror::Error)]
14#[non_exhaustive]
15pub enum CatchError {
16 #[error("the engine bailed out")]
18 Bailout,
19 #[error("the closure result pointer was null")]
21 NullPanicPtr,
22 #[error("the closure panicked: {0}")]
25 Panic(String),
26}
27
28fn panic_message(payload: &(dyn Any + Send)) -> String {
29 if let Some(s) = payload.downcast_ref::<&str>() {
30 (*s).to_owned()
31 } else if let Some(s) = payload.downcast_ref::<String>() {
32 s.clone()
33 } else {
34 "non-string panic payload".to_owned()
35 }
36}
37
38fn panic_exception(message: &str) -> PhpException {
39 PhpException::new(format!("Rust panic: {message}"), 0, ce::error())
40}
41
42pub fn run_handler<F: FnOnce() + UnwindSafe>(func: F) {
57 match try_catch(func) {
58 Ok(()) => {}
59 Err(CatchError::Bailout) => unsafe { bailout() },
60 Err(CatchError::Panic(message)) => panic_exception(&message).throw(),
61 Err(err @ CatchError::NullPanicPtr) => panic_exception(&err.to_string()).throw(),
62 }
63}
64
65pub(crate) fn catch_panic<R>(func: impl FnOnce() -> PhpResult<R>) -> PhpResult<R> {
69 match catch_unwind(AssertUnwindSafe(func)) {
70 Ok(result) => result,
71 Err(payload) => Err(panic_exception(&panic_message(payload.as_ref()))),
72 }
73}
74
75pub(crate) unsafe extern "C" fn panic_wrapper<R, F: FnOnce() -> R + UnwindSafe>(
76 ctx: *const c_void,
77) -> *const c_void {
78 let func = unsafe { std::ptr::read(ctx.cast::<F>()) };
83 let panic = catch_unwind(func);
84
85 Box::into_raw(Box::new(panic)).cast::<c_void>()
86}
87
88pub fn try_catch<R, F: FnOnce() -> R + UnwindSafe>(func: F) -> Result<R, CatchError> {
112 do_try_catch(func, false)
113}
114
115pub fn try_catch_first<R, F: FnOnce() -> R + UnwindSafe>(func: F) -> Result<R, CatchError> {
134 do_try_catch(func, true)
135}
136
137fn do_try_catch<R, F: FnOnce() -> R + UnwindSafe>(func: F, first: bool) -> Result<R, CatchError> {
138 let mut panic_ptr = null_mut();
139 let depth = cleanup_depth();
140 let has_bailout = unsafe {
141 if first {
142 ext_php_rs_zend_first_try_catch(
143 panic_wrapper::<R, F>,
144 (&raw const func).cast::<c_void>(),
145 &raw mut panic_ptr,
146 )
147 } else {
148 ext_php_rs_zend_try_catch(
149 panic_wrapper::<R, F>,
150 (&raw const func).cast::<c_void>(),
151 &raw mut panic_ptr,
152 )
153 }
154 };
155
156 std::mem::forget(func);
158
159 let panic = panic_ptr.cast::<std::thread::Result<R>>();
160
161 if has_bailout {
162 run_cleanups_above(depth);
163 return Err(CatchError::Bailout);
164 }
165
166 if panic.is_null() {
167 return Err(CatchError::NullPanicPtr);
168 }
169
170 match unsafe { *Box::from_raw(panic.cast::<std::thread::Result<R>>()) } {
171 Ok(r) => Ok(r),
172 Err(payload) => Err(CatchError::Panic(panic_message(payload.as_ref()))),
173 }
174}
175
176pub unsafe fn bailout() -> ! {
190 unsafe { ext_php_rs_zend_bailout() };
191}
192
193#[cfg(feature = "embed")]
194#[cfg(test)]
195mod tests {
196 use super::CatchError;
197 use crate::embed::Embed;
198 use crate::zend::{BailoutGuard, bailout, try_catch};
199 use std::sync::Arc;
200 use std::sync::atomic::{AtomicUsize, Ordering};
201
202 struct DropCounter(Arc<AtomicUsize>);
203 impl Drop for DropCounter {
204 fn drop(&mut self) {
205 self.0.fetch_add(1, Ordering::SeqCst);
206 }
207 }
208
209 #[test]
210 fn bailout_drops_inner_guard_and_spares_outer() {
211 let outer_drops = Arc::new(AtomicUsize::new(0));
212 let inner_drops = Arc::new(AtomicUsize::new(0));
213 let (caught, outer_alive_after) = Embed::run(|| {
214 let outer = BailoutGuard::new(DropCounter(Arc::clone(&outer_drops)));
215 let inner_drops = Arc::clone(&inner_drops);
216 let caught = try_catch(move || {
217 let _inner = BailoutGuard::new(DropCounter(inner_drops));
218 unsafe { bailout() }
219 });
220 let outer_alive_after = outer.get().0.load(Ordering::SeqCst) == 0;
221 drop(outer);
222 (caught.is_err(), outer_alive_after)
223 });
224
225 assert!(caught);
226 assert!(outer_alive_after);
227 assert_eq!(inner_drops.load(Ordering::SeqCst), 1);
228 assert_eq!(outer_drops.load(Ordering::SeqCst), 1);
229 }
230
231 #[test]
232 fn normal_return_drops_guard_once() {
233 let drops = Arc::new(AtomicUsize::new(0));
234 let ok = Embed::run(|| {
235 let drops = Arc::clone(&drops);
236 try_catch(move || {
237 let _guard = BailoutGuard::new(DropCounter(drops));
238 })
239 .is_ok()
240 });
241
242 assert!(ok);
243 assert_eq!(drops.load(Ordering::SeqCst), 1);
244 }
245
246 #[test]
247 fn test_catch() {
248 let caught = Embed::run(|| {
249 let catch = try_catch(|| unsafe { bailout() });
250 matches!(catch, Err(CatchError::Bailout))
251 });
252
253 assert!(caught);
254 }
255
256 #[test]
257 fn test_no_catch() {
258 let ok = Embed::run(|| try_catch(|| 1).is_ok());
259
260 assert!(ok);
261 }
262
263 #[test]
264 fn test_bailout() {
265 Embed::run(|| {
266 unsafe {
267 bailout();
268 }
269
270 #[allow(unreachable_code)]
271 #[allow(clippy::assertions_on_constants)]
272 {
273 assert!(false);
274 }
275 });
276 }
277
278 #[test]
279 fn test_panic_is_reported_not_resumed() {
280 let message = Embed::run(|| match try_catch(|| panic!("should panic")) {
281 Err(CatchError::Panic(message)) => message,
282 other => format!("unexpected: {other:?}"),
283 });
284
285 assert_eq!(message, "should panic");
286 }
287
288 #[test]
289 fn test_panic_payload_fallback() {
290 let message = Embed::run(|| match try_catch(|| std::panic::panic_any(42)) {
291 Err(CatchError::Panic(message)) => message,
292 other => format!("unexpected: {other:?}"),
293 });
294
295 assert_eq!(message, "non-string panic payload");
296 }
297
298 #[test]
299 fn test_return() {
300 let foo = Embed::run(|| {
301 let result = try_catch(|| "foo");
302
303 assert!(result.is_ok());
304
305 #[allow(clippy::unwrap_used)]
306 result.unwrap()
307 });
308
309 assert_eq!(foo, "foo");
310 }
311
312 #[test]
313 fn test_memory_leak() {
314 let drops = Arc::new(AtomicUsize::new(0));
315 Embed::run(|| {
316 let drops = Arc::clone(&drops);
317 let _ = try_catch(move || {
318 let _unguarded = DropCounter(drops);
319 unsafe { bailout() }
320 });
321 });
322
323 assert_eq!(drops.load(Ordering::SeqCst), 0);
324 }
325}