use std::sync::OnceLock;
use crate::ffi;
use crate::types::Zval;
use crate::zend::ExecuteData;
#[derive(Debug, Clone)]
pub struct FcallInfo<'a> {
pub function_name: Option<&'a str>,
pub class_name: Option<&'a str>,
pub filename: Option<&'a str>,
pub lineno: u32,
pub is_internal: bool,
}
impl FcallInfo<'_> {
fn empty() -> Self {
Self {
function_name: None,
class_name: None,
filename: None,
lineno: 0,
is_internal: false,
}
}
}
pub trait FcallObserver: 'static {
fn should_observe(&self, info: &FcallInfo) -> bool;
fn begin(&self, execute_data: &ExecuteData);
fn end(&self, execute_data: &ExecuteData, retval: Option<&Zval>);
}
type ObserverFactory = Box<dyn Fn() -> Box<dyn FcallObserver + Send + Sync> + Send + Sync>;
static OBSERVER_FACTORY: OnceLock<ObserverFactory> = OnceLock::new();
static OBSERVER_INSTANCE: OnceLock<Box<dyn FcallObserver + Send + Sync>> = OnceLock::new();
impl FcallInfo<'_> {
pub(crate) unsafe fn from_execute_data(
execute_data: *mut ffi::zend_execute_data,
) -> FcallInfo<'static> {
if execute_data.is_null() {
return FcallInfo::empty();
}
let func = unsafe { (*execute_data).func };
if func.is_null() {
return FcallInfo::empty();
}
let common = unsafe { &(*func).common };
let func_type = common.type_;
#[allow(clippy::cast_possible_truncation)]
let is_internal = func_type == ffi::ZEND_INTERNAL_FUNCTION as u8;
let function_name = if common.function_name.is_null() {
None
} else {
unsafe { zend_string_to_str(common.function_name) }
};
let class_name = if common.scope.is_null() {
None
} else {
let ce = unsafe { &*common.scope };
if ce.name.is_null() {
None
} else {
unsafe { zend_string_to_str(ce.name) }
}
};
let (filename, lineno) = if is_internal {
(None, 0)
} else {
let op_array = unsafe { &(*func).op_array };
let filename = if op_array.filename.is_null() {
None
} else {
unsafe { zend_string_to_str(op_array.filename) }
};
(filename, op_array.line_start)
};
FcallInfo {
function_name,
class_name,
filename,
lineno,
is_internal,
}
}
}
unsafe fn zend_string_to_str(zs: *mut ffi::zend_string) -> Option<&'static str> {
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()
}
fn get_observer() -> Option<&'static (dyn FcallObserver + Send + Sync)> {
OBSERVER_INSTANCE.get().map(std::convert::AsRef::as_ref)
}
unsafe extern "C" fn observer_fcall_init(
execute_data: *mut ffi::zend_execute_data,
) -> ffi::zend_observer_fcall_handlers {
let empty = ffi::zend_observer_fcall_handlers {
begin: None,
end: None,
};
let Some(observer) = get_observer() else {
return empty;
};
let info = unsafe { FcallInfo::from_execute_data(execute_data) };
if observer.should_observe(&info) {
ffi::zend_observer_fcall_handlers {
begin: Some(observer_begin),
end: Some(observer_end),
}
} else {
empty
}
}
unsafe extern "C" fn observer_begin(execute_data: *mut ffi::zend_execute_data) {
if let Some(observer) = get_observer()
&& let Some(ex) = unsafe { execute_data.as_ref() }
{
observer.begin(ex);
}
}
unsafe extern "C" fn observer_end(
execute_data: *mut ffi::zend_execute_data,
retval: *mut ffi::zval,
) {
if let Some(observer) = get_observer()
&& let Some(ex) = unsafe { execute_data.as_ref() }
{
let retval_ref = if retval.is_null() {
None
} else {
Some(unsafe { &*retval.cast_const().cast::<Zval>() })
};
observer.end(ex, retval_ref);
}
}
pub(crate) fn register_fcall_observer_factory(factory: ObserverFactory) {
assert!(
OBSERVER_FACTORY.set(factory).is_ok(),
"fcall_observer can only be registered once per extension"
);
}
pub(crate) unsafe fn observer_startup() {
if let Some(factory) = OBSERVER_FACTORY.get() {
if OBSERVER_INSTANCE.set(factory()).is_err() {
return;
}
unsafe { ffi::zend_observer_fcall_register(Some(observer_fcall_init)) };
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestObserver {
observe_all: bool,
}
unsafe impl Send for TestObserver {}
unsafe impl Sync for TestObserver {}
impl FcallObserver for TestObserver {
fn should_observe(&self, _info: &FcallInfo) -> bool {
self.observe_all
}
fn begin(&self, _execute_data: &ExecuteData) {}
fn end(&self, _execute_data: &ExecuteData, _retval: Option<&Zval>) {}
}
#[test]
fn test_fcall_info_empty() {
let info = FcallInfo::empty();
assert!(info.function_name.is_none());
assert!(info.class_name.is_none());
assert!(info.filename.is_none());
assert_eq!(info.lineno, 0);
assert!(!info.is_internal);
}
#[test]
fn test_observer_trait_impl() {
let observer = TestObserver { observe_all: true };
let info = FcallInfo::empty();
assert!(observer.should_observe(&info));
let observer = TestObserver { observe_all: false };
assert!(!observer.should_observe(&info));
}
}