From 49244e186c70b2c2da2d1c96ecabadb8868c7068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Szab=C3=B3?= Date: Tue, 28 Jan 2025 07:00:45 +0100 Subject: [PATCH] feat: add support for empty immutable shared arrays Why: - Since PHP 7.3, it's possible for extensions to create zvals backed by an immutable shared hashtable via the ZVAL_EMPTY_ARRAY macro. - This helps avoid redundant hashtable allocations when returning empty arrays back to userland PHP code, and could likewise be beneficial for Rust extensions too. What: - Add ZendHashTable::new_empty_immutable() to obtain a ZendHashTable that is actually an empty immutable shared hashtable. - Add ZendHashTable::is_immutable(). Use it to avoid attempting to free the immutable shared hashtable on drop, and to set appropriate type flags when initializing a zval with a ZendHashTable. --- allowed_bindings.rs | 4 ++ docsrs_bindings.rs | 104 ++++++++++++++++++++------------ src/flags.rs | 34 ++++++----- src/types/array.rs | 56 +++++++++++++++-- src/types/zval.rs | 9 ++- tests/src/integration/array.php | 23 +++++++ tests/src/lib.rs | 31 +++++++++- 7 files changed, 197 insertions(+), 64 deletions(-) diff --git a/allowed_bindings.rs b/allowed_bindings.rs index ba628ff47..86e29da84 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -81,6 +81,7 @@ bind! { zend_declare_class_constant, zend_declare_property, zend_do_implement_interface, + zend_empty_array, zend_execute_data, zend_function_entry, zend_hash_clean, @@ -137,6 +138,9 @@ bind! { E_RECOVERABLE_ERROR, E_DEPRECATED, E_USER_DEPRECATED, + GC_IMMUTABLE, + GC_FLAGS_MASK, + GC_FLAGS_SHIFT, HT_MIN_SIZE, IS_ARRAY, IS_ARRAY_EX, diff --git a/docsrs_bindings.rs b/docsrs_bindings.rs index aade607af..2f4b50b9a 100644 --- a/docsrs_bindings.rs +++ b/docsrs_bindings.rs @@ -82,6 +82,7 @@ where } pub const ZEND_DEBUG: u32 = 1; pub const _ZEND_TYPE_NAME_BIT: u32 = 16777216; +pub const _ZEND_TYPE_LITERAL_NAME_BIT: u32 = 8388608; pub const _ZEND_TYPE_NULLABLE_BIT: u32 = 2; pub const HT_MIN_SIZE: u32 = 8; pub const IS_UNDEF: u32 = 0; @@ -104,6 +105,9 @@ pub const IS_INDIRECT: u32 = 12; pub const IS_PTR: u32 = 13; pub const _IS_BOOL: u32 = 18; pub const Z_TYPE_FLAGS_SHIFT: u32 = 8; +pub const GC_FLAGS_MASK: u32 = 1008; +pub const GC_FLAGS_SHIFT: u32 = 0; +pub const GC_IMMUTABLE: u32 = 64; pub const IS_TYPE_REFCOUNTED: u32 = 1; pub const IS_TYPE_COLLECTABLE: u32 = 2; pub const IS_INTERNED_STRING_EX: u32 = 6; @@ -269,7 +273,7 @@ pub struct _IO_FILE { pub _wide_data: *mut _IO_wide_data, pub _freeres_list: *mut _IO_FILE, pub _freeres_buf: *mut ::std::os::raw::c_void, - pub __pad5: usize, + pub _prevchain: *mut *mut _IO_FILE, pub _mode: ::std::os::raw::c_int, pub _unused2: [::std::os::raw::c_char; 20usize], } @@ -537,6 +541,9 @@ pub type zend_string_init_interned_func_t = ::std::option::Option< extern "C" { pub static mut zend_string_init_interned: zend_string_init_interned_func_t; } +extern "C" { + pub static zend_empty_array: HashTable; +} extern "C" { pub fn zend_hash_clean(ht: *mut HashTable); } @@ -1992,7 +1999,7 @@ pub struct _php_stream { pub wrapperthis: *mut ::std::os::raw::c_void, pub wrapperdata: zval, pub _bitfield_align_1: [u8; 0], - pub _bitfield_1: __BindgenBitfieldUnit<[u8; 1usize]>, + pub _bitfield_1: __BindgenBitfieldUnit<[u8; 2usize]>, pub mode: [::std::os::raw::c_char; 16usize], pub flags: u32, pub res: *mut zend_resource, @@ -2011,105 +2018,122 @@ pub struct _php_stream { } impl _php_stream { #[inline] - pub fn is_persistent(&self) -> u8 { - unsafe { ::std::mem::transmute(self._bitfield_1.get(0usize, 1u8) as u8) } + pub fn is_persistent(&self) -> u16 { + unsafe { ::std::mem::transmute(self._bitfield_1.get(0usize, 1u8) as u16) } } #[inline] - pub fn set_is_persistent(&mut self, val: u8) { + pub fn set_is_persistent(&mut self, val: u16) { unsafe { - let val: u8 = ::std::mem::transmute(val); + let val: u16 = ::std::mem::transmute(val); self._bitfield_1.set(0usize, 1u8, val as u64) } } #[inline] - pub fn in_free(&self) -> u8 { - unsafe { ::std::mem::transmute(self._bitfield_1.get(1usize, 2u8) as u8) } + pub fn in_free(&self) -> u16 { + unsafe { ::std::mem::transmute(self._bitfield_1.get(1usize, 2u8) as u16) } } #[inline] - pub fn set_in_free(&mut self, val: u8) { + pub fn set_in_free(&mut self, val: u16) { unsafe { - let val: u8 = ::std::mem::transmute(val); + let val: u16 = ::std::mem::transmute(val); self._bitfield_1.set(1usize, 2u8, val as u64) } } #[inline] - pub fn eof(&self) -> u8 { - unsafe { ::std::mem::transmute(self._bitfield_1.get(3usize, 1u8) as u8) } + pub fn eof(&self) -> u16 { + unsafe { ::std::mem::transmute(self._bitfield_1.get(3usize, 1u8) as u16) } } #[inline] - pub fn set_eof(&mut self, val: u8) { + pub fn set_eof(&mut self, val: u16) { unsafe { - let val: u8 = ::std::mem::transmute(val); + let val: u16 = ::std::mem::transmute(val); self._bitfield_1.set(3usize, 1u8, val as u64) } } #[inline] - pub fn __exposed(&self) -> u8 { - unsafe { ::std::mem::transmute(self._bitfield_1.get(4usize, 1u8) as u8) } + pub fn __exposed(&self) -> u16 { + unsafe { ::std::mem::transmute(self._bitfield_1.get(4usize, 1u8) as u16) } } #[inline] - pub fn set___exposed(&mut self, val: u8) { + pub fn set___exposed(&mut self, val: u16) { unsafe { - let val: u8 = ::std::mem::transmute(val); + let val: u16 = ::std::mem::transmute(val); self._bitfield_1.set(4usize, 1u8, val as u64) } } #[inline] - pub fn fclose_stdiocast(&self) -> u8 { - unsafe { ::std::mem::transmute(self._bitfield_1.get(5usize, 2u8) as u8) } + pub fn fclose_stdiocast(&self) -> u16 { + unsafe { ::std::mem::transmute(self._bitfield_1.get(5usize, 2u8) as u16) } } #[inline] - pub fn set_fclose_stdiocast(&mut self, val: u8) { + pub fn set_fclose_stdiocast(&mut self, val: u16) { unsafe { - let val: u8 = ::std::mem::transmute(val); + let val: u16 = ::std::mem::transmute(val); self._bitfield_1.set(5usize, 2u8, val as u64) } } #[inline] - pub fn has_buffered_data(&self) -> u8 { - unsafe { ::std::mem::transmute(self._bitfield_1.get(7usize, 1u8) as u8) } + pub fn has_buffered_data(&self) -> u16 { + unsafe { ::std::mem::transmute(self._bitfield_1.get(7usize, 1u8) as u16) } } #[inline] - pub fn set_has_buffered_data(&mut self, val: u8) { + pub fn set_has_buffered_data(&mut self, val: u16) { unsafe { - let val: u8 = ::std::mem::transmute(val); + let val: u16 = ::std::mem::transmute(val); self._bitfield_1.set(7usize, 1u8, val as u64) } } #[inline] + pub fn fclose_stdiocast_flush_in_progress(&self) -> u16 { + unsafe { ::std::mem::transmute(self._bitfield_1.get(8usize, 1u8) as u16) } + } + #[inline] + pub fn set_fclose_stdiocast_flush_in_progress(&mut self, val: u16) { + unsafe { + let val: u16 = ::std::mem::transmute(val); + self._bitfield_1.set(8usize, 1u8, val as u64) + } + } + #[inline] pub fn new_bitfield_1( - is_persistent: u8, - in_free: u8, - eof: u8, - __exposed: u8, - fclose_stdiocast: u8, - has_buffered_data: u8, - ) -> __BindgenBitfieldUnit<[u8; 1usize]> { - let mut __bindgen_bitfield_unit: __BindgenBitfieldUnit<[u8; 1usize]> = Default::default(); + is_persistent: u16, + in_free: u16, + eof: u16, + __exposed: u16, + fclose_stdiocast: u16, + has_buffered_data: u16, + fclose_stdiocast_flush_in_progress: u16, + ) -> __BindgenBitfieldUnit<[u8; 2usize]> { + let mut __bindgen_bitfield_unit: __BindgenBitfieldUnit<[u8; 2usize]> = Default::default(); __bindgen_bitfield_unit.set(0usize, 1u8, { - let is_persistent: u8 = unsafe { ::std::mem::transmute(is_persistent) }; + let is_persistent: u16 = unsafe { ::std::mem::transmute(is_persistent) }; is_persistent as u64 }); __bindgen_bitfield_unit.set(1usize, 2u8, { - let in_free: u8 = unsafe { ::std::mem::transmute(in_free) }; + let in_free: u16 = unsafe { ::std::mem::transmute(in_free) }; in_free as u64 }); __bindgen_bitfield_unit.set(3usize, 1u8, { - let eof: u8 = unsafe { ::std::mem::transmute(eof) }; + let eof: u16 = unsafe { ::std::mem::transmute(eof) }; eof as u64 }); __bindgen_bitfield_unit.set(4usize, 1u8, { - let __exposed: u8 = unsafe { ::std::mem::transmute(__exposed) }; + let __exposed: u16 = unsafe { ::std::mem::transmute(__exposed) }; __exposed as u64 }); __bindgen_bitfield_unit.set(5usize, 2u8, { - let fclose_stdiocast: u8 = unsafe { ::std::mem::transmute(fclose_stdiocast) }; + let fclose_stdiocast: u16 = unsafe { ::std::mem::transmute(fclose_stdiocast) }; fclose_stdiocast as u64 }); __bindgen_bitfield_unit.set(7usize, 1u8, { - let has_buffered_data: u8 = unsafe { ::std::mem::transmute(has_buffered_data) }; + let has_buffered_data: u16 = unsafe { ::std::mem::transmute(has_buffered_data) }; has_buffered_data as u64 }); + __bindgen_bitfield_unit.set(8usize, 1u8, { + let fclose_stdiocast_flush_in_progress: u16 = + unsafe { ::std::mem::transmute(fclose_stdiocast_flush_in_progress) }; + fclose_stdiocast_flush_in_progress as u64 + }); __bindgen_bitfield_unit } } diff --git a/src/flags.rs b/src/flags.rs index da3a6844c..da3713fd8 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -8,22 +8,22 @@ use crate::ffi::{ CONST_CS, CONST_DEPRECATED, CONST_NO_FILE_CACHE, CONST_PERSISTENT, E_COMPILE_ERROR, E_COMPILE_WARNING, E_CORE_ERROR, E_CORE_WARNING, E_DEPRECATED, E_ERROR, E_NOTICE, E_PARSE, E_RECOVERABLE_ERROR, E_STRICT, E_USER_DEPRECATED, E_USER_ERROR, E_USER_NOTICE, E_USER_WARNING, - E_WARNING, IS_ARRAY, IS_CALLABLE, IS_CONSTANT_AST, IS_DOUBLE, IS_FALSE, IS_INDIRECT, - IS_ITERABLE, IS_LONG, IS_MIXED, IS_NULL, IS_OBJECT, IS_PTR, IS_REFERENCE, IS_RESOURCE, - IS_STRING, IS_TRUE, IS_TYPE_COLLECTABLE, IS_TYPE_REFCOUNTED, IS_UNDEF, IS_VOID, PHP_INI_ALL, - PHP_INI_PERDIR, PHP_INI_SYSTEM, PHP_INI_USER, ZEND_ACC_ABSTRACT, ZEND_ACC_ANON_CLASS, - ZEND_ACC_CALL_VIA_TRAMPOLINE, ZEND_ACC_CHANGED, ZEND_ACC_CLOSURE, ZEND_ACC_CONSTANTS_UPDATED, - ZEND_ACC_CTOR, ZEND_ACC_DEPRECATED, ZEND_ACC_DONE_PASS_TWO, ZEND_ACC_EARLY_BINDING, - ZEND_ACC_FAKE_CLOSURE, ZEND_ACC_FINAL, ZEND_ACC_GENERATOR, ZEND_ACC_HAS_FINALLY_BLOCK, - ZEND_ACC_HAS_RETURN_TYPE, ZEND_ACC_HAS_TYPE_HINTS, ZEND_ACC_HEAP_RT_CACHE, ZEND_ACC_IMMUTABLE, - ZEND_ACC_IMPLICIT_ABSTRACT_CLASS, ZEND_ACC_INTERFACE, ZEND_ACC_LINKED, ZEND_ACC_NEARLY_LINKED, - ZEND_ACC_NEVER_CACHE, ZEND_ACC_NO_DYNAMIC_PROPERTIES, ZEND_ACC_PRELOADED, ZEND_ACC_PRIVATE, - ZEND_ACC_PROMOTED, ZEND_ACC_PROTECTED, ZEND_ACC_PUBLIC, ZEND_ACC_RESOLVED_INTERFACES, - ZEND_ACC_RESOLVED_PARENT, ZEND_ACC_RETURN_REFERENCE, ZEND_ACC_STATIC, ZEND_ACC_STRICT_TYPES, - ZEND_ACC_TOP_LEVEL, ZEND_ACC_TRAIT, ZEND_ACC_TRAIT_CLONE, ZEND_ACC_UNRESOLVED_VARIANCE, - ZEND_ACC_USES_THIS, ZEND_ACC_USE_GUARDS, ZEND_ACC_VARIADIC, ZEND_EVAL_CODE, - ZEND_HAS_STATIC_IN_METHODS, ZEND_INTERNAL_FUNCTION, ZEND_USER_FUNCTION, Z_TYPE_FLAGS_SHIFT, - _IS_BOOL, + E_WARNING, GC_IMMUTABLE, IS_ARRAY, IS_CALLABLE, IS_CONSTANT_AST, IS_DOUBLE, IS_FALSE, + IS_INDIRECT, IS_ITERABLE, IS_LONG, IS_MIXED, IS_NULL, IS_OBJECT, IS_PTR, IS_REFERENCE, + IS_RESOURCE, IS_STRING, IS_TRUE, IS_TYPE_COLLECTABLE, IS_TYPE_REFCOUNTED, IS_UNDEF, IS_VOID, + PHP_INI_ALL, PHP_INI_PERDIR, PHP_INI_SYSTEM, PHP_INI_USER, ZEND_ACC_ABSTRACT, + ZEND_ACC_ANON_CLASS, ZEND_ACC_CALL_VIA_TRAMPOLINE, ZEND_ACC_CHANGED, ZEND_ACC_CLOSURE, + ZEND_ACC_CONSTANTS_UPDATED, ZEND_ACC_CTOR, ZEND_ACC_DEPRECATED, ZEND_ACC_DONE_PASS_TWO, + ZEND_ACC_EARLY_BINDING, ZEND_ACC_FAKE_CLOSURE, ZEND_ACC_FINAL, ZEND_ACC_GENERATOR, + ZEND_ACC_HAS_FINALLY_BLOCK, ZEND_ACC_HAS_RETURN_TYPE, ZEND_ACC_HAS_TYPE_HINTS, + ZEND_ACC_HEAP_RT_CACHE, ZEND_ACC_IMMUTABLE, ZEND_ACC_IMPLICIT_ABSTRACT_CLASS, + ZEND_ACC_INTERFACE, ZEND_ACC_LINKED, ZEND_ACC_NEARLY_LINKED, ZEND_ACC_NEVER_CACHE, + ZEND_ACC_NO_DYNAMIC_PROPERTIES, ZEND_ACC_PRELOADED, ZEND_ACC_PRIVATE, ZEND_ACC_PROMOTED, + ZEND_ACC_PROTECTED, ZEND_ACC_PUBLIC, ZEND_ACC_RESOLVED_INTERFACES, ZEND_ACC_RESOLVED_PARENT, + ZEND_ACC_RETURN_REFERENCE, ZEND_ACC_STATIC, ZEND_ACC_STRICT_TYPES, ZEND_ACC_TOP_LEVEL, + ZEND_ACC_TRAIT, ZEND_ACC_TRAIT_CLONE, ZEND_ACC_UNRESOLVED_VARIANCE, ZEND_ACC_USES_THIS, + ZEND_ACC_USE_GUARDS, ZEND_ACC_VARIADIC, ZEND_EVAL_CODE, ZEND_HAS_STATIC_IN_METHODS, + ZEND_INTERNAL_FUNCTION, ZEND_USER_FUNCTION, Z_TYPE_FLAGS_SHIFT, _IS_BOOL, }; use std::{convert::TryFrom, fmt::Display}; @@ -61,6 +61,8 @@ bitflags! { const RefCounted = (IS_TYPE_REFCOUNTED << Z_TYPE_FLAGS_SHIFT); const Collectable = (IS_TYPE_COLLECTABLE << Z_TYPE_FLAGS_SHIFT); + + const Immutable = GC_IMMUTABLE; } } diff --git a/src/types/array.rs b/src/types/array.rs index 3bf6f4113..978ca3077 100644 --- a/src/types/array.rs +++ b/src/types/array.rs @@ -14,14 +14,14 @@ use crate::{ convert::{FromZval, IntoZval}, error::{Error, Result}, ffi::{ - _zend_new_array, zend_array_count, zend_array_destroy, zend_array_dup, zend_hash_clean, - zend_hash_get_current_data_ex, zend_hash_get_current_key_type_ex, + _zend_new_array, zend_array_count, zend_array_destroy, zend_array_dup, zend_empty_array, + zend_hash_clean, zend_hash_get_current_data_ex, zend_hash_get_current_key_type_ex, zend_hash_get_current_key_zval_ex, zend_hash_index_del, zend_hash_index_find, zend_hash_index_update, zend_hash_move_backwards_ex, zend_hash_move_forward_ex, zend_hash_next_index_insert, zend_hash_str_del, zend_hash_str_find, zend_hash_str_update, - HashPosition, HT_MIN_SIZE, + HashPosition, GC_FLAGS_MASK, GC_FLAGS_SHIFT, HT_MIN_SIZE, }, - flags::DataType, + flags::{DataType, ZvalTypeFlags}, types::Zval, }; @@ -71,6 +71,27 @@ impl ZendHashTable { Self::with_capacity(HT_MIN_SIZE) } + /// Returns a shared immutable empty hashtable. + /// This is useful to avoid redundant allocations when returning + /// an empty collection from Rust code back to the PHP userland. + /// Do not use this if you intend to modify the hashtable. + /// + /// # Example + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let ht = ZendHashTable::new_empty_immutable(); + /// ``` + pub fn new_empty_immutable() -> ZBox { + unsafe { + // SAFETY: zend_empty_array is a static global. + let ptr = (&zend_empty_array as *const ZendHashTable) as *mut ZendHashTable; + + // SAFETY: `as_mut()` panics if the pointer is null. + ZBox::from_raw(ptr.as_mut().expect("zend_empty_array inconsistent")) + } + } + /// Creates a new, empty, PHP hashtable with an initial size, returned /// inside a [`ZBox`]. /// @@ -102,6 +123,27 @@ impl ZendHashTable { } } + /// Determine whether this hashtable is immutable. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let ht = ZendHashTable::new(); + /// assert!(!ht.is_immutable()); + /// + /// let immutable_ht = ZendHashTable::new_empty_immutable(); + /// assert!(immutable_ht.is_immutable()); + /// ``` + pub fn is_immutable(&self) -> bool { + // SAFETY: Type info is initialized by Zend on array init. + let gc_type_info = unsafe { self.gc.u.type_info }; + let gc_flags = (gc_type_info >> GC_FLAGS_SHIFT) & (GC_FLAGS_MASK >> GC_FLAGS_SHIFT); + + gc_flags & ZvalTypeFlags::Immutable.bits() != 0 + } + /// Returns the current number of elements in the array. /// /// # Example @@ -539,8 +581,10 @@ impl ZendHashTable { unsafe impl ZBoxable for ZendHashTable { fn free(&mut self) { - // SAFETY: ZBox has immutable access to `self`. - unsafe { zend_array_destroy(self) } + if !self.is_immutable() { + // SAFETY: ZBox has immutable access to `self`. + unsafe { zend_array_destroy(self) } + } } } diff --git a/src/types/zval.rs b/src/types/zval.rs index 6de991d8c..d3028fb14 100644 --- a/src/types/zval.rs +++ b/src/types/zval.rs @@ -563,7 +563,14 @@ impl Zval { /// /// * `val` - The value to set the zval as. pub fn set_hashtable(&mut self, val: ZBox) { - self.change_type(ZvalTypeFlags::ArrayEx); + // Handle immutable shared arrays akin to ZVAL_EMPTY_ARRAY. + let type_info = if val.is_immutable() { + ZvalTypeFlags::Array + } else { + ZvalTypeFlags::ArrayEx + }; + + self.change_type(type_info); self.value.arr = val.into_raw(); } diff --git a/tests/src/integration/array.php b/tests/src/integration/array.php index 1232582c9..9a2f6f6b1 100644 --- a/tests/src/integration/array.php +++ b/tests/src/integration/array.php @@ -25,3 +25,26 @@ assert(in_array('1', $assoc)); assert(in_array('2', $assoc)); assert(in_array('3', $assoc)); + +// Test ZendHashtable drop logic +$immutable = test_zend_hashtable(); +assert(!$immutable); + +// Test immutable ZendHashtable drop logic +$immutable = test_immutable_zend_hashtable(); +assert($immutable); + +// Test that an immutable ZendHashtable is transparent to userland +$immutable = test_immutable_zend_hashtable_ret(); +$immutable[] = 'fpp'; +assert(count($immutable) === 1); + +// Test empty array -> Vec -> array conversion +$empty = test_array( [] ); +assert(is_array($empty)); +assert(count($empty) === 0); + +// Test empty array -> HashMap -> array conversion +$empty_assoc = test_array_assoc( [] ); +assert(is_array($empty_assoc)); +assert(count($empty_assoc) === 0); diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 237c08ef0..07f5a54fb 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -1,5 +1,8 @@ #![cfg_attr(windows, feature(abi_vectorcall))] -use ext_php_rs::{binary::Binary, prelude::*, types::ZendObject, types::Zval}; +use ext_php_rs::{ + binary::Binary, convert::IntoZval, prelude::*, types::ZendHashTable, types::ZendObject, + types::Zval, +}; use std::collections::HashMap; #[php_function] @@ -42,6 +45,32 @@ pub fn test_array_assoc(a: HashMap) -> HashMap { a } +#[php_function] +pub fn test_zend_hashtable() -> bool { + // Also tests dropping the hashtable at the end of this function + let mut ht = ZendHashTable::new(); + ht.insert("key", "value").unwrap(); + + ht.is_immutable() +} + +#[php_function] +pub fn test_immutable_zend_hashtable() -> bool { + // Also tests dropping the hashtable at the end of this function + let ht = ZendHashTable::new_empty_immutable(); + + ht.is_immutable() +} + +#[php_function] +pub fn test_immutable_zend_hashtable_ret() -> Zval { + let mut zv = Zval::new(); + ZendHashTable::new_empty_immutable() + .set_zval(&mut zv, false) + .unwrap(); + zv +} + #[php_function] pub fn test_binary(a: Binary) -> Binary { a