use super::bailout_guard::{cleanup_depth, run_cleanups_above};
use crate::exception::{PhpException, PhpResult};
use crate::ffi::{
ext_php_rs_zend_bailout, ext_php_rs_zend_first_try_catch, ext_php_rs_zend_try_catch,
};
use crate::zend::ce;
use std::any::Any;
use std::ffi::c_void;
use std::mem::{ManuallyDrop, MaybeUninit};
use std::panic::{AssertUnwindSafe, UnwindSafe, catch_unwind};
use std::ptr::null_mut;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CatchError {
#[error("the engine bailed out")]
Bailout,
#[error("the closure result pointer was null")]
NullPanicPtr,
#[error("the closure panicked: {0}")]
Panic(String),
}
fn panic_message(payload: &(dyn Any + Send)) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
(*s).to_owned()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"non-string panic payload".to_owned()
}
}
fn panic_exception(message: &str) -> PhpException {
PhpException::new(format!("Rust panic: {message}"), 0, ce::error())
}
pub fn run_handler<F: FnOnce() + UnwindSafe>(func: F) {
match try_catch(func) {
Ok(()) => {}
Err(CatchError::Bailout) => unsafe { bailout() },
Err(CatchError::Panic(message)) => panic_exception(&message).throw(),
Err(err @ CatchError::NullPanicPtr) => panic_exception(&err.to_string()).throw(),
}
}
pub(crate) fn catch_panic<R>(func: impl FnOnce() -> PhpResult<R>) -> PhpResult<R> {
match catch_unwind(AssertUnwindSafe(func)) {
Ok(result) => result,
Err(payload) => Err(panic_exception(&panic_message(payload.as_ref()))),
}
}
pub(crate) struct CatchFrame<R, F> {
func: ManuallyDrop<F>,
result: MaybeUninit<std::thread::Result<R>>,
}
impl<R, F: FnOnce() -> R + UnwindSafe> CatchFrame<R, F> {
pub(crate) fn new(func: F) -> Self {
Self {
func: ManuallyDrop::new(func),
result: MaybeUninit::uninit(),
}
}
pub(crate) unsafe fn into_result(self) -> std::thread::Result<R> {
unsafe { self.result.assume_init() }
}
}
pub(crate) unsafe extern "C" fn panic_wrapper<R, F: FnOnce() -> R + UnwindSafe>(
ctx: *const c_void,
) -> *const c_void {
let frame = unsafe { &mut *ctx.cast_mut().cast::<CatchFrame<R, F>>() };
let func = unsafe { ManuallyDrop::take(&mut frame.func) };
frame.result.write(catch_unwind(func));
ctx
}
pub fn try_catch<R, F: FnOnce() -> R + UnwindSafe>(func: F) -> Result<R, CatchError> {
do_try_catch(func, false)
}
pub fn try_catch_first<R, F: FnOnce() -> R + UnwindSafe>(func: F) -> Result<R, CatchError> {
do_try_catch(func, true)
}
fn do_try_catch<R, F: FnOnce() -> R + UnwindSafe>(func: F, first: bool) -> Result<R, CatchError> {
let mut frame = CatchFrame::new(func);
let ctx = (&raw mut frame).cast::<c_void>().cast_const();
let mut done = null_mut();
let depth = cleanup_depth();
let has_bailout = unsafe {
if first {
ext_php_rs_zend_first_try_catch(panic_wrapper::<R, F>, ctx, &raw mut done)
} else {
ext_php_rs_zend_try_catch(panic_wrapper::<R, F>, ctx, &raw mut done)
}
};
if has_bailout {
run_cleanups_above(depth);
return Err(CatchError::Bailout);
}
if done.is_null() {
return Err(CatchError::NullPanicPtr);
}
match unsafe { frame.into_result() } {
Ok(r) => Ok(r),
Err(payload) => Err(CatchError::Panic(panic_message(payload.as_ref()))),
}
}
pub unsafe fn bailout() -> ! {
unsafe { ext_php_rs_zend_bailout() };
}
#[cfg(feature = "embed")]
#[cfg(test)]
mod tests {
use super::CatchError;
use crate::embed::Embed;
use crate::zend::{BailoutGuard, bailout, try_catch};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
struct DropCounter(Arc<AtomicUsize>);
impl Drop for DropCounter {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
#[test]
fn bailout_drops_inner_guard_and_spares_outer() {
let outer_drops = Arc::new(AtomicUsize::new(0));
let inner_drops = Arc::new(AtomicUsize::new(0));
let (caught, outer_alive_after) = Embed::run(|| {
let outer = BailoutGuard::new(DropCounter(Arc::clone(&outer_drops)));
let inner_drops = Arc::clone(&inner_drops);
let caught = try_catch(move || {
let _inner = BailoutGuard::new(DropCounter(inner_drops));
unsafe { bailout() }
});
let outer_alive_after = outer.get().0.load(Ordering::SeqCst) == 0;
drop(outer);
(caught.is_err(), outer_alive_after)
});
assert!(caught);
assert!(outer_alive_after);
assert_eq!(inner_drops.load(Ordering::SeqCst), 1);
assert_eq!(outer_drops.load(Ordering::SeqCst), 1);
}
#[test]
fn normal_return_drops_guard_once() {
let drops = Arc::new(AtomicUsize::new(0));
let ok = Embed::run(|| {
let drops = Arc::clone(&drops);
try_catch(move || {
let _guard = BailoutGuard::new(DropCounter(drops));
})
.is_ok()
});
assert!(ok);
assert_eq!(drops.load(Ordering::SeqCst), 1);
}
#[test]
fn test_catch() {
let caught = Embed::run(|| {
let catch = try_catch(|| unsafe { bailout() });
matches!(catch, Err(CatchError::Bailout))
});
assert!(caught);
}
#[test]
fn test_no_catch() {
let ok = Embed::run(|| try_catch(|| 1).is_ok());
assert!(ok);
}
#[test]
fn test_bailout() {
Embed::run(|| {
unsafe {
bailout();
}
#[allow(unreachable_code)]
#[allow(clippy::assertions_on_constants)]
{
assert!(false);
}
});
}
#[test]
fn test_panic_is_reported_not_resumed() {
let message = Embed::run(|| match try_catch(|| panic!("should panic")) {
Err(CatchError::Panic(message)) => message,
other => format!("unexpected: {other:?}"),
});
assert_eq!(message, "should panic");
}
#[test]
fn test_panic_payload_fallback() {
let message = Embed::run(|| match try_catch(|| std::panic::panic_any(42)) {
Err(CatchError::Panic(message)) => message,
other => format!("unexpected: {other:?}"),
});
assert_eq!(message, "non-string panic payload");
}
#[test]
fn test_return() {
let foo = Embed::run(|| {
let result = try_catch(|| "foo");
assert!(result.is_ok());
#[allow(clippy::unwrap_used)]
result.unwrap()
});
assert_eq!(foo, "foo");
}
#[test]
fn test_memory_leak() {
let drops = Arc::new(AtomicUsize::new(0));
Embed::run(|| {
let drops = Arc::clone(&drops);
let _ = try_catch(move || {
let _unguarded = DropCounter(drops);
unsafe { bailout() }
});
});
assert_eq!(drops.load(Ordering::SeqCst), 0);
}
}