@@ -10,6 +10,7 @@ use crate::{
1010 zend_std_read_property, zend_std_write_property, zend_throw_error,
1111 } ,
1212 flags:: { PropertyFlags , ZvalTypeFlags } ,
13+ internal:: property:: PropertyDescriptor ,
1314 types:: { ZendClassObject , ZendHashTable , ZendObject , ZendStr , Zval } ,
1415} ;
1516
@@ -135,24 +136,23 @@ impl ZendObjectHandlers {
135136 cache_slot : * mut * mut c_void ,
136137 rv : * mut Zval ,
137138 ) -> PhpResult < * mut Zval > {
138- let prop_name = unsafe {
139- member
140- . as_ref ( )
141- . ok_or ( "Invalid property name pointer given" ) ?
142- } ;
143139 let self_ = & * obj;
144- let prop = T :: get_metadata ( ) . find_property ( prop_name . as_str ( ) ? ) ;
140+ let prop = unsafe { resolve_property :: < T > ( member , cache_slot ) ? } ;
145141
146142 // retval needs to be treated as initialized, so we set the type to null
147143 let rv_mut = unsafe { rv. as_mut ( ) . ok_or ( "Invalid return zval given" ) ? } ;
148144 rv_mut. u1 . type_info = ZvalTypeFlags :: Null . bits ( ) ;
149145
150146 Ok ( match prop {
151147 Some ( prop_info) => {
152- // Check visibility before allowing access
153148 let object_ce = unsafe { ( * object) . ce } ;
154149 if !unsafe { check_property_access ( prop_info. flags , object_ce) } {
155150 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+ } ;
156156 unsafe {
157157 throw_property_access_error (
158158 T :: CLASS_NAME ,
@@ -208,21 +208,20 @@ impl ZendObjectHandlers {
208208 value : * mut Zval ,
209209 cache_slot : * mut * mut c_void ,
210210 ) -> PhpResult < * mut Zval > {
211- let prop_name = unsafe {
212- member
213- . as_ref ( )
214- . ok_or ( "Invalid property name pointer given" ) ?
215- } ;
216211 let self_ = & mut * obj;
217- let prop = T :: get_metadata ( ) . find_property ( prop_name . as_str ( ) ? ) ;
212+ let prop = unsafe { resolve_property :: < T > ( member , cache_slot ) ? } ;
218213 let value_mut = unsafe { value. as_mut ( ) . ok_or ( "Invalid return zval given" ) ? } ;
219214
220215 Ok ( match prop {
221216 Some ( prop_info) => {
222- // Check visibility before allowing access
223217 let object_ce = unsafe { ( * object) . ce } ;
224218 if !unsafe { check_property_access ( prop_info. flags , object_ce) } {
225219 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+ } ;
226225 unsafe {
227226 throw_property_access_error (
228227 T :: CLASS_NAME ,
@@ -341,12 +340,7 @@ impl ZendObjectHandlers {
341340 has_set_exists : c_int ,
342341 cache_slot : * mut * mut c_void ,
343342 ) -> PhpResult < c_int > {
344- let prop_name = unsafe {
345- member
346- . as_ref ( )
347- . ok_or ( "Invalid property name pointer given" ) ?
348- } ;
349- let prop = T :: get_metadata ( ) . find_property ( prop_name. as_str ( ) ?) ;
343+ let prop = unsafe { resolve_property :: < T > ( member, cache_slot) ? } ;
350344 let self_ = & * obj;
351345
352346 match has_set_exists {
@@ -411,6 +405,54 @@ impl ZendObjectHandlers {
411405 }
412406}
413407
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) ]
422+ unsafe 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+
414456/// Gets the current calling scope from the executor globals.
415457///
416458/// # Safety
0 commit comments