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