Skip to main content

ext_php_rs/zend/
observer.rs

1//! Zend Observer API bindings for function call observation.
2//!
3//! Enables creation of profilers, tracers, and instrumentation tools.
4//!
5//! # Example
6//!
7//! ```ignore
8//! use ext_php_rs::prelude::*;
9//!
10//! struct MyProfiler;
11//!
12//! impl FcallObserver for MyProfiler {
13//!     fn should_observe(&self, info: &FcallInfo) -> bool {
14//!         !info.is_internal
15//!     }
16//!
17//!     fn begin(&self, _execute_data: &ExecuteData) {}
18//!     fn end(&self, _execute_data: &ExecuteData, _retval: Option<&Zval>) {}
19//! }
20//! ```
21
22use std::sync::OnceLock;
23
24use crate::ffi;
25use crate::types::Zval;
26use crate::zend::ExecuteData;
27
28/// Metadata about a function being called.
29#[derive(Debug, Clone)]
30pub struct FcallInfo<'a> {
31    /// Function name (`None` for anonymous functions or main script).
32    pub function_name: Option<&'a str>,
33    /// Class name for method calls.
34    pub class_name: Option<&'a str>,
35    /// Source filename (`None` for internal functions).
36    pub filename: Option<&'a str>,
37    /// Line number (0 for internal functions).
38    pub lineno: u32,
39    /// Whether this is an internal (built-in) PHP function.
40    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
55/// Trait for observing PHP function calls.
56///
57/// # Lifecycle
58///
59/// 1. `should_observe` is called once per unique function (cached by PHP)
60/// 2. If `true`, `begin` is called at function entry
61/// 3. `end` is called at function exit (even on exceptions)
62///
63/// # Thread Safety
64///
65/// Observer must be `Send + Sync`. Use thread-safe primitives for mutable state.
66pub trait FcallObserver: 'static {
67    /// Whether to observe a specific function (result is cached by PHP).
68    fn should_observe(&self, info: &FcallInfo) -> bool;
69
70    /// Called when an observed function begins execution.
71    fn begin(&self, execute_data: &ExecuteData);
72
73    /// Called when an observed function ends execution (even on exceptions).
74    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    /// # Safety
84    ///
85    /// `execute_data` must be a valid pointer.
86    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
142/// # Safety
143///
144/// Pointer must be valid and string must be valid UTF-8.
145unsafe 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
159/// # Safety
160///
161/// Called from PHP's C code.
162unsafe 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
186/// # Safety
187///
188/// Called from PHP's C code.
189unsafe 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
197/// # Safety
198///
199/// Called from PHP's C code.
200unsafe 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
216/// # Panics
217///
218/// Panics if called more than once.
219pub(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
226/// # Safety
227///
228/// Must be called during MINIT phase only.
229pub(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}