Skip to main content

ext_php_rs/zend/
handlers.rs

1use std::{ffi::CString, ffi::c_void, mem::MaybeUninit, os::raw::c_int, ptr};
2
3use crate::{
4    class::RegisteredClass,
5    exception::PhpResult,
6    ffi::{
7        ext_php_rs_executor_globals, instanceof_function_slow, std_object_handlers,
8        zend_class_entry, zend_is_true, zend_object_handlers, zend_object_std_dtor,
9        zend_objects_clone_members, zend_std_get_properties, zend_std_has_property,
10        zend_std_read_property, zend_std_write_property, zend_throw_error,
11    },
12    flags::{PropertyFlags, ZvalTypeFlags},
13    internal::property::PropertyDescriptor,
14    types::{ZendClassObject, ZendHashTable, ZendObject, ZendStr, Zval},
15};
16
17/// A set of functions associated with a PHP class.
18pub type ZendObjectHandlers = zend_object_handlers;
19
20impl ZendObjectHandlers {
21    /// Creates a new set of object handlers based on the standard object
22    /// handlers.
23    #[must_use]
24    pub fn new<T: RegisteredClass>() -> ZendObjectHandlers {
25        let mut this = MaybeUninit::uninit();
26
27        // SAFETY: `this` is allocated on the stack and is a valid memory location.
28        unsafe { Self::init::<T>(&raw mut *this.as_mut_ptr()) };
29
30        // SAFETY: We just initialized the handlers in the previous statement, therefore
31        // we are returning a valid object.
32        unsafe { this.assume_init() }
33    }
34
35    /// Initializes a given set of object handlers by copying the standard
36    /// object handlers into the memory location, as well as setting up the
37    /// `T` type destructor.
38    ///
39    /// # Parameters
40    ///
41    /// * `ptr` - Pointer to memory location to copy the standard handlers to.
42    ///
43    /// # Safety
44    ///
45    /// Caller must guarantee that the `ptr` given is a valid memory location.
46    ///
47    /// # Panics
48    ///
49    /// * If the offset of the `T` type is not a valid `i32` value.
50    pub unsafe fn init<T: RegisteredClass>(ptr: *mut ZendObjectHandlers) {
51        unsafe { ptr::copy_nonoverlapping(&raw const std_object_handlers, ptr, 1) };
52        let offset = ZendClassObject::<T>::std_offset();
53        unsafe { (*ptr).offset = offset.try_into().expect("Invalid offset") };
54        unsafe { (*ptr).free_obj = Some(Self::free_obj::<T>) };
55        unsafe { (*ptr).clone_obj = Some(Self::clone_obj::<T>) };
56        unsafe { (*ptr).read_property = Some(Self::read_property::<T>) };
57        unsafe { (*ptr).write_property = Some(Self::write_property::<T>) };
58        unsafe { (*ptr).get_properties = Some(Self::get_properties::<T>) };
59        unsafe { (*ptr).has_property = Some(Self::has_property::<T>) };
60    }
61
62    unsafe extern "C" fn free_obj<T: RegisteredClass>(object: *mut ZendObject) {
63        // Try to get the ZendClassObject. This may return None for:
64        // - PHP subclasses/mocks that didn't call the parent constructor
65        // - Objects where the Rust side was never initialized
66        if let Some(obj) = unsafe {
67            object
68                .as_mut()
69                .and_then(|obj| ZendClassObject::<T>::from_zend_obj_mut(obj))
70        } {
71            // Manually drop the object as we don't want to free the underlying memory.
72            unsafe { ptr::drop_in_place(&raw mut obj.obj) };
73        }
74
75        // Always call the standard destructor to clean up the PHP object
76        unsafe { zend_object_std_dtor(object) };
77    }
78
79    unsafe extern "C" fn clone_obj<T: RegisteredClass>(object: *mut ZendObject) -> *mut ZendObject {
80        // PHP will call OBJ_RELEASE on the returned pointer if an exception
81        // is thrown, so we must NEVER return the original object. Always
82        // allocate a new (possibly uninitialized) object for error paths.
83        let cloned_val = unsafe {
84            object
85                .as_ref()
86                .and_then(|obj| ZendClassObject::<T>::from_zend_obj(obj))
87                .and_then(|old| old.obj.as_ref())
88                .and_then(RegisteredClass::clone_obj)
89        };
90
91        if let Some(val) = cloned_val {
92            let mut new = ZendClassObject::<T>::new(val);
93            unsafe { zend_objects_clone_members(&raw mut new.std, object) };
94            let raw = new.into_raw();
95            &raw mut raw.std
96        } else {
97            let msg = CString::new(format!(
98                "Trying to clone an uncloneable object of class {}",
99                T::CLASS_NAME
100            ))
101            .expect("Failed to create error message");
102            unsafe { zend_throw_error(ptr::null_mut(), msg.as_ptr()) };
103            // Return a new uninitialized object that PHP can safely release.
104            // free_obj handles uninitialized (None) objects gracefully.
105            let empty = unsafe { ZendClassObject::<T>::new_uninit(None) };
106            let raw = empty.into_raw();
107            &raw mut raw.std
108        }
109    }
110
111    #[allow(clippy::items_after_statements)]
112    unsafe extern "C" fn read_property<T: RegisteredClass>(
113        object: *mut ZendObject,
114        member: *mut ZendStr,
115        type_: c_int,
116        cache_slot: *mut *mut c_void,
117        rv: *mut Zval,
118    ) -> *mut Zval {
119        // If the object doesn't have a valid Rust backing (e.g., a mock or subclass
120        // that didn't call the parent constructor), fall back to standard PHP handling
121        let Some(obj) = (unsafe {
122            object
123                .as_mut()
124                .and_then(|obj| ZendClassObject::<T>::from_zend_obj_mut(obj))
125        }) else {
126            return unsafe { zend_std_read_property(object, member, type_, cache_slot, rv) };
127        };
128
129        #[allow(clippy::inline_always)]
130        #[inline(always)]
131        unsafe fn internal<T: RegisteredClass>(
132            object: *mut ZendObject,
133            obj: &mut ZendClassObject<T>,
134            member: *mut ZendStr,
135            type_: c_int,
136            cache_slot: *mut *mut c_void,
137            rv: *mut Zval,
138        ) -> PhpResult<*mut Zval> {
139            let self_ = &*obj;
140            let prop = unsafe { resolve_property::<T>(member, cache_slot)? };
141
142            // retval needs to be treated as initialized, so we set the type to null
143            let rv_mut = unsafe { rv.as_mut().ok_or("Invalid return zval given")? };
144            rv_mut.u1.type_info = ZvalTypeFlags::Null.bits();
145
146            Ok(match prop {
147                Some(prop_info) => {
148                    let object_ce = unsafe { (*object).ce };
149                    if !unsafe { check_property_access(prop_info.flags, object_ce) } {
150                        let is_private = prop_info.flags.contains(PropertyFlags::Private);
151                        let prop_name = unsafe {
152                            member
153                                .as_ref()
154                                .ok_or("Invalid property name pointer given")?
155                        };
156                        unsafe {
157                            throw_property_access_error(
158                                T::CLASS_NAME,
159                                prop_name.as_str()?,
160                                is_private,
161                            );
162                        }
163                        return Ok(rv);
164                    }
165                    let getter = prop_info
166                        .get
167                        .ok_or("No getter available for this property.")?;
168                    getter(self_, rv_mut)?;
169                    rv
170                }
171                None => unsafe { zend_std_read_property(object, member, type_, cache_slot, rv) },
172            })
173        }
174
175        match unsafe { internal::<T>(object, obj, member, type_, cache_slot, rv) } {
176            Ok(rv) => rv,
177            Err(e) => {
178                let _ = e.throw();
179                unsafe { (*rv).set_null() };
180                rv
181            }
182        }
183    }
184
185    #[allow(clippy::items_after_statements)]
186    unsafe extern "C" fn write_property<T: RegisteredClass>(
187        object: *mut ZendObject,
188        member: *mut ZendStr,
189        value: *mut Zval,
190        cache_slot: *mut *mut c_void,
191    ) -> *mut Zval {
192        // If the object doesn't have a valid Rust backing (e.g., a mock or subclass
193        // that didn't call the parent constructor), fall back to standard PHP handling
194        let Some(obj) = (unsafe {
195            object
196                .as_mut()
197                .and_then(|obj| ZendClassObject::<T>::from_zend_obj_mut(obj))
198        }) else {
199            return unsafe { zend_std_write_property(object, member, value, cache_slot) };
200        };
201
202        #[allow(clippy::inline_always)]
203        #[inline(always)]
204        unsafe fn internal<T: RegisteredClass>(
205            object: *mut ZendObject,
206            obj: &mut ZendClassObject<T>,
207            member: *mut ZendStr,
208            value: *mut Zval,
209            cache_slot: *mut *mut c_void,
210        ) -> PhpResult<*mut Zval> {
211            let self_ = &mut *obj;
212            let prop = unsafe { resolve_property::<T>(member, cache_slot)? };
213            let value_mut = unsafe { value.as_mut().ok_or("Invalid return zval given")? };
214
215            Ok(match prop {
216                Some(prop_info) => {
217                    let object_ce = unsafe { (*object).ce };
218                    if !unsafe { check_property_access(prop_info.flags, object_ce) } {
219                        let is_private = prop_info.flags.contains(PropertyFlags::Private);
220                        let prop_name = unsafe {
221                            member
222                                .as_ref()
223                                .ok_or("Invalid property name pointer given")?
224                        };
225                        unsafe {
226                            throw_property_access_error(
227                                T::CLASS_NAME,
228                                prop_name.as_str()?,
229                                is_private,
230                            );
231                        }
232                        return Ok(value);
233                    }
234                    let setter = prop_info
235                        .set
236                        .ok_or("No setter available for this property.")?;
237                    setter(self_, value_mut)?;
238                    value
239                }
240                None => unsafe { zend_std_write_property(object, member, value, cache_slot) },
241            })
242        }
243
244        match unsafe { internal::<T>(object, obj, member, value, cache_slot) } {
245            Ok(rv) => rv,
246            Err(e) => {
247                let _ = e.throw();
248                value
249            }
250        }
251    }
252
253    #[allow(clippy::items_after_statements)]
254    unsafe extern "C" fn get_properties<T: RegisteredClass>(
255        object: *mut ZendObject,
256    ) -> *mut ZendHashTable {
257        // Get the standard properties first (this works for all objects)
258        let props = unsafe {
259            zend_std_get_properties(object)
260                .as_mut()
261                .or_else(|| Some(ZendHashTable::new().into_raw()))
262                .expect("Failed to get property hashtable")
263        };
264
265        // If the object doesn't have a valid Rust backing (e.g., a mock or subclass
266        // that didn't call the parent constructor), just return standard properties
267        let Some(obj) = (unsafe {
268            object
269                .as_mut()
270                .and_then(|obj| ZendClassObject::<T>::from_zend_obj_mut(obj))
271        }) else {
272            return props;
273        };
274
275        #[allow(clippy::inline_always)]
276        #[inline(always)]
277        unsafe fn internal<T: RegisteredClass>(
278            obj: &mut ZendClassObject<T>,
279            props: &mut ZendHashTable,
280        ) -> PhpResult {
281            let self_ = &*obj;
282            let metadata = T::get_metadata();
283            let method_mangled = metadata.method_mangled_names();
284
285            for desc in metadata.field_properties() {
286                let Some(getter) = desc.get else { continue };
287                let mut zv = Zval::new();
288                if getter(self_, &mut zv).is_err() {
289                    continue;
290                }
291                props.insert(desc.mangled_name, zv).map_err(|e| {
292                    format!("Failed to insert value into properties hashtable: {e:?}")
293                })?;
294            }
295
296            for (i, desc) in metadata.method_properties().iter().enumerate() {
297                let Some(getter) = desc.get else { continue };
298                let mut zv = Zval::new();
299                if getter(self_, &mut zv).is_err() {
300                    continue;
301                }
302                props.insert(&*method_mangled[i], zv).map_err(|e| {
303                    format!("Failed to insert value into properties hashtable: {e:?}")
304                })?;
305            }
306
307            Ok(())
308        }
309
310        if let Err(e) = unsafe { internal::<T>(obj, props) } {
311            let _ = e.throw();
312        }
313
314        props
315    }
316
317    #[allow(clippy::items_after_statements)]
318    unsafe extern "C" fn has_property<T: RegisteredClass>(
319        object: *mut ZendObject,
320        member: *mut ZendStr,
321        has_set_exists: c_int,
322        cache_slot: *mut *mut c_void,
323    ) -> c_int {
324        // If the object doesn't have a valid Rust backing (e.g., a mock or subclass
325        // that didn't call the parent constructor), fall back to standard PHP handling
326        let Some(obj) = (unsafe {
327            object
328                .as_mut()
329                .and_then(|obj| ZendClassObject::<T>::from_zend_obj_mut(obj))
330        }) else {
331            return unsafe { zend_std_has_property(object, member, has_set_exists, cache_slot) };
332        };
333
334        #[allow(clippy::inline_always)]
335        #[inline(always)]
336        unsafe fn internal<T: RegisteredClass>(
337            object: *mut ZendObject,
338            obj: &mut ZendClassObject<T>,
339            member: *mut ZendStr,
340            has_set_exists: c_int,
341            cache_slot: *mut *mut c_void,
342        ) -> PhpResult<c_int> {
343            let prop = unsafe { resolve_property::<T>(member, cache_slot)? };
344            let self_ = &*obj;
345
346            match has_set_exists {
347                //
348                // * 0 (has) whether property exists and is not NULL
349                0 => {
350                    if let Some(val) = prop {
351                        let getter = val.get.ok_or("No getter available for this property.")?;
352                        let mut zv = Zval::new();
353                        getter(self_, &mut zv)?;
354                        if !zv.is_null() {
355                            return Ok(1);
356                        }
357                    }
358                }
359                //
360                // * 1 (set) whether property exists and is true
361                1 => {
362                    if let Some(val) = prop {
363                        let getter = val.get.ok_or("No getter available for this property.")?;
364                        let mut zv = Zval::new();
365                        getter(self_, &mut zv)?;
366
367                        cfg_if::cfg_if! {
368                            if #[cfg(php84)] {
369                                #[allow(clippy::unnecessary_mut_passed)]
370                                if unsafe { zend_is_true(&raw mut zv) } {
371                                    return Ok(1);
372                                }
373                            } else {
374                                #[allow(clippy::unnecessary_mut_passed)]
375                                if unsafe { zend_is_true(&raw mut zv) } == 1 {
376                                    return Ok(1);
377                                }
378                            }
379                        }
380                    }
381                }
382                //
383                // * 2 (exists) whether property exists
384                2 => {
385                    if prop.is_some() {
386                        return Ok(1);
387                    }
388                }
389                _ => return Err(
390                    "Invalid value given for `has_set_exists` in struct `has_property` function."
391                        .into(),
392                ),
393            }
394
395            Ok(unsafe { zend_std_has_property(object, member, has_set_exists, cache_slot) })
396        }
397
398        match unsafe { internal::<T>(object, obj, member, has_set_exists, cache_slot) } {
399            Ok(rv) => rv,
400            Err(e) => {
401                let _ = e.throw();
402                0
403            }
404        }
405    }
406}
407
408/// Resolves a property descriptor via `cache_slot` or linear scan fallback.
409///
410/// On cache hit (`cache_slot[1]` matches this class's metadata pointer), returns
411/// the cached `&PropertyDescriptor<T>` directly, skipping string conversion and
412/// `find_property`. On miss, performs the full lookup and populates the cache for
413/// subsequent calls.
414///
415/// # Safety
416///
417/// - `member` must be a valid `ZendStr` pointer.
418/// - `cache_slot` must be null or point to at least 2 writable `*mut c_void` slots
419///   (guaranteed by PHP's opcode compiler for all property access opcodes).
420#[allow(clippy::inline_always)]
421#[inline(always)]
422unsafe fn resolve_property<T: RegisteredClass>(
423    member: *mut ZendStr,
424    cache_slot: *mut *mut c_void,
425) -> PhpResult<Option<&'static PropertyDescriptor<T>>> {
426    let meta = T::get_metadata();
427    let meta_ptr = ptr::from_ref(meta).cast::<c_void>().cast_mut();
428
429    if !cache_slot.is_null() {
430        let guard = unsafe { *cache_slot.add(1) };
431        if guard == meta_ptr {
432            let desc = unsafe { &*(*cache_slot).cast::<PropertyDescriptor<T>>() };
433            return Ok(Some(desc));
434        }
435    }
436
437    let prop_name = unsafe {
438        member
439            .as_ref()
440            .ok_or("Invalid property name pointer given")?
441    };
442    let Some(descriptor) = meta.find_property(prop_name.as_str()?) else {
443        return Ok(None);
444    };
445
446    if !cache_slot.is_null() {
447        unsafe {
448            *cache_slot = ptr::from_ref(descriptor).cast::<c_void>().cast_mut();
449            *cache_slot.add(1) = meta_ptr;
450        }
451    }
452
453    Ok(Some(descriptor))
454}
455
456/// Gets the current calling scope from the executor globals.
457///
458/// # Safety
459///
460/// Must only be called during PHP execution when executor globals are valid.
461#[inline]
462unsafe fn get_calling_scope() -> *const zend_class_entry {
463    let eg = unsafe { ext_php_rs_executor_globals().as_ref() };
464    let Some(eg) = eg else {
465        return ptr::null();
466    };
467    let execute_data = eg.current_execute_data;
468
469    if execute_data.is_null() {
470        return ptr::null();
471    }
472
473    let func = unsafe { (*execute_data).func };
474    if func.is_null() {
475        return ptr::null();
476    }
477
478    // Access the common.scope field through the union
479    unsafe { (*func).common.scope }
480}
481
482/// Checks if the calling scope has access to a property with the given flags.
483///
484/// Returns `true` if access is allowed, `false` otherwise.
485///
486/// # Safety
487///
488/// Must only be called during PHP execution when executor globals are valid.
489/// The `object_ce` pointer must be valid.
490#[inline]
491unsafe fn check_property_access(flags: PropertyFlags, object_ce: *const zend_class_entry) -> bool {
492    // Public properties are always accessible
493    if !flags.contains(PropertyFlags::Private) && !flags.contains(PropertyFlags::Protected) {
494        return true;
495    }
496
497    let calling_scope = unsafe { get_calling_scope() };
498
499    if flags.contains(PropertyFlags::Private) {
500        // Private: must be called from the exact same class
501        return calling_scope == object_ce;
502    }
503
504    if flags.contains(PropertyFlags::Protected) {
505        // Protected: must be called from same class or a subclass
506        if calling_scope.is_null() {
507            return false;
508        }
509
510        // Same class check
511        if calling_scope == object_ce {
512            return true;
513        }
514
515        // Check if calling_scope is a subclass of object_ce
516        // or if object_ce is a subclass of calling_scope (for parent access)
517        unsafe {
518            instanceof_function_slow(calling_scope, object_ce)
519                || instanceof_function_slow(object_ce, calling_scope)
520        }
521    } else {
522        true
523    }
524}
525
526/// Throws an error for invalid property access.
527///
528/// # Safety
529///
530/// Must only be called during PHP execution.
531///
532/// # Panics
533///
534/// Panics if the error message cannot be converted to a `CString`.
535unsafe fn throw_property_access_error(class_name: &str, prop_name: &str, is_private: bool) {
536    let visibility = if is_private { "private" } else { "protected" };
537    let message = CString::new(format!(
538        "Cannot access {visibility} property {class_name}::${prop_name}"
539    ))
540    .expect("Failed to create error message");
541
542    unsafe {
543        zend_throw_error(ptr::null_mut(), message.as_ptr());
544    }
545}