@@ -10,7 +10,7 @@ use ext_php_rs::boxed::ZBox;
1010
1111use ext_php_rs:: convert:: { FromZval , IntoZval , IntoZvalDyn } ;
1212use ext_php_rs:: exception:: { PhpException , PhpResult } ;
13- use ext_php_rs:: ffi:: { zend_class_entry, zend_object, zval} ;
13+ use ext_php_rs:: ffi:: { zend_class_entry, zend_object, zend_object_handlers , zval, HashTable } ;
1414use ext_php_rs:: flags:: DataType ;
1515use ext_php_rs:: prelude:: * ;
1616use ext_php_rs:: types:: { ArrayKey , ZendCallable , ZendHashTable , ZendObject , Zval } ;
@@ -36,8 +36,30 @@ extern "C" {
3636 name_length : usize ,
3737 value : * mut zval ,
3838 ) ;
39+
40+ /// PHP's per-thread GC scratch buffer for `get_gc` handlers. The
41+ /// handler reports the object's outgoing references by adding zvals
42+ /// here, then `zend_get_gc_buffer_use` writes the buffer's contents
43+ /// into the (table, n) out-params the cycle collector reads.
44+ fn zend_get_gc_buffer_create ( ) -> * mut std:: ffi:: c_void ;
45+ fn zend_get_gc_buffer_add_zval ( buffer : * mut std:: ffi:: c_void , zv : * mut zval ) ;
46+ fn zend_get_gc_buffer_use (
47+ buffer : * mut std:: ffi:: c_void ,
48+ table : * mut * mut zval ,
49+ n : * mut std:: os:: raw:: c_int ,
50+ ) ;
3951}
4052
53+ /// `Z_TYPE_INFO` for an OBJECT zval that carries a refcount.
54+ ///
55+ /// PHP packs `(type | flags << 8)` into a single 32-bit word. For
56+ /// `IS_OBJECT` (8) plus `IS_TYPE_REFCOUNTED` (1) and `IS_TYPE_COLLECTABLE`
57+ /// (2) shifted into the flags byte, the value is `0x10 | 0x208 == 0x208`
58+ /// — but the canonical value PHP uses is `0x0a08`. We only ever read this
59+ /// from PHP's own headers indirectly, so the constant lives here so the
60+ /// gc-trace path doesn't depend on ext-php-rs exposing it.
61+ const PHP_IS_OBJECT_EX : u32 = 8 | ( ( ( 1 << 0 ) | ( 1 << 1 ) ) << 8 ) ;
62+
4163#[ derive( Clone ) ]
4264struct BinaryString ( Vec < u8 > ) ;
4365
@@ -1222,6 +1244,81 @@ fn native_ast(native_ast: &Zval) -> PhpResult<&WpMySqlNativeAst> {
12221244 . ok_or_else ( || php_error ( "Missing native AST handle" ) )
12231245}
12241246
1247+ /// Storage for the customised `zend_object_handlers` for `WP_MySQL_Native_Ast`.
1248+ ///
1249+ /// We can't mutate the static handlers struct ext-php-rs registers, so we
1250+ /// clone it on startup, patch `get_gc`, and point the class entry's
1251+ /// `default_object_handlers` at this owned copy. Lives for the duration
1252+ /// of the module — written exactly once during MINIT.
1253+ static mut WP_MYSQL_NATIVE_AST_HANDLERS : Option < zend_object_handlers > = None ;
1254+
1255+ /// Custom `get_gc` handler that exposes `WpMySqlNativeAst.node_cache` to
1256+ /// PHP's cycle collector.
1257+ ///
1258+ /// The cache forms `AST -> wrappers -> $native_ast property -> AST`,
1259+ /// which PHP can't traverse on its own because the `node_cache` lives in
1260+ /// a Rust `HashMap<usize, ZBox<ZendObject>>` opaque to the engine. By
1261+ /// reporting each cached wrapper here, PHP's mark-and-sweep can complete
1262+ /// the cycle and reclaim both the AST and its wrappers.
1263+ ///
1264+ /// # Safety
1265+ ///
1266+ /// Called by PHP's GC with a non-null `object` pointer guaranteed to
1267+ /// belong to the registered class. `table` and `n` are non-null
1268+ /// out-params owned by the caller. The zvals we push into the gc buffer
1269+ /// are read-only handles for traversal — they don't transfer ownership
1270+ /// or change refcounts, so we deliberately do *not* call `set_object`
1271+ /// (which addrefs); we set the union directly.
1272+ unsafe extern "C" fn ast_get_gc (
1273+ object : * mut zend_object ,
1274+ table : * mut * mut zval ,
1275+ n : * mut std:: os:: raw:: c_int ,
1276+ ) -> * mut HashTable {
1277+ let buf = zend_get_gc_buffer_create ( ) ;
1278+
1279+ if let Some ( ast) = ext_php_rs:: types:: ZendClassObject :: < WpMySqlNativeAst > :: from_zend_obj ( & * object)
1280+ . and_then ( |z| z. obj . as_ref ( ) )
1281+ {
1282+ if let Ok ( cache) = ast. node_cache . try_borrow ( ) {
1283+ for boxed in cache. values ( ) {
1284+ // Build a zval pointing at the cached wrapper without
1285+ // bumping refcount — the gc buffer just enumerates outgoing
1286+ // references; mutating refcounts here would un-balance the
1287+ // collector's accounting.
1288+ let mut zv: zval = std:: mem:: zeroed ( ) ;
1289+ zv. value . obj =
1290+ ( boxed. as_ref ( ) as * const ZendObject ) as * mut zend_object as * mut _ ;
1291+ zv. u1 . type_info = PHP_IS_OBJECT_EX ;
1292+ zend_get_gc_buffer_add_zval ( buf, & mut zv) ;
1293+ }
1294+ }
1295+ }
1296+
1297+ zend_get_gc_buffer_use ( buf, table, n) ;
1298+ std:: ptr:: null_mut ( )
1299+ }
1300+
1301+ /// Patch `WP_MySQL_Native_Ast`'s class entry to use a `get_gc` handler
1302+ /// that walks the Rust-side `node_cache`. Called once during MINIT
1303+ /// after ext-php-rs has registered the class.
1304+ fn install_ast_gc_handler ( ) -> PhpResult < ( ) > {
1305+ let class = ClassEntry :: try_find ( "WP_MySQL_Native_Ast" )
1306+ . ok_or_else ( || php_error ( "WP_MySQL_Native_Ast class not registered" ) ) ?;
1307+ unsafe {
1308+ let class_mut = ( class as * const zend_class_entry ) as * mut zend_class_entry ;
1309+ let default = ( * class_mut) . default_object_handlers ;
1310+ if default. is_null ( ) {
1311+ return Err ( php_error ( "WP_MySQL_Native_Ast has no default object handlers" ) ) ;
1312+ }
1313+ let mut patched = std:: ptr:: read ( default) ;
1314+ patched. get_gc = Some ( ast_get_gc) ;
1315+ WP_MYSQL_NATIVE_AST_HANDLERS = Some ( patched) ;
1316+ let stored = WP_MYSQL_NATIVE_AST_HANDLERS . as_ref ( ) . unwrap ( ) ;
1317+ ( * class_mut) . default_object_handlers = stored as * const _ ;
1318+ }
1319+ Ok ( ( ) )
1320+ }
1321+
12251322/// Build a Zval that references an existing PHP object with refcount bumped.
12261323///
12271324/// Used on cache hits to hand a stored `ZBox<ZendObject>` back to PHP without
@@ -2120,5 +2217,13 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
21202217 ) )
21212218 . function ( wrap_function ! ( wp_sqlite_mysql_native_ast_get_start) )
21222219 . function ( wrap_function ! ( wp_sqlite_mysql_native_ast_get_length) )
2220+ . startup_function ( module_startup)
21232221 . info_function ( php_module_info)
21242222}
2223+
2224+ extern "C" fn module_startup ( _type : i32 , _module_number : i32 ) -> i32 {
2225+ if install_ast_gc_handler ( ) . is_err ( ) {
2226+ return 0 ;
2227+ }
2228+ 1
2229+ }
0 commit comments