ext_php_rs/zend/
exception_observer.rs1use std::sync::OnceLock;
23use std::sync::atomic::{AtomicPtr, Ordering};
24
25use crate::ffi;
26
27#[derive(Debug)]
29pub struct ExceptionInfo {
30 pub class_name: String,
32 pub message: Option<String>,
34 pub code: i64,
36 pub file: Option<String>,
38 pub line: u32,
40}
41
42impl ExceptionInfo {
43 unsafe fn from_zend_object(exception: *mut ffi::zend_object) -> Option<Self> {
47 if exception.is_null() {
48 return None;
49 }
50
51 let ex = unsafe { &*exception };
52
53 let class_name = if ex.ce.is_null() {
54 "Unknown".to_string()
55 } else {
56 let ce = unsafe { &*ex.ce };
57 if ce.name.is_null() {
58 "Unknown".to_string()
59 } else {
60 unsafe { zend_string_to_string(ce.name) }.unwrap_or_else(|| "Unknown".to_string())
61 }
62 };
63
64 let message = unsafe { read_exception_property(exception, "message") };
65 let code = unsafe { read_exception_property_long(exception, "code") };
66 let file = unsafe { read_exception_property(exception, "file") };
67 let line = unsafe { read_exception_property_long(exception, "line") }
68 .try_into()
69 .unwrap_or(0);
70
71 Some(Self {
72 class_name,
73 message,
74 code,
75 file,
76 line,
77 })
78 }
79
80 #[must_use]
101 pub fn backtrace(&self) -> Option<Vec<super::BacktraceFrame>> {
102 let eg = unsafe { crate::ffi::ext_php_rs_executor_globals().as_ref()? };
103 let mut execute_data = eg.current_execute_data;
104
105 let mut frames = Vec::new();
106 while !execute_data.is_null() {
107 if let Some(frame) = unsafe { super::BacktraceFrame::from_execute_data(execute_data) } {
108 frames.push(frame);
109 }
110 execute_data = unsafe { (*execute_data).prev_execute_data };
111 }
112
113 if frames.is_empty() {
114 None
115 } else {
116 Some(frames)
117 }
118 }
119}
120
121unsafe fn read_exception_property(exception: *mut ffi::zend_object, name: &str) -> Option<String> {
125 let ce = unsafe { (*exception).ce };
126 if ce.is_null() {
127 return None;
128 }
129
130 let name_cstr = std::ffi::CString::new(name).ok()?;
131 let mut rv = std::mem::MaybeUninit::<ffi::zval>::uninit();
132
133 let prop = unsafe {
134 ffi::zend_read_property(
135 ce,
136 exception,
137 name_cstr.as_ptr(),
138 name.len(),
139 true,
140 rv.as_mut_ptr(),
141 )
142 };
143
144 if prop.is_null() {
145 return None;
146 }
147
148 let prop_ref = unsafe { &*prop };
149 let type_info = unsafe { prop_ref.u1.type_info };
150
151 if (type_info & 0xFF) == ffi::IS_STRING {
152 let zs = unsafe { prop_ref.value.str_ };
153 unsafe { zend_string_to_string(zs) }
154 } else {
155 None
156 }
157}
158
159unsafe fn read_exception_property_long(exception: *mut ffi::zend_object, name: &str) -> i64 {
163 let ce = unsafe { (*exception).ce };
164 if ce.is_null() {
165 return 0;
166 }
167
168 let Ok(name_cstr) = std::ffi::CString::new(name) else {
169 return 0;
170 };
171 let mut rv = std::mem::MaybeUninit::<ffi::zval>::uninit();
172
173 let prop = unsafe {
174 ffi::zend_read_property(
175 ce,
176 exception,
177 name_cstr.as_ptr(),
178 name.len(),
179 true,
180 rv.as_mut_ptr(),
181 )
182 };
183
184 if prop.is_null() {
185 return 0;
186 }
187
188 let prop_ref = unsafe { &*prop };
189 let type_info = unsafe { prop_ref.u1.type_info };
190
191 if (type_info & 0xFF) == ffi::IS_LONG {
192 unsafe { prop_ref.value.lval }
193 } else {
194 0
195 }
196}
197
198unsafe fn zend_string_to_string(zs: *mut ffi::zend_string) -> Option<String> {
202 if zs.is_null() {
203 return None;
204 }
205 let len = unsafe { (*zs).len };
206 let ptr = unsafe { (*zs).val.as_ptr() };
207 let slice = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
208 std::str::from_utf8(slice).ok().map(String::from)
209}
210
211pub trait ExceptionObserver: 'static {
243 fn on_exception(&self, exception: &ExceptionInfo);
248}
249
250type ExceptionObserverFactory =
251 Box<dyn Fn() -> Box<dyn ExceptionObserver + Send + Sync> + Send + Sync>;
252
253static EXCEPTION_OBSERVER_FACTORY: OnceLock<ExceptionObserverFactory> = OnceLock::new();
254static EXCEPTION_OBSERVER_INSTANCE: OnceLock<Box<dyn ExceptionObserver + Send + Sync>> =
255 OnceLock::new();
256
257type ThrowHook = unsafe extern "C" fn(*mut ffi::zend_object);
258
259static PREVIOUS_HOOK: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut());
260
261fn get_exception_observer() -> Option<&'static (dyn ExceptionObserver + Send + Sync)> {
262 EXCEPTION_OBSERVER_INSTANCE
263 .get()
264 .map(std::convert::AsRef::as_ref)
265}
266
267unsafe extern "C" fn exception_observer_callback(exception: *mut ffi::zend_object) {
271 if let Some(observer) = get_exception_observer()
272 && let Some(info) = (unsafe { ExceptionInfo::from_zend_object(exception) })
273 {
274 observer.on_exception(&info);
275 }
276
277 let prev = PREVIOUS_HOOK.load(Ordering::Acquire);
278 if !prev.is_null() {
279 let prev = unsafe { std::mem::transmute::<*mut (), ThrowHook>(prev) };
282 unsafe { prev(exception) };
283 }
284}
285
286pub(crate) fn register_exception_observer_factory(factory: ExceptionObserverFactory) {
290 assert!(
291 EXCEPTION_OBSERVER_FACTORY.set(factory).is_ok(),
292 "exception_observer can only be registered once per extension"
293 );
294}
295
296pub(crate) unsafe fn exception_observer_startup() {
300 let Some(factory) = EXCEPTION_OBSERVER_FACTORY.get() else {
301 return;
302 };
303 EXCEPTION_OBSERVER_INSTANCE.get_or_init(factory);
304
305 let ours: ThrowHook = exception_observer_callback;
306 let prev = unsafe { ffi::zend_throw_exception_hook }
307 .filter(|&prev| !std::ptr::fn_addr_eq(prev, ours))
308 .map_or(std::ptr::null_mut(), |prev| prev as *mut ());
309 PREVIOUS_HOOK.store(prev, Ordering::Release);
310
311 unsafe {
312 ffi::zend_throw_exception_hook = Some(ours);
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 struct TestExceptionObserver;
321
322 unsafe impl Send for TestExceptionObserver {}
323 unsafe impl Sync for TestExceptionObserver {}
324
325 impl ExceptionObserver for TestExceptionObserver {
326 fn on_exception(&self, _exception: &ExceptionInfo) {}
327 }
328
329 #[test]
330 fn test_exception_info_fields() {
331 let info = ExceptionInfo {
332 class_name: "RuntimeException".to_string(),
333 message: Some("Test error".to_string()),
334 code: 42,
335 file: Some("/path/to/file.php".to_string()),
336 line: 100,
337 };
338
339 assert_eq!(info.class_name, "RuntimeException");
340 assert_eq!(info.message, Some("Test error".to_string()));
341 assert_eq!(info.code, 42);
342 assert_eq!(info.file, Some("/path/to/file.php".to_string()));
343 assert_eq!(info.line, 100);
344 }
345
346 #[test]
347 fn test_observer_trait_impl() {
348 let observer = TestExceptionObserver;
349 let info = ExceptionInfo {
350 class_name: "Exception".to_string(),
351 message: None,
352 code: 0,
353 file: None,
354 line: 0,
355 };
356 observer.on_exception(&info);
357 }
358}