Skip to content

Commit 079968b

Browse files
committed
Add custom gc_handler for WP_MySQL_Native_Ast
Patch the class's default_object_handlers->get_gc on module startup so PHP's cycle collector can walk the Rust-side node_cache. The handler enumerates cached ZendObject wrappers into PHP's gc_buffer without bumping refcounts; PHP's collector uses these to detect that node_cache -> wrappers -> $native_ast property -> WpMySqlNativeAst forms a cycle and collects it. This is the implementation half of the cycle-collection contract added in the previous commit's tests. Expect compile/runtime iteration — ext-php-rs 0.15 doesn't expose IS_OBJECT_EX or zend_get_gc_buffer_* directly, so they are declared inline and may need adjustment for the runner's PHP build.
1 parent 0c945e0 commit 079968b

1 file changed

Lines changed: 106 additions & 1 deletion

File tree

  • packages/php-ext-wp-mysql-parser/src

packages/php-ext-wp-mysql-parser/src/lib.rs

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use ext_php_rs::boxed::ZBox;
1010

1111
use ext_php_rs::convert::{FromZval, IntoZval, IntoZvalDyn};
1212
use 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};
1414
use ext_php_rs::flags::DataType;
1515
use ext_php_rs::prelude::*;
1616
use 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)]
4264
struct 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

Comments
 (0)