Skip to content

Commit

Permalink
Add support for empty immutable shared arrays
Browse files Browse the repository at this point in the history
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.
- Make ZendHashTable's TryFrom implementations from Vec and HashMap return
  an empty immutable shared hashtable if the input collection was empty.
  Although this would allow every user to automatically benefit from
  this optimization, I'm not convinced about this part because this is technically a
  breaking change for consumers that construct ZendHashTables via these
  converters in their Rust code. Maybe it'd be better to not change the
  converters and let projects explicitly use ::new_empty_immutable if they
  deem the optimization would be useful.
  • Loading branch information
mszabo-wikia committed Jan 28, 2025
1 parent 464407b commit 8316c30
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 24 deletions.
4 changes: 4 additions & 0 deletions allowed_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
34 changes: 18 additions & 16 deletions src/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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;
}
}

Expand Down
64 changes: 58 additions & 6 deletions src/types/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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<Self> {
unsafe {
// SAFETY: zend_empty_array is a static global.
let ptr = (&zend_empty_array as *const ZendHashTable) as *mut ZendHashTable;

// SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
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`].
///
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) }
}
}
}

Expand Down Expand Up @@ -878,6 +922,10 @@ where
type Error = Error;

fn try_from(value: HashMap<K, V>) -> Result<Self> {
if value.is_empty() {
return Ok(ZendHashTable::new_empty_immutable());
}

let mut ht = ZendHashTable::with_capacity(
value.len().try_into().map_err(|_| Error::IntegerOverflow)?,
);
Expand Down Expand Up @@ -943,6 +991,10 @@ where
type Error = Error;

fn try_from(value: Vec<T>) -> Result<Self> {
if value.is_empty() {
return Ok(ZendHashTable::new_empty_immutable());
}

let mut ht = ZendHashTable::with_capacity(
value.len().try_into().map_err(|_| Error::IntegerOverflow)?,
);
Expand Down
9 changes: 8 additions & 1 deletion src/types/zval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,14 @@ impl Zval {
///
/// * `val` - The value to set the zval as.
pub fn set_hashtable(&mut self, val: ZBox<ZendHashTable>) {
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();
}

Expand Down
23 changes: 23 additions & 0 deletions tests/src/integration/array.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,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);
31 changes: 30 additions & 1 deletion tests/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -42,6 +45,32 @@ pub fn test_array_assoc(a: HashMap<String, String>) -> HashMap<String, String> {
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<u32>) -> Binary<u32> {
a
Expand Down

0 comments on commit 8316c30

Please sign in to comment.