ext_php_rs/zend/
observer.rs1use std::sync::OnceLock;
23
24use crate::ffi;
25use crate::types::Zval;
26use crate::zend::ExecuteData;
27
28#[derive(Debug, Clone)]
30pub struct FcallInfo<'a> {
31 pub function_name: Option<&'a str>,
33 pub class_name: Option<&'a str>,
35 pub filename: Option<&'a str>,
37 pub lineno: u32,
39 pub is_internal: bool,
41}
42
43impl FcallInfo<'_> {
44 fn empty() -> Self {
45 Self {
46 function_name: None,
47 class_name: None,
48 filename: None,
49 lineno: 0,
50 is_internal: false,
51 }
52 }
53}
54
55pub trait FcallObserver: 'static {
67 fn should_observe(&self, info: &FcallInfo) -> bool;
69
70 fn begin(&self, execute_data: &ExecuteData);
72
73 fn end(&self, execute_data: &ExecuteData, retval: Option<&Zval>);
75}
76
77type ObserverFactory = Box<dyn Fn() -> Box<dyn FcallObserver + Send + Sync> + Send + Sync>;
78
79static OBSERVER_FACTORY: OnceLock<ObserverFactory> = OnceLock::new();
80static OBSERVER_INSTANCE: OnceLock<Box<dyn FcallObserver + Send + Sync>> = OnceLock::new();
81
82impl FcallInfo<'_> {
83 pub(crate) unsafe fn from_execute_data(
87 execute_data: *mut ffi::zend_execute_data,
88 ) -> FcallInfo<'static> {
89 if execute_data.is_null() {
90 return FcallInfo::empty();
91 }
92
93 let func = unsafe { (*execute_data).func };
94 if func.is_null() {
95 return FcallInfo::empty();
96 }
97
98 let common = unsafe { &(*func).common };
99 let func_type = common.type_;
100 #[allow(clippy::cast_possible_truncation)]
101 let is_internal = func_type == ffi::ZEND_INTERNAL_FUNCTION as u8;
102
103 let function_name = if common.function_name.is_null() {
104 None
105 } else {
106 unsafe { zend_string_to_str(common.function_name) }
107 };
108
109 let class_name = if common.scope.is_null() {
110 None
111 } else {
112 let ce = unsafe { &*common.scope };
113 if ce.name.is_null() {
114 None
115 } else {
116 unsafe { zend_string_to_str(ce.name) }
117 }
118 };
119
120 let (filename, lineno) = if is_internal {
121 (None, 0)
122 } else {
123 let op_array = unsafe { &(*func).op_array };
124 let filename = if op_array.filename.is_null() {
125 None
126 } else {
127 unsafe { zend_string_to_str(op_array.filename) }
128 };
129 (filename, op_array.line_start)
130 };
131
132 FcallInfo {
133 function_name,
134 class_name,
135 filename,
136 lineno,
137 is_internal,
138 }
139 }
140}
141
142unsafe fn zend_string_to_str(zs: *mut ffi::zend_string) -> Option<&'static str> {
146 if zs.is_null() {
147 return None;
148 }
149 let len = unsafe { (*zs).len };
150 let ptr = unsafe { (*zs).val.as_ptr() };
151 let slice = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
152 std::str::from_utf8(slice).ok()
153}
154
155fn get_observer() -> Option<&'static (dyn FcallObserver + Send + Sync)> {
156 OBSERVER_INSTANCE.get().map(std::convert::AsRef::as_ref)
157}
158
159unsafe extern "C" fn observer_fcall_init(
163 execute_data: *mut ffi::zend_execute_data,
164) -> ffi::zend_observer_fcall_handlers {
165 let empty = ffi::zend_observer_fcall_handlers {
166 begin: None,
167 end: None,
168 };
169
170 let Some(observer) = get_observer() else {
171 return empty;
172 };
173
174 let info = unsafe { FcallInfo::from_execute_data(execute_data) };
175
176 if observer.should_observe(&info) {
177 ffi::zend_observer_fcall_handlers {
178 begin: Some(observer_begin),
179 end: Some(observer_end),
180 }
181 } else {
182 empty
183 }
184}
185
186unsafe extern "C" fn observer_begin(execute_data: *mut ffi::zend_execute_data) {
190 if let Some(observer) = get_observer()
191 && let Some(ex) = unsafe { execute_data.as_ref() }
192 {
193 observer.begin(ex);
194 }
195}
196
197unsafe extern "C" fn observer_end(
201 execute_data: *mut ffi::zend_execute_data,
202 retval: *mut ffi::zval,
203) {
204 if let Some(observer) = get_observer()
205 && let Some(ex) = unsafe { execute_data.as_ref() }
206 {
207 let retval_ref = if retval.is_null() {
208 None
209 } else {
210 Some(unsafe { &*retval.cast_const().cast::<Zval>() })
211 };
212 observer.end(ex, retval_ref);
213 }
214}
215
216pub(crate) fn register_fcall_observer_factory(factory: ObserverFactory) {
220 assert!(
221 OBSERVER_FACTORY.set(factory).is_ok(),
222 "fcall_observer can only be registered once per extension"
223 );
224}
225
226pub(crate) unsafe fn observer_startup() {
230 let Some(factory) = OBSERVER_FACTORY.get() else {
231 return;
232 };
233 OBSERVER_INSTANCE.get_or_init(factory);
234 unsafe { ffi::zend_observer_fcall_register(Some(observer_fcall_init)) };
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 struct TestObserver {
242 observe_all: bool,
243 }
244
245 unsafe impl Send for TestObserver {}
246 unsafe impl Sync for TestObserver {}
247
248 impl FcallObserver for TestObserver {
249 fn should_observe(&self, _info: &FcallInfo) -> bool {
250 self.observe_all
251 }
252
253 fn begin(&self, _execute_data: &ExecuteData) {}
254
255 fn end(&self, _execute_data: &ExecuteData, _retval: Option<&Zval>) {}
256 }
257
258 #[test]
259 fn test_fcall_info_empty() {
260 let info = FcallInfo::empty();
261 assert!(info.function_name.is_none());
262 assert!(info.class_name.is_none());
263 assert!(info.filename.is_none());
264 assert_eq!(info.lineno, 0);
265 assert!(!info.is_internal);
266 }
267
268 #[test]
269 fn test_observer_trait_impl() {
270 let observer = TestObserver { observe_all: true };
271 let info = FcallInfo::empty();
272 assert!(observer.should_observe(&info));
273
274 let observer = TestObserver { observe_all: false };
275 assert!(!observer.should_observe(&info));
276 }
277}