use std::sync::OnceLock;
use crate::ffi;
#[derive(Debug)]
pub struct ExceptionInfo {
pub class_name: String,
pub message: Option<String>,
pub code: i64,
pub file: Option<String>,
pub line: u32,
}
impl ExceptionInfo {
unsafe fn from_zend_object(exception: *mut ffi::zend_object) -> Option<Self> {
if exception.is_null() {
return None;
}
let ex = unsafe { &*exception };
let class_name = if ex.ce.is_null() {
"Unknown".to_string()
} else {
let ce = unsafe { &*ex.ce };
if ce.name.is_null() {
"Unknown".to_string()
} else {
unsafe { zend_string_to_string(ce.name) }.unwrap_or_else(|| "Unknown".to_string())
}
};
let message = unsafe { read_exception_property(exception, "message") };
let code = unsafe { read_exception_property_long(exception, "code") };
let file = unsafe { read_exception_property(exception, "file") };
let line = unsafe { read_exception_property_long(exception, "line") }
.try_into()
.unwrap_or(0);
Some(Self {
class_name,
message,
code,
file,
line,
})
}
#[must_use]
pub fn backtrace(&self) -> Option<Vec<super::BacktraceFrame>> {
let eg = unsafe { crate::ffi::ext_php_rs_executor_globals().as_ref()? };
let mut execute_data = eg.current_execute_data;
let mut frames = Vec::new();
while !execute_data.is_null() {
if let Some(frame) = unsafe { super::BacktraceFrame::from_execute_data(execute_data) } {
frames.push(frame);
}
execute_data = unsafe { (*execute_data).prev_execute_data };
}
if frames.is_empty() {
None
} else {
Some(frames)
}
}
}
unsafe fn read_exception_property(exception: *mut ffi::zend_object, name: &str) -> Option<String> {
let ce = unsafe { (*exception).ce };
if ce.is_null() {
return None;
}
let name_cstr = std::ffi::CString::new(name).ok()?;
let mut rv = std::mem::MaybeUninit::<ffi::zval>::uninit();
let prop = unsafe {
ffi::zend_read_property(
ce,
exception,
name_cstr.as_ptr(),
name.len(),
true,
rv.as_mut_ptr(),
)
};
if prop.is_null() {
return None;
}
let prop_ref = unsafe { &*prop };
let type_info = unsafe { prop_ref.u1.type_info };
if (type_info & 0xFF) as u32 == ffi::IS_STRING {
let zs = unsafe { prop_ref.value.str_ };
unsafe { zend_string_to_string(zs) }
} else {
None
}
}
unsafe fn read_exception_property_long(exception: *mut ffi::zend_object, name: &str) -> i64 {
let ce = unsafe { (*exception).ce };
if ce.is_null() {
return 0;
}
let Ok(name_cstr) = std::ffi::CString::new(name) else {
return 0;
};
let mut rv = std::mem::MaybeUninit::<ffi::zval>::uninit();
let prop = unsafe {
ffi::zend_read_property(
ce,
exception,
name_cstr.as_ptr(),
name.len(),
true,
rv.as_mut_ptr(),
)
};
if prop.is_null() {
return 0;
}
let prop_ref = unsafe { &*prop };
let type_info = unsafe { prop_ref.u1.type_info };
if (type_info & 0xFF) as u32 == ffi::IS_LONG {
unsafe { prop_ref.value.lval }
} else {
0
}
}
unsafe fn zend_string_to_string(zs: *mut ffi::zend_string) -> Option<String> {
if zs.is_null() {
return None;
}
let len = unsafe { (*zs).len };
let ptr = unsafe { (*zs).val.as_ptr() };
let slice = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
std::str::from_utf8(slice).ok().map(String::from)
}
pub trait ExceptionObserver: 'static {
fn on_exception(&self, exception: &ExceptionInfo);
}
type ExceptionObserverFactory =
Box<dyn Fn() -> Box<dyn ExceptionObserver + Send + Sync> + Send + Sync>;
static EXCEPTION_OBSERVER_FACTORY: OnceLock<ExceptionObserverFactory> = OnceLock::new();
static EXCEPTION_OBSERVER_INSTANCE: OnceLock<Box<dyn ExceptionObserver + Send + Sync>> =
OnceLock::new();
static PREVIOUS_HOOK: OnceLock<Option<unsafe extern "C" fn(*mut ffi::zend_object)>> =
OnceLock::new();
fn get_exception_observer() -> Option<&'static (dyn ExceptionObserver + Send + Sync)> {
EXCEPTION_OBSERVER_INSTANCE
.get()
.map(std::convert::AsRef::as_ref)
}
unsafe extern "C" fn exception_observer_callback(exception: *mut ffi::zend_object) {
if let Some(observer) = get_exception_observer()
&& let Some(info) = (unsafe { ExceptionInfo::from_zend_object(exception) })
{
observer.on_exception(&info);
}
if let Some(Some(prev)) = PREVIOUS_HOOK.get() {
unsafe { prev(exception) };
}
}
pub(crate) fn register_exception_observer_factory(factory: ExceptionObserverFactory) {
assert!(
EXCEPTION_OBSERVER_FACTORY.set(factory).is_ok(),
"exception_observer can only be registered once per extension"
);
}
pub(crate) unsafe fn exception_observer_startup() {
if let Some(factory) = EXCEPTION_OBSERVER_FACTORY.get() {
if EXCEPTION_OBSERVER_INSTANCE.set(factory()).is_err() {
return;
}
let prev = unsafe { ffi::zend_throw_exception_hook };
let _ = PREVIOUS_HOOK.set(prev);
unsafe {
ffi::zend_throw_exception_hook = Some(exception_observer_callback);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestExceptionObserver;
unsafe impl Send for TestExceptionObserver {}
unsafe impl Sync for TestExceptionObserver {}
impl ExceptionObserver for TestExceptionObserver {
fn on_exception(&self, _exception: &ExceptionInfo) {}
}
#[test]
fn test_exception_info_fields() {
let info = ExceptionInfo {
class_name: "RuntimeException".to_string(),
message: Some("Test error".to_string()),
code: 42,
file: Some("/path/to/file.php".to_string()),
line: 100,
};
assert_eq!(info.class_name, "RuntimeException");
assert_eq!(info.message, Some("Test error".to_string()));
assert_eq!(info.code, 42);
assert_eq!(info.file, Some("/path/to/file.php".to_string()));
assert_eq!(info.line, 100);
}
#[test]
fn test_observer_trait_impl() {
let observer = TestExceptionObserver;
let info = ExceptionInfo {
class_name: "Exception".to_string(),
message: None,
code: 0,
file: None,
line: 0,
};
observer.on_exception(&info);
}
}