Skip to content

feat: add support for empty immutable shared arrays #355

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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 @@ -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
104 changes: 64 additions & 40 deletions docsrs_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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],
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
}
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
56 changes: 50 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()` panics if the pointer is null.
ZBox::from_raw(ptr.as_mut().expect("zend_empty_array inconsistent"))
}
}
Comment on lines +85 to +93
Copy link
Collaborator

@Xenira Xenira Feb 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably not return a mutable vallue, as that allows mutable access to the immutable hashtable.

    let mut ht = ZendHashTable::new_empty_immutable();
    ht.insert("key", 1).unwrap();

would be valid but results in signal: 11 (SIGSEGV) (core dumped)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delayed update—yeah, this definitely isn't ideal.

std::ptr only supports mutable pointers, so making the backing ptr immutable wouldn't play well with ZBox.

Maybe the right way to go here is to have a separate helper similar to the RETURN_EMPTY_ARRAY C macro in the Zend API, since really the only major use case for this in extensions is to return an empty array to userland without overhead. Then particularly performance-sensitive extensions can opt into using this as needed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just have a struct ZendEmptyArray; that implements IntoZval?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems like a good solution, thanks.


/// 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
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
Loading