use std::{convert::TryInto, fmt::Debug, os::raw::c_char, ptr};
use crate::{
boxed::{ZBox, ZBoxable},
class::RegisteredClass,
convert::{FromZendObject, FromZval, FromZvalMut, IntoZval, IntoZvalDyn},
error::{Error, Result},
ffi::{
HashTable, ZEND_ISEMPTY, ZEND_PROPERTY_EXISTS, ZEND_PROPERTY_ISSET,
ext_php_rs_zend_object_release, object_properties_init, zend_call_known_function,
zend_function, zend_hash_str_find_ptr_lc, zend_object, zend_objects_new,
},
flags::DataType,
rc::PhpRc,
types::{ZendClassObject, ZendStr, Zval},
zend::{ClassEntry, ExecutorGlobals, ZendObjectHandlers, ce},
};
#[cfg(php84)]
use crate::ffi::{zend_lazy_object_init, zend_lazy_object_mark_as_initialized};
#[cfg(all(feature = "closure", php84))]
use crate::{
closure::Closure,
ffi::{
_zend_fcall_info_cache, ZEND_LAZY_OBJECT_STRATEGY_GHOST, ZEND_LAZY_OBJECT_STRATEGY_PROXY,
zend_is_callable_ex, zend_object_make_lazy,
},
};
pub type ZendObject = zend_object;
impl ZendObject {
#[must_use]
pub fn new(ce: &ClassEntry) -> ZBox<Self> {
unsafe {
let ptr = match ce.__bindgen_anon_2.create_object {
None => {
let ptr = zend_objects_new(ptr::from_ref(ce).cast_mut());
assert!(!ptr.is_null(), "Failed to allocate memory for Zend object");
object_properties_init(ptr, ptr::from_ref(ce).cast_mut());
ptr
}
Some(v) => v(ptr::from_ref(ce).cast_mut()),
};
ZBox::from_raw(
ptr.as_mut()
.expect("Failed to allocate memory for Zend object"),
)
}
}
#[must_use]
pub fn new_stdclass() -> ZBox<Self> {
Self::new(ce::stdclass())
}
#[must_use]
pub fn from_class_object<T: RegisteredClass>(obj: ZBox<ZendClassObject<T>>) -> ZBox<Self> {
let this = obj.into_raw();
unsafe { ZBox::from_raw(this.get_mut_zend_obj()) }
}
#[must_use]
pub fn get_class_entry(&self) -> &'static ClassEntry {
unsafe { self.ce.as_ref() }.expect("Could not retrieve class entry.")
}
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(TryInto::try_into)
}
}
#[must_use]
pub fn instance_of(&self, ce: &ClassEntry) -> bool {
self.get_class_entry().instance_of(ce)
}
#[must_use]
pub fn is_instance<T: RegisteredClass>(&self) -> bool {
(self.ce.cast_const()).eq(&ptr::from_ref(T::get_metadata().ce()))
}
#[must_use]
pub fn is_traversable(&self) -> bool {
self.instance_of(ce::traversable())
}
#[allow(clippy::inline_always)]
#[inline(always)]
pub fn try_call_method(&self, name: &str, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
let mut retval = Zval::new();
let len = params.len();
let params = params
.into_iter()
.map(|val| val.as_zval(false))
.collect::<Result<Vec<_>>>()?;
let packed = params.into_boxed_slice();
unsafe {
let res = zend_hash_str_find_ptr_lc(
&raw const (*self.ce).function_table,
name.as_ptr().cast::<c_char>(),
name.len(),
)
.cast::<zend_function>();
if res.is_null() {
return Err(Error::Callable);
}
zend_call_known_function(
res,
ptr::from_ref(self).cast_mut(),
self.ce,
&raw mut retval,
len.try_into()?,
packed.as_ptr().cast_mut(),
std::ptr::null_mut(),
);
};
Ok(retval)
}
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(),
&raw mut *name,
1,
ptr::null_mut(),
&raw 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,
&raw mut *name,
&raw mut value,
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(),
&raw mut *name,
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)
}
}
pub fn extract<'a, T>(&'a self) -> Result<T>
where
T: FromZendObject<'a>,
{
T::from_zend_object(self)
}
#[inline]
#[must_use]
pub fn get_id(&self) -> u32 {
self.handle
}
#[must_use]
pub fn hash(&self) -> String {
format!("{:016x}0000000000000000", self.handle)
}
#[cfg(php84)]
const IS_OBJ_LAZY_UNINITIALIZED: u32 = 1 << 31;
#[cfg(php84)]
const IS_OBJ_LAZY_PROXY: u32 = 1 << 30;
#[cfg(php84)]
#[must_use]
pub fn is_lazy(&self) -> bool {
(self.extra_flags & (Self::IS_OBJ_LAZY_UNINITIALIZED | Self::IS_OBJ_LAZY_PROXY)) != 0
}
#[cfg(php84)]
#[must_use]
pub fn is_lazy_proxy(&self) -> bool {
(self.extra_flags & Self::IS_OBJ_LAZY_PROXY) != 0
}
#[cfg(php84)]
#[must_use]
pub fn is_lazy_ghost(&self) -> bool {
(self.extra_flags & Self::IS_OBJ_LAZY_UNINITIALIZED) != 0
&& (self.extra_flags & Self::IS_OBJ_LAZY_PROXY) == 0
}
#[cfg(php84)]
#[must_use]
pub fn is_lazy_initialized(&self) -> bool {
if !self.is_lazy() {
return false;
}
(self.extra_flags & Self::IS_OBJ_LAZY_UNINITIALIZED) == 0
}
#[cfg(php84)]
#[must_use]
pub fn lazy_init(&mut self) -> Option<&mut Self> {
if !self.is_lazy() {
return None;
}
unsafe { zend_lazy_object_init(self).as_mut() }
}
#[cfg(php84)]
#[must_use]
pub fn mark_lazy_initialized(&mut self) -> Option<&mut Self> {
if !self.is_lazy() {
return None;
}
unsafe { zend_lazy_object_mark_as_initialized(self).as_mut() }
}
#[cfg(php84)]
#[must_use]
pub fn lazy_get_instance(&mut self) -> Option<&mut Self> {
if !self.is_lazy_proxy() || !self.is_lazy_initialized() {
return None;
}
unsafe { zend_lazy_object_init(self).as_mut() }
}
#[cfg(all(feature = "closure", php84))]
#[cfg_attr(docs, doc(cfg(all(feature = "closure", php84))))]
#[allow(clippy::cast_possible_truncation)]
pub fn make_lazy_ghost<F>(&mut self, initializer: F) -> Result<()>
where
F: Fn() + 'static,
{
self.make_lazy_internal(initializer, ZEND_LAZY_OBJECT_STRATEGY_GHOST as u8)
}
#[cfg(all(feature = "closure", php84))]
#[cfg_attr(docs, doc(cfg(all(feature = "closure", php84))))]
#[allow(clippy::cast_possible_truncation)]
pub fn make_lazy_proxy<F>(&mut self, initializer: F) -> Result<()>
where
F: Fn() -> Option<ZBox<ZendObject>> + 'static,
{
self.make_lazy_internal(initializer, ZEND_LAZY_OBJECT_STRATEGY_PROXY as u8)
}
#[cfg(all(feature = "closure", php84))]
fn make_lazy_internal<F, R>(&mut self, initializer: F, strategy: u8) -> Result<()>
where
F: Fn() -> R + 'static,
R: IntoZval + 'static,
{
let ce = unsafe { self.ce.as_ref() }.ok_or(Error::InvalidPointer)?;
if !ce.can_be_lazy() {
return Err(Error::LazyObjectFailed);
}
if self.is_lazy() && !self.is_lazy_initialized() {
return Err(Error::LazyObjectFailed);
}
let closure = Closure::wrap(Box::new(initializer) as Box<dyn Fn() -> R>);
let mut initializer_zv = Zval::new();
closure.set_zval(&mut initializer_zv, false)?;
let mut fcc: _zend_fcall_info_cache = unsafe { std::mem::zeroed() };
let is_callable = unsafe {
zend_is_callable_ex(
&raw mut initializer_zv,
ptr::null_mut(),
0,
ptr::null_mut(),
&raw mut fcc,
ptr::null_mut(),
)
};
if !is_callable {
return Err(Error::Callable);
}
let ce = self.ce;
let result = unsafe {
zend_object_make_lazy(self, ce, &raw mut initializer_zv, &raw mut fcc, strategy)
};
if result.is_null() {
Err(Error::LazyObjectFailed)
} else {
Ok(())
}
}
#[inline]
unsafe fn handlers(&self) -> Result<&ZendObjectHandlers> {
unsafe { self.handlers.as_ref() }.ok_or(Error::InvalidScope)
}
#[inline]
fn mut_ptr(&self) -> *mut Self {
ptr::from_ref(self).cast_mut()
}
}
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 (key, val) in props {
dbg.field(key.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<'a> FromZvalMut<'a> for &'a mut ZendObject {
const TYPE: DataType = DataType::Object(None);
fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
zval.object_mut()
}
}
impl IntoZval for ZBox<ZendObject> {
const TYPE: DataType = DataType::Object(None);
const NULLABLE: bool = false;
#[inline]
fn set_zval(mut self, zv: &mut Zval, _: bool) -> Result<()> {
self.dec_count();
zv.set_object(self.into_raw());
Ok(())
}
}
impl IntoZval for &mut ZendObject {
const TYPE: DataType = DataType::Object(None);
const NULLABLE: bool = false;
#[inline]
fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
zv.set_object(self);
Ok(())
}
}
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,
ptr::from_ref(obj).cast_mut(),
obj.ce,
&raw mut ret,
0,
ptr::null_mut(),
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"),
);
}
}
}
impl<T: RegisteredClass> From<ZBox<ZendClassObject<T>>> for ZBox<ZendObject> {
#[inline]
fn from(obj: ZBox<ZendClassObject<T>>) -> Self {
ZendObject::from_class_object(obj)
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u32)]
pub enum PropertyQuery {
Isset = ZEND_PROPERTY_ISSET,
NotEmpty = ZEND_ISEMPTY,
Exists = ZEND_PROPERTY_EXISTS,
}