Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
mszabo-wikia committed Jan 28, 2025
1 parent 464407b commit 8316c30
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
@@ -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,
34 changes: 18 additions & 16 deletions src/flags.rs
Original file line number Diff line number Diff line change
@@ -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;
}
}

64 changes: 58 additions & 6 deletions src/types/array.rs
Original file line number Diff line number Diff line change
@@ -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<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`].
///
@@ -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) }
}
}
}

@@ -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)?,
);
@@ -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)?,
);
9 changes: 8 additions & 1 deletion src/types/zval.rs
Original file line number Diff line number Diff line change
@@ -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();
}

23 changes: 23 additions & 0 deletions tests/src/integration/array.php
Original file line number Diff line number Diff line change
@@ -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]
@@ -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

0 comments on commit 8316c30

Please sign in to comment.