use std::{
ffi::CString, ffi::c_void, mem::MaybeUninit, os::raw::c_int, panic::AssertUnwindSafe,
panic::catch_unwind, ptr,
};
use crate::{
class::RegisteredClass,
error::php_error,
exception::PhpResult,
ffi::{
BP_VAR_IS, BP_VAR_R, ZEND_ACC_STATIC, ext_php_rs_executor_globals,
instanceof_function_slow, std_object_handlers, zend_class_entry, zend_hash_find,
zend_hash_str_find, zend_is_true, zend_object_handlers, zend_object_std_dtor,
zend_objects_clone_members, zend_property_info, zend_std_compare_objects,
zend_std_get_properties, zend_std_get_property_ptr_ptr, zend_std_has_property,
zend_std_read_property, zend_std_unset_property, zend_std_write_property, zend_throw_error,
},
flags::ErrorType,
flags::{PropertyFlags, ZvalTypeFlags},
internal::property::PropertyDescriptor,
types::{ZendClassObject, ZendHashTable, ZendObject, ZendStr, Zval},
zend::{ExecutorGlobals, catch_panic},
};
const UNCOMPARABLE: c_int = 1;
pub type ZendObjectHandlers = zend_object_handlers;
impl ZendObjectHandlers {
#[must_use]
pub fn new<T: RegisteredClass>() -> ZendObjectHandlers {
let mut this = MaybeUninit::uninit();
unsafe { Self::init::<T>(&raw mut *this.as_mut_ptr()) };
unsafe { this.assume_init() }
}
#[expect(
clippy::expect_used,
reason = "the property offset is computed from a struct layout and fits in the engine's offset type"
)]
pub unsafe fn init<T: RegisteredClass>(ptr: *mut ZendObjectHandlers) {
unsafe { ptr::copy_nonoverlapping(&raw const std_object_handlers, ptr, 1) };
let offset = ZendClassObject::<T>::std_offset();
unsafe { (*ptr).offset = offset.try_into().expect("Invalid offset") };
unsafe { (*ptr).free_obj = Some(Self::free_obj::<T>) };
unsafe { (*ptr).clone_obj = Some(Self::clone_obj::<T>) };
unsafe { (*ptr).read_property = Some(Self::read_property::<T>) };
unsafe { (*ptr).write_property = Some(Self::write_property::<T>) };
unsafe { (*ptr).get_properties = Some(Self::get_properties::<T>) };
unsafe { (*ptr).get_gc = Some(Self::get_gc) };
unsafe { (*ptr).has_property = Some(Self::has_property::<T>) };
unsafe { (*ptr).get_property_ptr_ptr = Some(Self::get_property_ptr_ptr::<T>) };
unsafe { (*ptr).unset_property = Some(Self::unset_property::<T>) };
unsafe { (*ptr).compare = Some(Self::compare::<T>) };
}
unsafe extern "C" fn get_gc(
object: *mut ZendObject,
table: *mut *mut Zval,
n: *mut c_int,
) -> *mut ZendHashTable {
let obj = unsafe { &mut *object };
if obj.properties.is_null() {
unsafe {
*table = obj.properties_table.as_mut_ptr();
*n = (*obj.ce).default_properties_count;
}
ptr::null_mut()
} else {
unsafe {
*table = ptr::null_mut();
*n = 0;
}
obj.properties
}
}
unsafe extern "C" fn free_obj<T: RegisteredClass>(object: *mut ZendObject) {
if let Some(obj) = unsafe {
object
.as_mut()
.and_then(|obj| ZendClassObject::<T>::from_zend_obj_mut(obj))
} {
let dropped = catch_unwind(AssertUnwindSafe(|| unsafe {
ptr::drop_in_place(&raw mut obj.obj);
}));
if let Err(payload) = dropped {
let message = payload
.downcast_ref::<&str>()
.map(|s| (*s).to_owned())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "non-string panic payload".to_owned());
php_error(
&ErrorType::Warning,
&format!("Rust panic in Drop for {}: {message}", T::CLASS_NAME),
);
}
}
unsafe { zend_object_std_dtor(object) };
}
#[expect(
clippy::expect_used,
reason = "the formatted message is built from a class name and contains no NUL byte"
)]
unsafe extern "C" fn clone_obj<T: RegisteredClass>(object: *mut ZendObject) -> *mut ZendObject {
let cloned_val = catch_panic(|| {
Ok(unsafe {
object
.as_ref()
.and_then(|obj| ZendClassObject::<T>::from_zend_obj(obj))
.and_then(|old| old.obj.as_ref())
.and_then(RegisteredClass::clone_obj)
})
});
match cloned_val {
Ok(Some(val)) => {
let mut new = ZendClassObject::<T>::new(val);
unsafe { zend_objects_clone_members(&raw mut new.std, object) };
let raw = new.into_raw();
unsafe { &raw mut (*raw).std }
}
Ok(None) => {
let msg = CString::new(format!(
"Trying to clone an uncloneable object of class {}",
T::CLASS_NAME
))
.expect("Failed to create error message");
unsafe { zend_throw_error(ptr::null_mut(), msg.as_ptr()) };
Self::released_placeholder::<T>()
}
Err(panic) => {
panic.throw();
Self::released_placeholder::<T>()
}
}
}
fn released_placeholder<T: RegisteredClass>() -> *mut ZendObject {
let empty = unsafe { ZendClassObject::<T>::new_uninit(None) };
let raw = empty.into_raw();
unsafe { &raw mut (*raw).std }
}
#[allow(clippy::items_after_statements)]
unsafe extern "C" fn read_property<T: RegisteredClass>(
object: *mut ZendObject,
member: *mut ZendStr,
type_: c_int,
cache_slot: *mut *mut c_void,
rv: *mut Zval,
) -> *mut Zval {
let Some(obj) = (unsafe {
object
.as_mut()
.and_then(|obj| ZendClassObject::<T>::from_zend_obj_mut(obj))
}) else {
let cache_slot = unsafe { std_cache_slot::<T>(member, cache_slot) };
return unsafe { zend_std_read_property(object, member, type_, cache_slot, rv) };
};
#[allow(clippy::inline_always)]
#[inline(always)]
unsafe fn internal<T: RegisteredClass>(
object: *mut ZendObject,
this: &T,
member: *mut ZendStr,
type_: c_int,
cache_slot: *mut *mut c_void,
rv: *mut Zval,
) -> PhpResult<(*mut Zval, Option<Zval>)> {
let prop = unsafe { resolve_property::<T>(member, cache_slot)? };
let rv_mut = unsafe { rv.as_mut().ok_or("Invalid return zval given")? };
rv_mut.u1.type_info = ZvalTypeFlags::Null.bits();
Ok(match prop {
Some(prop_info) => {
let object_ce = unsafe { (*object).ce };
if !unsafe { check_property_access(prop_info.flags, object_ce) } {
let is_private = prop_info.flags.contains(PropertyFlags::Private);
let prop_name = unsafe {
member
.as_ref()
.ok_or("Invalid property name pointer given")?
};
unsafe {
throw_property_access_error(
T::CLASS_NAME,
prop_name.as_str()?,
is_private,
);
}
return Ok((rv, None));
}
let getter = prop_info
.get
.ok_or("No getter available for this property.")?;
getter(this, rv_mut)?;
if !matches!(u32::try_from(type_), Ok(BP_VAR_R | BP_VAR_IS)) {
if !rv_mut.is_object() && !rv_mut.is_reference() {
let class = unsafe { (*object).ce.as_ref() }
.and_then(|ce| ce.name())
.unwrap_or(T::CLASS_NAME);
let name = unsafe { member.as_ref().ok_or("Invalid property name")? };
php_error(
&ErrorType::Notice,
&format!(
"Indirect modification of overloaded property {class}::${} has no effect",
name.as_str()?
),
);
}
return Ok((rv, None));
}
if !cache_slot.is_null() {
return Ok((rv, None));
}
let info = unsafe {
zend_hash_find(&raw const T::get_metadata().ce().properties_info, member)
};
match unsafe { declared_slot(object, info) } {
Some(slot) => (slot, Some(unsafe { store_in_slot(slot, rv_mut) })),
None => (rv, None),
}
}
None => (
unsafe { zend_std_read_property(object, member, type_, cache_slot, rv) },
None,
),
})
}
match catch_panic(|| unsafe {
internal::<T>(object, &**obj, member, type_, cache_slot, rv)
}) {
Ok((retval, released)) => {
drop(released);
retval
}
Err(e) => {
e.throw();
unsafe { (*rv).set_null() };
rv
}
}
}
#[allow(clippy::items_after_statements)]
unsafe extern "C" fn write_property<T: RegisteredClass>(
object: *mut ZendObject,
member: *mut ZendStr,
value: *mut Zval,
cache_slot: *mut *mut c_void,
) -> *mut Zval {
let Some(obj) = (unsafe {
object
.as_mut()
.and_then(|obj| ZendClassObject::<T>::from_zend_obj_mut(obj))
}) else {
let cache_slot = unsafe { std_cache_slot::<T>(member, cache_slot) };
return unsafe { zend_std_write_property(object, member, value, cache_slot) };
};
#[allow(clippy::inline_always)]
#[inline(always)]
unsafe fn internal<T: RegisteredClass>(
object: *mut ZendObject,
obj: &mut ZendClassObject<T>,
member: *mut ZendStr,
value: *mut Zval,
cache_slot: *mut *mut c_void,
) -> PhpResult<*mut Zval> {
let self_ = &mut *obj;
let prop = unsafe { resolve_property::<T>(member, cache_slot)? };
let value_mut = unsafe { value.as_mut().ok_or("Invalid return zval given")? };
Ok(match prop {
Some(prop_info) => {
let object_ce = unsafe { (*object).ce };
if !unsafe { check_property_access(prop_info.flags, object_ce) } {
let is_private = prop_info.flags.contains(PropertyFlags::Private);
let prop_name = unsafe {
member
.as_ref()
.ok_or("Invalid property name pointer given")?
};
unsafe {
throw_property_access_error(
T::CLASS_NAME,
prop_name.as_str()?,
is_private,
);
}
return Ok(value);
}
let setter = prop_info
.set
.ok_or("No setter available for this property.")?;
setter(self_, value_mut)?;
value
}
None => unsafe { zend_std_write_property(object, member, value, cache_slot) },
})
}
match catch_panic(|| unsafe { internal::<T>(object, obj, member, value, cache_slot) }) {
Ok(rv) => rv,
Err(e) => {
e.throw();
value
}
}
}
#[allow(clippy::items_after_statements)]
unsafe extern "C" fn get_properties<T: RegisteredClass>(
object: *mut ZendObject,
) -> *mut ZendHashTable {
let props = unsafe { zend_std_get_properties(object) };
if props.is_null() {
return props;
}
let Some(obj) = (unsafe {
object
.as_ref()
.and_then(|obj| ZendClassObject::<T>::from_zend_obj(obj))
}) else {
return props;
};
let mut released = Vec::new();
if let Err(e) = catch_panic(|| {
let _ = unsafe { refresh_slots::<T>(object, &**obj, &mut released) };
Ok(())
}) {
e.throw();
}
drop(released);
props
}
unsafe extern "C" fn get_property_ptr_ptr<T: RegisteredClass>(
object: *mut ZendObject,
member: *mut ZendStr,
type_: c_int,
cache_slot: *mut *mut c_void,
) -> *mut Zval {
let backed = unsafe {
object
.as_ref()
.and_then(|obj| ZendClassObject::<T>::from_zend_obj(obj))
}
.is_some();
if !backed {
let cache_slot = unsafe { std_cache_slot::<T>(member, cache_slot) };
return unsafe { zend_std_get_property_ptr_ptr(object, member, type_, cache_slot) };
}
match catch_panic(|| unsafe { resolve_property::<T>(member, cache_slot) }) {
Ok(Some(_)) => {
if !cache_slot.is_null() {
unsafe { cache_slot.add(2).write(ptr::null_mut()) };
}
ptr::null_mut()
}
Ok(None) => unsafe { zend_std_get_property_ptr_ptr(object, member, type_, cache_slot) },
Err(e) => {
e.throw();
ptr::null_mut()
}
}
}
unsafe extern "C" fn unset_property<T: RegisteredClass>(
object: *mut ZendObject,
member: *mut ZendStr,
cache_slot: *mut *mut c_void,
) {
let backed = unsafe {
object
.as_ref()
.and_then(|obj| ZendClassObject::<T>::from_zend_obj(obj))
}
.is_some();
if !backed {
let cache_slot = unsafe { std_cache_slot::<T>(member, cache_slot) };
return unsafe { zend_std_unset_property(object, member, cache_slot) };
}
let refused = catch_panic(|| {
let Some(prop) = (unsafe { resolve_property::<T>(member, cache_slot)? }) else {
return Ok(false);
};
let name = unsafe { member.as_ref().ok_or("Invalid property name")? }.as_str()?;
if unsafe { check_property_access(prop.flags, (*object).ce) } {
unsafe { throw_unset_error(T::CLASS_NAME, name) };
} else {
let is_private = prop.flags.contains(PropertyFlags::Private);
unsafe { throw_property_access_error(T::CLASS_NAME, name, is_private) };
}
Ok(true)
});
match refused {
Ok(true) => {}
Ok(false) => unsafe { zend_std_unset_property(object, member, cache_slot) },
Err(e) => e.throw(),
}
}
unsafe extern "C" fn compare<T: RegisteredClass>(o1: *mut Zval, o2: *mut Zval) -> c_int {
let objects = unsafe { o1.as_ref().zip(o2.as_ref()) }
.filter(|(a, b)| a.is_object() && b.is_object())
.map(|(a, b)| unsafe { (a.value.obj, b.value.obj) })
.filter(|&(a, b)| a != b && unsafe { (*a).ce == (*b).ce });
let Some((a, b)) = objects else {
return unsafe { zend_std_compare_objects(o1, o2) };
};
let mut released = Vec::new();
let refreshed = catch_panic(|| {
for object in [a, b] {
let Some(obj) = (unsafe {
object
.as_ref()
.and_then(|obj| ZendClassObject::<T>::from_zend_obj(obj))
}) else {
continue;
};
unsafe { refresh_slots::<T>(object, &**obj, &mut released) }?;
}
Ok(())
});
drop(released);
match refreshed {
Ok(()) if !ExecutorGlobals::has_exception() => unsafe {
zend_std_compare_objects(o1, o2)
},
Ok(()) => UNCOMPARABLE,
Err(e) => {
e.throw();
UNCOMPARABLE
}
}
}
#[allow(clippy::items_after_statements)]
unsafe extern "C" fn has_property<T: RegisteredClass>(
object: *mut ZendObject,
member: *mut ZendStr,
has_set_exists: c_int,
cache_slot: *mut *mut c_void,
) -> c_int {
let Some(obj) = (unsafe {
object
.as_mut()
.and_then(|obj| ZendClassObject::<T>::from_zend_obj_mut(obj))
}) else {
let cache_slot = unsafe { std_cache_slot::<T>(member, cache_slot) };
return unsafe { zend_std_has_property(object, member, has_set_exists, cache_slot) };
};
#[allow(clippy::inline_always)]
#[inline(always)]
unsafe fn internal<T: RegisteredClass>(
object: *mut ZendObject,
obj: &mut ZendClassObject<T>,
member: *mut ZendStr,
has_set_exists: c_int,
cache_slot: *mut *mut c_void,
) -> PhpResult<c_int> {
let prop = unsafe { resolve_property::<T>(member, cache_slot)? };
let self_ = &*obj;
match has_set_exists {
0 => {
if let Some(val) = prop {
let getter = val.get.ok_or("No getter available for this property.")?;
let mut zv = Zval::new();
getter(self_, &mut zv)?;
return Ok(c_int::from(!zv.is_null()));
}
}
1 => {
if let Some(val) = prop {
let getter = val.get.ok_or("No getter available for this property.")?;
let mut zv = Zval::new();
getter(self_, &mut zv)?;
cfg_if::cfg_if! {
if #[cfg(php84)] {
#[allow(clippy::unnecessary_mut_passed)]
let truthy = unsafe { zend_is_true(&raw mut zv) };
} else {
#[allow(clippy::unnecessary_mut_passed)]
let truthy = unsafe { zend_is_true(&raw mut zv) } == 1;
}
}
return Ok(c_int::from(truthy));
}
}
2 => {
if prop.is_some() {
return Ok(1);
}
}
_ => return Err(
"Invalid value given for `has_set_exists` in struct `has_property` function."
.into(),
),
}
Ok(unsafe { zend_std_has_property(object, member, has_set_exists, cache_slot) })
}
match catch_panic(|| unsafe {
internal::<T>(object, obj, member, has_set_exists, cache_slot)
}) {
Ok(rv) => rv,
Err(e) => {
e.throw();
0
}
}
}
}
#[allow(clippy::inline_always)]
#[inline(always)]
unsafe fn resolve_property<T: RegisteredClass>(
member: *mut ZendStr,
cache_slot: *mut *mut c_void,
) -> PhpResult<Option<&'static PropertyDescriptor<T>>> {
let meta = T::get_metadata();
let meta_ptr = ptr::from_ref(meta).cast::<c_void>().cast_mut();
if !cache_slot.is_null() {
let guard = unsafe { *cache_slot.add(1) };
if guard == meta_ptr {
let desc = unsafe { &*(*cache_slot).cast::<PropertyDescriptor<T>>() };
return Ok(Some(desc));
}
}
let prop_name = unsafe {
member
.as_ref()
.ok_or("Invalid property name pointer given")?
};
let Some(descriptor) = meta.find_property(prop_name.as_str()?) else {
return Ok(None);
};
if !cache_slot.is_null() {
unsafe {
*cache_slot = ptr::from_ref(descriptor).cast::<c_void>().cast_mut();
*cache_slot.add(1) = meta_ptr;
}
}
Ok(Some(descriptor))
}
unsafe fn declared_slot(object: *mut ZendObject, info: *const Zval) -> Option<*mut Zval> {
let info = unsafe {
info.as_ref()?
.value
.ptr
.cast::<zend_property_info>()
.as_ref()?
};
if info.flags & ZEND_ACC_STATIC != 0 {
return None;
}
let offset = usize::try_from(info.offset).ok()?;
Some(unsafe { object.byte_add(offset).cast::<Zval>() })
}
#[must_use]
unsafe fn store_in_slot(slot: *mut Zval, value: &mut Zval) -> Zval {
let slot = unsafe { &mut *slot };
let mut previous = Zval::new();
previous.value = slot.value;
previous.u1 = slot.u1;
slot.value = value.value;
slot.u1 = value.u1;
value.u1.type_info = ZvalTypeFlags::Null.bits();
previous
}
unsafe fn refresh_slots<T: RegisteredClass>(
object: *mut ZendObject,
this: &T,
released: &mut Vec<Zval>,
) -> PhpResult {
let metadata = T::get_metadata();
let properties_info = &raw const metadata.ce().properties_info;
let mut result = Ok(());
for desc in metadata.all_properties() {
let Some(getter) = desc.get else { continue };
let info = unsafe {
zend_hash_str_find(properties_info, desc.name.as_ptr().cast(), desc.name.len())
};
let Some(slot) = (unsafe { declared_slot(object, info) }) else {
continue;
};
let mut zv = Zval::new();
if let Err(e) = getter(this, &mut zv) {
if result.is_ok() {
result = Err(e);
}
continue;
}
let previous = unsafe { store_in_slot(slot, &mut zv) };
if ZvalTypeFlags::from_bits_truncate(unsafe { previous.u1.type_info })
.contains(ZvalTypeFlags::RefCounted)
{
released.push(previous);
}
}
result
}
unsafe fn std_cache_slot<T: RegisteredClass>(
member: *mut ZendStr,
cache_slot: *mut *mut c_void,
) -> *mut *mut c_void {
let is_rust_prop = unsafe { member.as_ref() }
.and_then(|name| name.as_str().ok())
.is_some_and(|name| T::get_metadata().find_property(name).is_some());
if !is_rust_prop {
return cache_slot;
}
if !cache_slot.is_null() {
for i in 0..3 {
unsafe { cache_slot.add(i).write(ptr::null_mut()) };
}
}
ptr::null_mut()
}
#[inline]
unsafe fn get_calling_scope() -> *const zend_class_entry {
let eg = unsafe { ext_php_rs_executor_globals().as_ref() };
let Some(eg) = eg else {
return ptr::null();
};
let execute_data = eg.current_execute_data;
if execute_data.is_null() {
return ptr::null();
}
let func = unsafe { (*execute_data).func };
if func.is_null() {
return ptr::null();
}
unsafe { (*func).common.scope }
}
#[inline]
unsafe fn check_property_access(flags: PropertyFlags, object_ce: *const zend_class_entry) -> bool {
if !flags.contains(PropertyFlags::Private) && !flags.contains(PropertyFlags::Protected) {
return true;
}
let calling_scope = unsafe { get_calling_scope() };
if flags.contains(PropertyFlags::Private) {
return calling_scope == object_ce;
}
if flags.contains(PropertyFlags::Protected) {
if calling_scope.is_null() {
return false;
}
if calling_scope == object_ce {
return true;
}
unsafe {
instanceof_function_slow(calling_scope, object_ce)
|| instanceof_function_slow(object_ce, calling_scope)
}
} else {
true
}
}
unsafe fn throw_property_access_error(class_name: &str, prop_name: &str, is_private: bool) {
let visibility = if is_private { "private" } else { "protected" };
let message = CString::new(format!(
"Cannot access {visibility} property {class_name}::${prop_name}"
))
.unwrap_or_else(|_| c"Cannot access property".to_owned());
unsafe {
zend_throw_error(ptr::null_mut(), message.as_ptr());
}
}
unsafe fn throw_unset_error(class_name: &str, prop_name: &str) {
let message = CString::new(format!("Cannot unset property {class_name}::${prop_name}"))
.unwrap_or_else(|_| c"Cannot unset property".to_owned());
unsafe {
zend_throw_error(ptr::null_mut(), message.as_ptr());
}
}