33use std:: cell:: RefCell ;
44use std:: collections:: { HashMap , HashSet } ;
55use std:: os:: raw:: c_char;
6+ use std:: os:: raw:: c_int;
67use std:: ptr;
8+ use std:: sync:: atomic:: { AtomicBool , Ordering } ;
79use std:: sync:: Arc ;
810
911use ext_php_rs:: boxed:: ZBox ;
1012
1113use ext_php_rs:: convert:: { FromZval , IntoZval , IntoZvalDyn } ;
1214use ext_php_rs:: exception:: { PhpException , PhpResult } ;
13- use ext_php_rs:: ffi:: { zend_class_entry, zend_object, zval} ;
15+ use ext_php_rs:: ffi:: {
16+ ext_php_rs_executor_globals, zend_class_entry, zend_get_gc_buffer, zend_object,
17+ zend_object_handlers, zval, HashTable , IS_OBJECT_EX ,
18+ } ;
1419use ext_php_rs:: flags:: DataType ;
1520use ext_php_rs:: prelude:: * ;
1621use ext_php_rs:: types:: { ArrayKey , ZendCallable , ZendHashTable , ZendObject , Zval } ;
@@ -37,6 +42,13 @@ extern "C" {
3742 value : * mut zval ,
3843 ) ;
3944
45+ fn zend_get_gc_buffer_grow ( gc_buffer : * mut zend_get_gc_buffer ) ;
46+
47+ fn zend_std_get_gc (
48+ object : * mut zend_object ,
49+ table : * mut * mut zval ,
50+ n : * mut c_int ,
51+ ) -> * mut HashTable ;
4052}
4153
4254#[ derive( Clone ) ]
@@ -1223,19 +1235,132 @@ fn native_ast(native_ast: &Zval) -> PhpResult<&WpMySqlNativeAst> {
12231235 . ok_or_else ( || php_error ( "Missing native AST handle" ) )
12241236}
12251237
1226- /// Build a Zval that references an existing PHP object with refcount bumped.
1238+ /// Tracks whether the GC handler has been installed on `WP_MySQL_Native_Ast`'s
1239+ /// shared handlers struct. We patch in place rather than swapping the handlers
1240+ /// pointer because ext-php-rs uses that pointer as part of registered-class
1241+ /// identity checks.
1242+ static WP_MYSQL_NATIVE_AST_HANDLERS_PATCHED : AtomicBool = AtomicBool :: new ( false ) ;
1243+
1244+ /// Custom `get_gc` handler that exposes `WpMySqlNativeAst.node_cache` to PHP's
1245+ /// cycle collector.
1246+ ///
1247+ /// The Rust cache owns strong references to cached parser-node wrappers, and
1248+ /// each wrapper points back at the native AST through its `$native_ast`
1249+ /// property. Reporting the cached wrappers here gives PHP a complete view of
1250+ /// the otherwise opaque `AST -> cache -> wrapper -> AST` cycle.
1251+ unsafe extern "C" fn ast_get_gc (
1252+ object : * mut zend_object ,
1253+ table : * mut * mut zval ,
1254+ n : * mut c_int ,
1255+ ) -> * mut HashTable {
1256+ * table = ptr:: null_mut ( ) ;
1257+ * n = 0 ;
1258+
1259+ let mut standard_table = ptr:: null_mut ( ) ;
1260+ let mut standard_count = 0 ;
1261+ let standard_properties = zend_std_get_gc ( object, & mut standard_table, & mut standard_count) ;
1262+
1263+ let Some ( ast) = ext_php_rs:: types:: ZendClassObject :: < WpMySqlNativeAst > :: from_zend_obj ( & * object)
1264+ . and_then ( |z| z. obj . as_ref ( ) )
1265+ else {
1266+ * table = standard_table;
1267+ * n = standard_count;
1268+ return standard_properties;
1269+ } ;
1270+
1271+ let Ok ( cache) = ast. node_cache . try_borrow ( ) else {
1272+ * table = standard_table;
1273+ * n = standard_count;
1274+ return standard_properties;
1275+ } ;
1276+
1277+ let Some ( gc_buffer) = gc_buffer_create ( ) else {
1278+ * table = standard_table;
1279+ * n = standard_count;
1280+ return standard_properties;
1281+ } ;
1282+
1283+ if !standard_table. is_null ( ) {
1284+ for offset in 0 ..standard_count {
1285+ gc_buffer_add_zval ( gc_buffer, standard_table. add ( offset as usize ) ) ;
1286+ }
1287+ }
1288+ for boxed in cache. values ( ) {
1289+ gc_buffer_add_obj ( gc_buffer, ptr:: from_ref ( boxed. as_ref ( ) ) . cast_mut ( ) ) ;
1290+ }
1291+ gc_buffer_use ( gc_buffer, table, n) ;
1292+
1293+ standard_properties
1294+ }
1295+
1296+ unsafe fn gc_buffer_create ( ) -> Option < * mut zend_get_gc_buffer > {
1297+ let executor_globals = ext_php_rs_executor_globals ( ) ;
1298+ if executor_globals. is_null ( ) {
1299+ return None ;
1300+ }
1301+
1302+ let gc_buffer = ptr:: addr_of_mut!( ( * executor_globals) . get_gc_buffer) ;
1303+ ( * gc_buffer) . cur = ( * gc_buffer) . start ;
1304+ Some ( gc_buffer)
1305+ }
1306+
1307+ unsafe fn gc_buffer_add_obj ( gc_buffer : * mut zend_get_gc_buffer , obj : * mut zend_object ) {
1308+ if ( * gc_buffer) . cur == ( * gc_buffer) . end {
1309+ zend_get_gc_buffer_grow ( gc_buffer) ;
1310+ }
1311+
1312+ let slot = ( * gc_buffer) . cur ;
1313+ ( * slot) . value . obj = obj;
1314+ ( * slot) . u1 . type_info = IS_OBJECT_EX ;
1315+ ( * gc_buffer) . cur = ( * gc_buffer) . cur . add ( 1 ) ;
1316+ }
1317+
1318+ unsafe fn gc_buffer_add_zval ( gc_buffer : * mut zend_get_gc_buffer , value : * const zval ) {
1319+ if value. is_null ( ) {
1320+ return ;
1321+ }
1322+ if ( * gc_buffer) . cur == ( * gc_buffer) . end {
1323+ zend_get_gc_buffer_grow ( gc_buffer) ;
1324+ }
1325+
1326+ let slot = ( * gc_buffer) . cur ;
1327+ ptr:: copy_nonoverlapping ( value, slot, 1 ) ;
1328+ ( * gc_buffer) . cur = ( * gc_buffer) . cur . add ( 1 ) ;
1329+ }
1330+
1331+ unsafe fn gc_buffer_use ( gc_buffer : * mut zend_get_gc_buffer , table : * mut * mut zval , n : * mut c_int ) {
1332+ * table = ( * gc_buffer) . start ;
1333+ if ( * gc_buffer) . start . is_null ( ) {
1334+ * n = 0 ;
1335+ } else {
1336+ * n = ( * gc_buffer) . cur . offset_from ( ( * gc_buffer) . start ) as c_int ;
1337+ }
1338+ }
1339+
1340+ /// Patch `WP_MySQL_Native_Ast`'s shared `zend_object_handlers` so PHP's cycle
1341+ /// collector can walk cached wrappers held by the Rust-side HashMap.
1342+ unsafe fn install_gc_handler_for ( obj : * mut zend_object ) {
1343+ if WP_MYSQL_NATIVE_AST_HANDLERS_PATCHED . load ( Ordering :: Acquire ) {
1344+ return ;
1345+ }
1346+
1347+ let handlers = ( * obj) . handlers ;
1348+ if handlers. is_null ( ) {
1349+ return ;
1350+ }
1351+
1352+ let handlers_mut = handlers as * mut zend_object_handlers ;
1353+ ( * handlers_mut) . get_gc = Some ( ast_get_gc) ;
1354+ WP_MYSQL_NATIVE_AST_HANDLERS_PATCHED . store ( true , Ordering :: Release ) ;
1355+ }
1356+
1357+ /// Build a Zval that references an existing PHP object.
12271358///
12281359/// Used on cache hits to hand a stored `ZBox<ZendObject>` back to PHP without
1229- /// allocating a new wrapper. PHP convention for object zvals is that the
1230- /// zval owns one strong reference, so we have to addref before publishing
1231- /// the pointer; otherwise the object would be freed twice (once when this
1232- /// zval drops, once when the cache drops).
1233- fn zval_from_object_addref ( obj : & mut ZendObject ) -> Zval {
1234- // PHP convention: an object zval owns one strong reference, so bump
1235- // the embedded zend_refcounted_h refcount before publishing the
1236- // pointer; otherwise the object would be freed twice (once when this
1237- // zval drops, once when the cache drops).
1238- obj. gc . refcount += 1 ;
1360+ /// allocating a new wrapper. `Zval::set_object()` bumps the object refcount
1361+ /// for the returned zval; the `ZBox` in `node_cache` owns the cache's
1362+ /// reference.
1363+ fn zval_from_cached_object ( obj : & mut ZendObject ) -> Zval {
12391364 let mut zv = Zval :: new ( ) ;
12401365 zv. set_object ( obj) ;
12411366 zv
@@ -1266,12 +1391,14 @@ impl WpMySqlNativeAst {
12661391 index : usize ,
12671392 classes : & PhpClasses ,
12681393 ) -> PhpResult < Zval > {
1269- let mut cache = self . node_cache . borrow_mut ( ) ;
1394+ {
1395+ let mut cache = self . node_cache . borrow_mut ( ) ;
12701396
1271- // Cache hit: skip allocation entirely and return a Zval pointing
1272- // at the cached wrapper with refcount bumped.
1273- if let Some ( boxed) = cache. get_mut ( & index) {
1274- return Ok ( zval_from_object_addref ( boxed) ) ;
1397+ // Cache hit: skip allocation entirely and return a Zval pointing
1398+ // at the cached wrapper with refcount bumped by `set_object()`.
1399+ if let Some ( boxed) = cache. get_mut ( & index) {
1400+ return Ok ( zval_from_cached_object ( boxed) ) ;
1401+ }
12751402 }
12761403
12771404 // Cache miss: build the wrapper as `create_php_node_with_classes`
@@ -1312,11 +1439,15 @@ impl WpMySqlNativeAst {
13121439 idx_i64,
13131440 ) ?;
13141441
1442+ let mut cache = self . node_cache . borrow_mut ( ) ;
1443+ if let Some ( boxed) = cache. get_mut ( & index) {
1444+ return Ok ( zval_from_cached_object ( boxed) ) ;
1445+ }
13151446 cache. insert ( index, object) ;
13161447 let stored = cache
13171448 . get_mut ( & index)
13181449 . expect ( "just-inserted node missing from cache" ) ;
1319- Ok ( zval_from_object_addref ( stored) )
1450+ Ok ( zval_from_cached_object ( stored) )
13201451 }
13211452}
13221453
@@ -1838,6 +1969,15 @@ impl WpMySqlNativeParser {
18381969 . into_zval ( false )
18391970 . map_err ( php_error) ?;
18401971 let native_ast = native_ast ( & native_ast_zval) ?;
1972+ // Install the custom `get_gc` after ext-php-rs can resolve the
1973+ // object. PHP's cycle collector then sees wrappers held by the
1974+ // Rust-side cache.
1975+ unsafe {
1976+ let obj_ref = native_ast_zval
1977+ . object ( )
1978+ . ok_or_else ( || php_error ( "Native AST zval is not an object" ) ) ?;
1979+ install_gc_handler_for ( ptr:: from_ref ( obj_ref) . cast_mut ( ) ) ;
1980+ }
18411981 native_ast. arena . create_php_ast ( & native_ast_zval)
18421982 } )
18431983 }
0 commit comments