Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions allowed_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ bind! {
zend_declare_class_constant,
zend_declare_property,
zend_do_implement_interface,
zend_empty_array,
zend_read_static_property,
zend_update_static_property,
zend_enum_add_case,
Expand Down Expand Up @@ -153,6 +154,9 @@ bind! {
E_RECOVERABLE_ERROR,
E_DEPRECATED,
E_USER_DEPRECATED,
GC_FLAGS_MASK,
GC_FLAGS_SHIFT,
GC_IMMUTABLE,
HT_MIN_SIZE,
IS_ARRAY,
IS_ARRAY_EX,
Expand Down
6 changes: 6 additions & 0 deletions docsrs_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,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;
Expand Down Expand Up @@ -769,6 +772,9 @@ pub const zend_hash_key_type_HASH_KEY_IS_STRING: zend_hash_key_type = 1;
pub const zend_hash_key_type_HASH_KEY_IS_LONG: zend_hash_key_type = 2;
pub const zend_hash_key_type_HASH_KEY_NON_EXISTENT: zend_hash_key_type = 3;
pub type zend_hash_key_type = ::std::os::raw::c_uint;
unsafe extern "C" {
pub static zend_empty_array: HashTable;
}
unsafe extern "C" {
pub fn zend_hash_clean(ht: *mut HashTable);
}
Expand Down
35 changes: 19 additions & 16 deletions src/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,22 @@ use crate::ffi::{
_IS_BOOL, 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, Z_TYPE_FLAGS_SHIFT, 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_USE_GUARDS,
ZEND_ACC_USES_THIS, ZEND_ACC_VARIADIC, ZEND_EVAL_CODE, ZEND_HAS_STATIC_IN_METHODS,
ZEND_INTERNAL_FUNCTION, ZEND_USER_FUNCTION,
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, Z_TYPE_FLAGS_SHIFT,
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_USE_GUARDS, ZEND_ACC_USES_THIS, ZEND_ACC_VARIADIC, ZEND_EVAL_CODE,
ZEND_HAS_STATIC_IN_METHODS, ZEND_INTERNAL_FUNCTION, ZEND_USER_FUNCTION,
};

use std::{convert::TryFrom, fmt::Display};
Expand Down Expand Up @@ -88,6 +88,9 @@ bitflags! {
const RefCounted = (IS_TYPE_REFCOUNTED << Z_TYPE_FLAGS_SHIFT);
/// Collectable
const Collectable = (IS_TYPE_COLLECTABLE << Z_TYPE_FLAGS_SHIFT);

/// Immutable (used for the shared empty array)
const Immutable = GC_IMMUTABLE;
}
}

Expand Down
102 changes: 98 additions & 4 deletions src/types/array/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ use crate::{
error::Result,
ffi::zend_ulong,
ffi::{
_zend_new_array, HT_MIN_SIZE, zend_array_count, zend_array_destroy, zend_array_dup,
zend_hash_clean, zend_hash_index_del, zend_hash_index_find, zend_hash_index_update,
zend_hash_next_index_insert, zend_hash_str_del, zend_hash_str_find, zend_hash_str_update,
_zend_new_array, GC_FLAGS_MASK, GC_FLAGS_SHIFT, HT_MIN_SIZE, zend_array_count,
zend_array_destroy, zend_array_dup, zend_empty_array, zend_hash_clean, zend_hash_index_del,
zend_hash_index_find, zend_hash_index_update, zend_hash_next_index_insert,
zend_hash_str_del, zend_hash_str_find, zend_hash_str_update,
},
flags::DataType,
flags::{DataType, ZvalTypeFlags},
types::Zval,
};

Expand Down Expand Up @@ -648,10 +649,37 @@ impl ZendHashTable {
pub fn iter(&self) -> Iter<'_> {
self.into_iter()
}

/// Determines whether this hashtable is immutable.
///
/// Immutable hashtables are shared and cannot be modified. The primary
/// example is the empty immutable shared array returned by
/// [`ZendEmptyArray`].
///
/// # Example
///
/// ```no_run
/// use ext_php_rs::types::ZendHashTable;
///
/// let ht = ZendHashTable::new();
/// assert!(!ht.is_immutable());
/// ```
#[must_use]
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
}
}

unsafe impl ZBoxable for ZendHashTable {
fn free(&mut self) {
// Do not attempt to free the immutable shared empty array.
if self.is_immutable() {
return;
}
// SAFETY: ZBox has immutable access to `self`.
unsafe { zend_array_destroy(self) }
}
Expand Down Expand Up @@ -719,3 +747,69 @@ impl<'a> FromZvalMut<'a> for &'a mut ZendHashTable {
zval.array_mut()
}
}

/// Represents an empty, immutable, shared PHP array.
///
/// Since PHP 7.3, it's possible for extensions to return a zval backed by
/// an immutable shared hashtable. This helps avoid redundant hashtable
/// allocations when returning empty arrays to userland PHP code.
///
/// This struct provides a safe way to return an empty array without allocating
/// a new hashtable. It implements [`IntoZval`] so it can be used as a return
/// type for PHP functions.
///
/// # Safety
///
/// Unlike [`ZendHashTable`], this type does not allow any mutation of the
/// underlying array, as it points to a shared static empty array in PHP's
/// memory.
///
/// # Example
///
/// ```rust,ignore
/// use ext_php_rs::prelude::*;
/// use ext_php_rs::types::ZendEmptyArray;
///
/// #[php_function]
/// pub fn get_empty_array() -> ZendEmptyArray {
/// ZendEmptyArray
/// }
/// ```
///
/// This is more efficient than returning `Vec::<i32>::new()` or creating
/// a new `ZendHashTable` when you know the result will be empty.
#[derive(Debug, Clone, Copy, Default)]
pub struct ZendEmptyArray;

