use std::{
collections::HashMap,
convert::TryInto,
ffi::c_void,
fmt::Debug,
marker::PhantomData,
mem::{self, MaybeUninit},
ops::{Deref, DerefMut},
os::raw::c_int,
ptr::{self, NonNull},
sync::atomic::{AtomicBool, AtomicPtr, Ordering},
};
use crate::{
bindings::{
ext_php_rs_zend_object_alloc, ext_php_rs_zend_object_release, object_properties_init,
std_object_handlers, zend_call_known_function, zend_is_true, zend_object,
zend_object_handlers, zend_object_std_dtor, zend_object_std_init,
zend_objects_clone_members, zend_objects_new, zend_standard_class_def,
zend_std_get_properties, zend_std_has_property, zend_std_read_property,
zend_std_write_property, zend_string, HashTable, ZEND_ISEMPTY, ZEND_PROPERTY_EXISTS,
ZEND_PROPERTY_ISSET,
},
errors::{Error, Result},
php::{
boxed::{ZBox, ZBoxable},
class::ClassEntry,
enums::DataType,
exceptions::{PhpException, PhpResult},
execution_data::ExecutionData,
flags::ZvalTypeFlags,
function::FunctionBuilder,
globals::ExecutorGlobals,
},
};
use super::{
props::Property,
rc::PhpRc,
string::ZendStr,
zval::{FromZval, FromZvalMut, IntoZval, Zval},
};
pub type ZendObject = zend_object;
pub type ZendObjectHandlers = zend_object_handlers;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u32)]
pub enum PropertyQuery {
Isset = ZEND_PROPERTY_ISSET,
NotEmpty = ZEND_ISEMPTY,
Exists = ZEND_PROPERTY_EXISTS,
}
impl ZendObject {
pub fn new(ce: &ClassEntry) -> ZBox<Self> {
unsafe {
let ptr = zend_objects_new(ce as *const _ as *mut _);
ZBox::from_raw(
ptr.as_mut()
.expect("Failed to allocate memory for Zend object"),
)
}
}
pub fn new_stdclass() -> ZBox<Self> {
Self::new(unsafe {
zend_standard_class_def
.as_ref()
.expect("`stdClass` class instance not initialized yet")
})
}
pub fn from_class_object<T: RegisteredClass>(obj: ZBox<ZendClassObject<T>>) -> ZBox<Self> {
let this = obj.into_raw();
unsafe { ZBox::from_raw(&mut this.std) }
}
pub fn get_class_name(&self) -> Result<String> {
unsafe {
self.handlers()?
.get_class_name
.and_then(|f| f(self).as_ref())
.ok_or(Error::InvalidScope)
.and_then(|s| s.try_into())
}
}
pub fn is_instance<T: RegisteredClass>(&self) -> bool {
(self.ce as *const ClassEntry).eq(&(T::get_metadata().ce() as *const _))
}
pub fn get_property<'a, T>(&'a self, name: &str) -> Result<T>
where
T: FromZval<'a>,
{
if !self.has_property(name, PropertyQuery::Exists)? {
return Err(Error::InvalidProperty);
}
let mut name = ZendStr::new(name, false)?;
let mut rv = Zval::new();
let zv = unsafe {
self.handlers()?.read_property.ok_or(Error::InvalidScope)?(
self.mut_ptr(),
name.deref_mut(),
1,
std::ptr::null_mut(),
&mut rv,
)
.as_ref()
}
.ok_or(Error::InvalidScope)?;
T::from_zval(zv).ok_or_else(|| Error::ZvalConversion(zv.get_type()))
}
pub fn set_property(&mut self, name: &str, value: impl IntoZval) -> Result<()> {
let mut name = ZendStr::new(name, false)?;
let mut value = value.into_zval(false)?;
unsafe {
self.handlers()?.write_property.ok_or(Error::InvalidScope)?(
self,
name.deref_mut(),
&mut value,
std::ptr::null_mut(),
)
.as_ref()
}
.ok_or(Error::InvalidScope)?;
Ok(())
}
pub fn has_property(&self, name: &str, query: PropertyQuery) -> Result<bool> {
let mut name = ZendStr::new(name, false)?;
Ok(unsafe {
self.handlers()?.has_property.ok_or(Error::InvalidScope)?(
self.mut_ptr(),
name.deref_mut(),
query as _,
std::ptr::null_mut(),
)
} > 0)
}
pub fn get_properties(&self) -> Result<&HashTable> {
unsafe {
self.handlers()?
.get_properties
.and_then(|props| props(self.mut_ptr()).as_ref())
.ok_or(Error::InvalidScope)
}
}
#[inline]
unsafe fn handlers(&self) -> Result<&ZendObjectHandlers> {
self.handlers.as_ref().ok_or(Error::InvalidScope)
}
#[inline]
fn mut_ptr(&self) -> *mut Self {
(self as *const Self) as *mut Self
}
pub fn extract<'a, T>(&'a self) -> Result<T>
where
T: FromZendObject<'a>,
{
T::from_zend_object(self)
}
}
unsafe impl ZBoxable for ZendObject {
fn free(&mut self) {
unsafe { ext_php_rs_zend_object_release(self) }
}
}
impl Debug for ZendObject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut dbg = f.debug_struct(
self.get_class_name()
.unwrap_or_else(|_| "ZendObject".to_string())
.as_str(),
);
if let Ok(props) = self.get_properties() {
for (id, key, val) in props.iter() {
dbg.field(key.unwrap_or_else(|| id.to_string()).as_str(), val);
}
}
dbg.finish()
}
}
impl<'a> FromZval<'a> for &'a ZendObject {
const TYPE: DataType = DataType::Object(None);
fn from_zval(zval: &'a Zval) -> Option<Self> {
zval.object()
}
}
impl IntoZval for ZBox<ZendObject> {
const TYPE: DataType = DataType::Object(None);
fn set_zval(mut self, zv: &mut Zval, _: bool) -> Result<()> {
self.dec_count();
zv.set_object(self.into_raw());
Ok(())
}
}
pub trait FromZendObject<'a>: Sized {
fn from_zend_object(obj: &'a ZendObject) -> Result<Self>;
}
pub trait FromZendObjectMut<'a>: Sized {
fn from_zend_object_mut(obj: &'a mut ZendObject) -> Result<Self>;
}
pub trait IntoZendObject {
fn into_zend_object(self) -> Result<ZBox<ZendObject>>;
}
impl FromZendObject<'_> for String {
fn from_zend_object(obj: &ZendObject) -> Result<Self> {
let mut ret = Zval::new();
unsafe {
zend_call_known_function(
(*obj.ce).__tostring,
obj as *const _ as *mut _,
obj.ce,
&mut ret,
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
);
}
if let Some(err) = ExecutorGlobals::take_exception() {
let class_name = obj.get_class_name();
panic!(
"Uncaught exception during call to {}::__toString(): {:?}",
class_name.expect("unable to determine class name"),
err
);
} else if let Some(output) = ret.extract() {
Ok(output)
} else {
let class_name = obj.get_class_name();
panic!(
"{}::__toString() must return a string",
class_name.expect("unable to determine class name"),
);
}
}
}
pub struct ClassRef<'a, T: RegisteredClass + 'a> {
ptr: &'a mut ZendClassObject<T>,
}
impl<'a, T: RegisteredClass> ClassRef<'a, T> {
pub fn from_ref(obj: &'a T) -> Option<Self> {
let ptr = unsafe { ZendClassObject::from_obj_ptr(obj)? };
Some(Self { ptr })
}
}
impl<'a, T: RegisteredClass> IntoZval for ClassRef<'a, T> {
const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME));
#[inline]
fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
zv.set_object(&mut self.ptr.std);
Ok(())
}
}
impl<T: RegisteredClass> From<ZBox<ZendClassObject<T>>> for ZBox<ZendObject> {
#[inline]
fn from(obj: ZBox<ZendClassObject<T>>) -> Self {
ZendObject::from_class_object(obj)
}
}
impl<T: RegisteredClass + Default> Default for ZBox<ZendClassObject<T>> {
#[inline]
fn default() -> Self {
ZendClassObject::new(T::default())
}
}
impl<T: RegisteredClass + Clone> Clone for ZBox<ZendClassObject<T>> {
fn clone(&self) -> Self {
unsafe {
let mut new = ZendClassObject::new((&***self).clone());
zend_objects_clone_members(&mut new.std, &self.std as *const _ as *mut _);
new
}
}
}
impl<T: RegisteredClass + Debug> Debug for ZendClassObject<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
(&**self).fmt(f)
}
}
impl<T: RegisteredClass> IntoZval for ZBox<ZendClassObject<T>> {
const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME));
fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
let obj = self.into_raw();
zv.set_object(&mut obj.std);
Ok(())
}
}
pub struct ConstructorMeta<T> {
pub constructor: fn(&mut ExecutionData) -> ConstructorResult<T>,
pub build_fn: fn(FunctionBuilder) -> FunctionBuilder,
}
pub trait RegisteredClass: Sized
where
Self: 'static,
{
const CLASS_NAME: &'static str;
const CONSTRUCTOR: Option<ConstructorMeta<Self>> = None;
fn get_metadata() -> &'static ClassMetadata<Self>;
unsafe fn get_property<'a, T: FromZval<'a>>(&'a self, name: &str) -> Option<T> {
let obj = ZendClassObject::<Self>::from_obj_ptr(self)?;
obj.std.get_property(name).ok()
}
unsafe fn set_property(&mut self, name: &str, value: impl IntoZval) -> Option<()> {
let obj = ZendClassObject::<Self>::from_obj_ptr(self)?;
obj.std.set_property(name, value).ok()?;
Some(())
}
fn get_properties<'a>() -> HashMap<&'static str, Property<'a, Self>>;
}
#[repr(C)]
pub struct ZendClassObject<T> {
obj: Option<T>,
std: zend_object,
}
impl<T: RegisteredClass> ZendClassObject<T> {
pub fn new(val: T) -> ZBox<Self> {
unsafe { Self::internal_new(Some(val)) }
}
pub unsafe fn new_uninit() -> ZBox<Self> {
Self::internal_new(None)
}
unsafe fn internal_new(val: Option<T>) -> ZBox<Self> {
let size = mem::size_of::<ZendClassObject<T>>();
let meta = T::get_metadata();
let ce = meta.ce() as *const _ as *mut _;
let obj = ext_php_rs_zend_object_alloc(size as _, ce) as *mut ZendClassObject<T>;
let obj = obj
.as_mut()
.expect("Failed to allocate for new Zend object");
zend_object_std_init(&mut obj.std, ce);
object_properties_init(&mut obj.std, ce);
ptr::write(&mut obj.obj, val);
obj.std.handlers = meta.handlers();
ZBox::from_raw(obj)
}
pub fn initialize(&mut self, val: T) -> Option<T> {
self.obj.replace(val)
}
pub(crate) unsafe fn from_obj_ptr(obj: &T) -> Option<&mut Self> {
let ptr = (obj as *const T as *mut Self).as_mut()?;
if ptr.std.is_instance::<T>() {
Some(ptr)
} else {
None
}
}
pub fn from_zend_obj(std: &zend_object) -> Option<&Self> {
Some(Self::_from_zend_obj(std)?)
}
pub fn from_zend_obj_mut(std: &mut zend_object) -> Option<&mut Self> {
Self::_from_zend_obj(std)
}
fn _from_zend_obj(std: &zend_object) -> Option<&mut Self> {
let std = std as *const zend_object as *const i8;
let ptr = unsafe {
let ptr = std.offset(0 - Self::std_offset() as isize) as *const Self;
(ptr as *mut Self).as_mut()?
};
if ptr.std.is_instance::<T>() {
Some(ptr)
} else {
None
}
}
pub fn get_mut_zend_obj(&mut self) -> &mut zend_object {
&mut self.std
}
pub(crate) fn std_offset() -> usize {
unsafe {
let null = NonNull::<Self>::dangling();
let base = null.as_ref() as *const Self;
let std = &null.as_ref().std as *const zend_object;
(std as usize) - (base as usize)
}
}
}
impl<'a, T: RegisteredClass> FromZval<'a> for &'a ZendClassObject<T> {
const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME));
fn from_zval(zval: &'a Zval) -> Option<Self> {
Self::from_zend_object(zval.object()?).ok()
}
}
impl<'a, T: RegisteredClass> FromZendObject<'a> for &'a ZendClassObject<T> {
fn from_zend_object(obj: &'a ZendObject) -> Result<Self> {
ZendClassObject::from_zend_obj(obj).ok_or(Error::InvalidScope)
}
}
impl<'a, T: RegisteredClass> FromZvalMut<'a> for &'a mut ZendClassObject<T> {
const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME));
fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
Self::from_zend_object_mut(zval.object_mut()?).ok()
}
}
impl<'a, T: RegisteredClass> FromZendObjectMut<'a> for &'a mut ZendClassObject<T> {
fn from_zend_object_mut(obj: &'a mut ZendObject) -> Result<Self> {
ZendClassObject::from_zend_obj_mut(obj).ok_or(Error::InvalidScope)
}
}
unsafe impl<T: RegisteredClass> ZBoxable for ZendClassObject<T> {
fn free(&mut self) {
unsafe { ext_php_rs_zend_object_release(&mut self.std) }
}
}
impl<T> Deref for ZendClassObject<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.obj
.as_ref()
.expect("Attempted to access uninitalized class object")
}
}
impl<T> DerefMut for ZendClassObject<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.obj
.as_mut()
.expect("Attempted to access uninitalized class object")
}
}
pub struct ClassMetadata<T> {
handlers_init: AtomicBool,
handlers: MaybeUninit<ZendObjectHandlers>,
ce: AtomicPtr<ClassEntry>,
phantom: PhantomData<T>,
}
impl<T> ClassMetadata<T> {
pub const fn new() -> Self {
Self {
handlers_init: AtomicBool::new(false),
handlers: MaybeUninit::uninit(),
ce: AtomicPtr::new(std::ptr::null_mut()),
phantom: PhantomData,
}
}
}
impl<T: RegisteredClass> ClassMetadata<T> {
pub fn handlers(&self) -> &ZendObjectHandlers {
self.check_handlers();
unsafe { &*self.handlers.as_ptr() }
}
pub fn has_ce(&self) -> bool {
!self.ce.load(Ordering::SeqCst).is_null()
}
pub fn ce(&self) -> &'static ClassEntry {
unsafe { self.ce.load(Ordering::SeqCst).as_ref() }
.expect("Attempted to retrieve class entry before it has been stored.")
}
pub fn set_ce(&self, ce: &'static mut ClassEntry) {
if !self.ce.load(Ordering::SeqCst).is_null() {
panic!("Class entry has already been set.");
}
self.ce.store(ce, Ordering::SeqCst);
}
fn check_handlers(&self) {
if !self.handlers_init.load(Ordering::Acquire) {
unsafe { ZendObjectHandlers::init::<T>(self.handlers.as_ptr() as *mut _) };
self.handlers_init.store(true, Ordering::Release);
}
}
}
pub enum ConstructorResult<T> {
Ok(T),
Exception(PhpException),
ArgError,
}
impl<T, E> From<std::result::Result<T, E>> for ConstructorResult<T>
where
E: Into<PhpException>,
{
fn from(result: std::result::Result<T, E>) -> Self {
match result {
Ok(x) => Self::Ok(x),
Err(e) => Self::Exception(e.into()),
}
}
}
impl<T> From<T> for ConstructorResult<T> {
fn from(result: T) -> Self {
Self::Ok(result)
}
}
impl ZendObjectHandlers {
pub unsafe fn init<T: RegisteredClass>(ptr: *mut ZendObjectHandlers) {
std::ptr::copy_nonoverlapping(&std_object_handlers, ptr, 1);
let offset = ZendClassObject::<T>::std_offset();
(*ptr).offset = offset as _;
(*ptr).free_obj = Some(Self::free_obj::<T>);
(*ptr).read_property = Some(Self::read_property::<T>);
(*ptr).write_property = Some(Self::write_property::<T>);
(*ptr).get_properties = Some(Self::get_properties::<T>);
(*ptr).has_property = Some(Self::has_property::<T>);
}
unsafe extern "C" fn free_obj<T: RegisteredClass>(object: *mut zend_object) {
let obj = object
.as_mut()
.and_then(|obj| ZendClassObject::<T>::from_zend_obj_mut(obj))
.expect("Invalid object pointer given for `free_obj`");
ptr::drop_in_place(&mut obj.obj);
zend_object_std_dtor(object)
}
unsafe extern "C" fn read_property<T: RegisteredClass>(
object: *mut zend_object,
member: *mut zend_string,
type_: c_int,
cache_slot: *mut *mut c_void,
rv: *mut Zval,
) -> *mut Zval {
#[inline(always)]
unsafe fn internal<T: RegisteredClass>(
object: *mut zend_object,
member: *mut zend_string,
type_: c_int,
cache_slot: *mut *mut c_void,
rv: *mut Zval,
) -> PhpResult<*mut Zval> {
let obj = object
.as_mut()
.and_then(|obj| ZendClassObject::<T>::from_zend_obj_mut(obj))
.ok_or("Invalid object pointer given")?;
let prop_name = member
.as_ref()
.ok_or("Invalid property name pointer given")?;
let self_ = &mut **obj;
let mut props = T::get_properties();
let prop = props.remove(prop_name.as_str().ok_or("Invalid property name given")?);
let rv_mut = rv.as_mut().ok_or("Invalid return zval given")?;
rv_mut.u1.type_info = ZvalTypeFlags::Null.bits();
Ok(match prop {
Some(prop) => {
prop.get(self_, rv_mut)?;
rv
}
None => zend_std_read_property(object, member, type_, cache_slot, rv),
})
}
match internal::<T>(object, member, type_, cache_slot, rv) {
Ok(rv) => rv,
Err(e) => {
let _ = e.throw();
(&mut *rv).set_null();
rv
}
}
}
unsafe extern "C" fn write_property<T: RegisteredClass>(
object: *mut zend_object,
member: *mut zend_string,
value: *mut Zval,
cache_slot: *mut *mut c_void,
) -> *mut Zval {
#[inline(always)]
unsafe fn internal<T: RegisteredClass>(
object: *mut zend_object,
member: *mut zend_string,
value: *mut Zval,
cache_slot: *mut *mut c_void,
) -> PhpResult<*mut Zval> {
let obj = object
.as_mut()
.and_then(|obj| ZendClassObject::<T>::from_zend_obj_mut(obj))
.ok_or("Invalid object pointer given")?;
let prop_name = member
.as_ref()
.ok_or("Invalid property name pointer given")?;
let self_ = &mut **obj;
let mut props = T::get_properties();
let prop = props.remove(prop_name.as_str().ok_or("Invalid property name given")?);
let value_mut = value.as_mut().ok_or("Invalid return zval given")?;
Ok(match prop {
Some(prop) => {
prop.set(self_, value_mut)?;
value
}
None => zend_std_write_property(object, member, value, cache_slot),
})
}
match internal::<T>(object, member, value, cache_slot) {
Ok(rv) => rv,
Err(e) => {
let _ = e.throw();
value
}
}
}
unsafe extern "C" fn get_properties<T: RegisteredClass>(
object: *mut zend_object,
) -> *mut HashTable {
#[inline(always)]
unsafe fn internal<T: RegisteredClass>(
object: *mut zend_object,
props: &mut HashTable,
) -> PhpResult {
let obj = object
.as_mut()
.and_then(|obj| ZendClassObject::<T>::from_zend_obj_mut(obj))
.ok_or("Invalid object pointer given")?;
let self_ = &mut **obj;
let struct_props = T::get_properties();
for (name, val) in struct_props.into_iter() {
let mut zv = Zval::new();
if val.get(self_, &mut zv).is_err() {
continue;
}
props.insert(name, zv).map_err(|e| {
format!("Failed to insert value into properties hashtable: {:?}", e)
})?;
}
Ok(())
}
let props = zend_std_get_properties(object)
.as_mut()
.or_else(|| Some(HashTable::new().into_raw()))
.expect("Failed to get property hashtable");
if let Err(e) = internal::<T>(object, props) {
let _ = e.throw();
}
props
}
unsafe extern "C" fn has_property<T: RegisteredClass>(
object: *mut zend_object,
member: *mut zend_string,
has_set_exists: c_int,
cache_slot: *mut *mut c_void,
) -> c_int {
#[inline(always)]
unsafe fn internal<T: RegisteredClass>(
object: *mut zend_object,
member: *mut zend_string,
has_set_exists: c_int,
cache_slot: *mut *mut c_void,
) -> PhpResult<c_int> {
let obj = object
.as_mut()
.and_then(|obj| ZendClassObject::<T>::from_zend_obj_mut(obj))
.ok_or("Invalid object pointer given")?;
let prop_name = member
.as_ref()
.ok_or("Invalid property name pointer given")?;
let props = T::get_properties();
let prop = props.get(prop_name.as_str().ok_or("Invalid property name given")?);
let self_ = &mut **obj;
match has_set_exists {
0 => {
if let Some(val) = prop {
let mut zv = Zval::new();
val.get(self_, &mut zv)?;
if !zv.is_null() {
return Ok(1);
}
}
}
1 => {
if let Some(val) = prop {
let mut zv = Zval::new();
val.get(self_, &mut zv)?;
if zend_is_true(&mut zv) == 1 {
return Ok(1);
}
}
}
2 => {
if prop.is_some() {
return Ok(1);
}
}
_ => return Err(
"Invalid value given for `has_set_exists` in struct `has_property` function."
.into(),
),
};
Ok(zend_std_has_property(
object,
member,
has_set_exists,
cache_slot,
))
}
match internal::<T>(object, member, has_set_exists, cache_slot) {
Ok(rv) => rv,
Err(e) => {
let _ = e.throw();
0
}
}
}
}