impl ZendEmptyArray {
/// Returns a reference to the underlying immutable empty hashtable.
///
/// # Example
///
/// ```no_run
/// use ext_php_rs::types::ZendEmptyArray;
///
/// let empty = ZendEmptyArray;
/// let ht = empty.as_hashtable();
/// assert!(ht.is_empty());
/// assert!(ht.is_immutable());
/// ```
#[must_use]
pub fn as_hashtable(&self) -> &ZendHashTable {
// SAFETY: zend_empty_array is a static global initialized by PHP.
unsafe { &zend_empty_array }
}
}

impl IntoZval for ZendEmptyArray {
const TYPE: DataType = DataType::Array;
const NULLABLE: bool = false;

fn set_zval(self, zv: &mut Zval, _persistent: bool) -> Result<()> {
// Set the zval to point to the immutable shared empty array.
// This mirrors the ZVAL_EMPTY_ARRAY macro in PHP.
zv.u1.type_info = ZvalTypeFlags::Array.bits();
zv.value.arr = ptr::from_ref(self.as_hashtable()).cast_mut();
Ok(())
}
}
2 changes: 1 addition & 1 deletion src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ mod object;
mod string;
mod zval;

pub use array::{ArrayKey, ZendHashTable};
pub use array::{ArrayKey, ZendEmptyArray, ZendHashTable};
pub use callable::ZendCallable;
pub use class_object::ZendClassObject;
pub use iterable::Iterable;
Expand Down
9 changes: 8 additions & 1 deletion src/types/zval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,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 (e.g., the empty array) similar to
// ZVAL_EMPTY_ARRAY. Immutable arrays should not be reference counted.
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
30 changes: 30 additions & 0 deletions tests/src/integration/array/array.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,33 @@
assert($mut_arr['added_by_rust'] === 'value', 'Added value should be correct');
$null_arr = null;
assert(test_optional_array_mut_ref($null_arr) === -1, 'Option<&mut ZendHashTable> should accept null');

// Test ZendEmptyArray - returns an empty immutable shared array
$empty = test_empty_array();
assert(is_array($empty), 'ZendEmptyArray should return an array');
assert(count($empty) === 0, 'ZendEmptyArray should return an empty array');

// Verify we can add elements to a copy (proves it's transparent to userland)
$empty[] = 'added';
assert(count($empty) === 1, 'Should be able to add elements to a copy of the empty array');
assert($empty[0] === 'added', 'Added element should be accessible');

// Test is_immutable() for normal hashtables
assert(test_hashtable_is_immutable() === false, 'Normal ZendHashTable should not be immutable');

// Test is_immutable() for empty array
assert(test_empty_array_is_immutable() === true, 'Empty array from ZendEmptyArray should be immutable');

// Test returning empty Vec (should still work)
$empty_vec = test_empty_vec();
assert(is_array($empty_vec), 'Empty Vec should return an array');
assert(count($empty_vec) === 0, 'Empty Vec should return an empty array');
$empty_vec[] = 42;
assert(count($empty_vec) === 1, 'Should be able to add elements to empty Vec result');

// Test returning empty HashMap (should still work)
$empty_hashmap = test_empty_hashmap();
assert(is_array($empty_hashmap), 'Empty HashMap should return an array');
assert(count($empty_hashmap) === 0, 'Empty HashMap should return an empty array');
$empty_hashmap['key'] = 'value';
assert(count($empty_hashmap) === 1, 'Should be able to add elements to empty HashMap result');
38 changes: 37 additions & 1 deletion tests/src/integration/array/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use ext_php_rs::{
ffi::HashTable,
php_function,
prelude::ModuleBuilder,
types::{ArrayKey, ZendHashTable, Zval},
types::{ArrayKey, ZendEmptyArray, ZendHashTable, Zval},
wrap_function,
};

Expand Down Expand Up @@ -60,6 +60,37 @@ pub fn test_optional_array_mut_ref(arr: Option<&mut ZendHashTable>) -> i64 {
}
}

/// Test returning an empty immutable array using `ZendEmptyArray`
#[php_function]
pub fn test_empty_array() -> ZendEmptyArray {
ZendEmptyArray
}

/// Test that a normal `ZendHashTable` is not immutable
#[php_function]
pub fn test_hashtable_is_immutable() -> bool {
let ht = ZendHashTable::new();
ht.is_immutable()
}

/// Test that the empty array from `ZendEmptyArray` is immutable
#[php_function]
pub fn test_empty_array_is_immutable() -> bool {
ZendEmptyArray.as_hashtable().is_immutable()
}

/// Test that returning an empty `Vec` still works (should allocate a new array)
#[php_function]
pub fn test_empty_vec() -> Vec<i32> {
Vec::new()
}

/// Test that returning an empty `HashMap` still works (should allocate a new array)
#[php_function]
pub fn test_empty_hashmap() -> HashMap<String, String> {
HashMap::new()
}

pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
builder
.function(wrap_function!(test_array))
Expand All @@ -69,6 +100,11 @@ pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
.function(wrap_function!(test_array_keys))
.function(wrap_function!(test_optional_array_ref))
.function(wrap_function!(test_optional_array_mut_ref))
.function(wrap_function!(test_empty_array))
.function(wrap_function!(test_hashtable_is_immutable))
.function(wrap_function!(test_empty_array_is_immutable))
.function(wrap_function!(test_empty_vec))
.function(wrap_function!(test_empty_hashmap))
}

#[cfg(test)]
Expand Down