Merge pull request #268 from joelwurtz/feat/iterator

feat(iterator): add helper for zend_object_iterator and iterable type
This commit is contained in:
Daniil Gentili 2023-11-24 18:52:00 +01:00 committed by GitHub
commit 3d66f175c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 838 additions and 103 deletions

View File

@ -94,6 +94,7 @@ bind! {
zend_internal_arg_info, zend_internal_arg_info,
zend_is_callable, zend_is_callable,
zend_is_identical, zend_is_identical,
zend_is_iterable,
zend_long, zend_long,
zend_lookup_class_ex, zend_lookup_class_ex,
zend_module_entry, zend_module_entry,
@ -163,6 +164,7 @@ bind! {
IS_UNDEF, IS_UNDEF,
IS_VOID, IS_VOID,
IS_PTR, IS_PTR,
IS_ITERABLE,
MAY_BE_ANY, MAY_BE_ANY,
MAY_BE_BOOL, MAY_BE_BOOL,
PHP_INI_USER, PHP_INI_USER,

View File

@ -97,6 +97,7 @@ pub const IS_RESOURCE: u32 = 9;
pub const IS_REFERENCE: u32 = 10; pub const IS_REFERENCE: u32 = 10;
pub const IS_CONSTANT_AST: u32 = 11; pub const IS_CONSTANT_AST: u32 = 11;
pub const IS_CALLABLE: u32 = 12; pub const IS_CALLABLE: u32 = 12;
pub const IS_ITERABLE: u32 = 13;
pub const IS_VOID: u32 = 14; pub const IS_VOID: u32 = 14;
pub const IS_MIXED: u32 = 16; pub const IS_MIXED: u32 = 16;
pub const IS_INDIRECT: u32 = 12; pub const IS_INDIRECT: u32 = 12;
@ -1658,6 +1659,9 @@ extern "C" {
named_params: *mut HashTable, named_params: *mut HashTable,
); );
} }
extern "C" {
pub fn zend_is_iterable(iterable: *const zval) -> bool;
}
pub const _zend_expected_type_Z_EXPECTED_LONG: _zend_expected_type = 0; pub const _zend_expected_type_Z_EXPECTED_LONG: _zend_expected_type = 0;
pub const _zend_expected_type_Z_EXPECTED_LONG_OR_NULL: _zend_expected_type = 1; pub const _zend_expected_type_Z_EXPECTED_LONG_OR_NULL: _zend_expected_type = 1;
pub const _zend_expected_type_Z_EXPECTED_BOOL: _zend_expected_type = 2; pub const _zend_expected_type_Z_EXPECTED_BOOL: _zend_expected_type = 2;

View File

@ -0,0 +1,55 @@
# `Iterable`
`Iterable`s are represented either by an `array` or `Traversable` type.
| `T` parameter | `&T` parameter | `T` Return type | `&T` Return type | PHP representation |
|---------------|----------------|-----------------| ---------------- |----------------------------------|
| Yes | No | No | No | `ZendHashTable` or `ZendIterator` |
Converting from a zval to a `Iterable` is valid when the value is either an array or an object
that implements the `Traversable` interface. This means that any value that can be used in a
`foreach` loop can be converted into a `Iterable`.
## Rust example
```rust,no_run
# #![cfg_attr(windows, feature(abi_vectorcall))]
# extern crate ext_php_rs;
# use ext_php_rs::prelude::*;
# use ext_php_rs::types::Iterable;
#[php_function]
pub fn test_iterable(mut iterable: Iterable) {
for (k, v) in iterable.iter().expect("cannot rewind iterator") {
println!("k: {} v: {}", k.string().unwrap(), v.string().unwrap());
}
}
# fn main() {}
```
## PHP example
```php
<?php
$generator = function() {
yield 'hello' => 'world';
yield 'rust' => 'php';
};
$array = [
'hello' => 'world',
'rust' => 'php',
];
test_iterable($generator());
test_iterable($array);
```
Output:
```text
k: hello v: world
k: rust v: php
k: hello v: world
k: rust v: php
```

View File

@ -0,0 +1,52 @@
# `ZendIterator`
`ZendIterator`s are represented by the `Traversable` type.
| `T` parameter | `&T` parameter | `T` Return type | `&T` Return type | PHP representation |
|---------------| -------------- |-----------------| ---------------- | ------------------ |
| No | Yes | No | No | `ZendIterator` |
Converting from a zval to a `ZendIterator` is valid when there is an associated iterator to
the variable. This means that any value, at the exception of an `array`, that can be used in
a `foreach` loop can be converted into a `ZendIterator`. As an example, a `Generator` can be
used but also a the result of a `query` call with `PDO`.
If you want a more universal `iterable` type that also supports arrays, see [Iterable](./iterable.md).
## Rust example
```rust,no_run
# #![cfg_attr(windows, feature(abi_vectorcall))]
# extern crate ext_php_rs;
# use ext_php_rs::prelude::*;
# use ext_php_rs::types::ZendIterator;
#[php_function]
pub fn test_iterator(iterator: &mut ZendIterator) {
for (k, v) in iterator.iter().expect("cannot rewind iterator") {
// Note that the key can be anything, even an object
// when iterating over Traversables!
println!("k: {} v: {}", k.string().unwrap(), v.string().unwrap());
}
}
# fn main() {}
```
## PHP example
```php
<?php
$generator = function() {
yield 'hello' => 'world';
yield 'rust' => 'php';
};
test_iterator($generator());
```
Output:
```text
k: hello v: world
k: rust v: php
```

View File

@ -172,6 +172,7 @@ impl ToStub for DataType {
DataType::Reference => "reference", DataType::Reference => "reference",
DataType::Callable => "callable", DataType::Callable => "callable",
DataType::Bool => "bool", DataType::Bool => "bool",
DataType::Iterable => "iterable",
_ => "mixed", _ => "mixed",
} }
) )

View File

@ -8,10 +8,10 @@ use crate::ffi::{
CONST_CS, CONST_DEPRECATED, CONST_NO_FILE_CACHE, CONST_PERSISTENT, E_COMPILE_ERROR, 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_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_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_LONG, E_WARNING, IS_ARRAY, IS_CALLABLE, IS_CONSTANT_AST, IS_DOUBLE, IS_FALSE, IS_INDIRECT,
IS_MIXED, IS_NULL, IS_OBJECT, IS_PTR, IS_REFERENCE, IS_RESOURCE, IS_STRING, IS_TRUE, IS_ITERABLE, IS_LONG, IS_MIXED, IS_NULL, IS_OBJECT, IS_PTR, IS_REFERENCE, IS_RESOURCE,
IS_TYPE_COLLECTABLE, IS_TYPE_REFCOUNTED, IS_UNDEF, IS_VOID, PHP_INI_ALL, PHP_INI_PERDIR, IS_STRING, IS_TRUE, IS_TYPE_COLLECTABLE, IS_TYPE_REFCOUNTED, IS_UNDEF, IS_VOID, PHP_INI_ALL,
PHP_INI_SYSTEM, PHP_INI_USER, ZEND_ACC_ABSTRACT, ZEND_ACC_ANON_CLASS, 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_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_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_FAKE_CLOSURE, ZEND_ACC_FINAL, ZEND_ACC_GENERATOR, ZEND_ACC_HAS_FINALLY_BLOCK,
@ -49,6 +49,7 @@ bitflags! {
const ConstantExpression = IS_CONSTANT_AST; const ConstantExpression = IS_CONSTANT_AST;
const Void = IS_VOID; const Void = IS_VOID;
const Ptr = IS_PTR; const Ptr = IS_PTR;
const Iterable = IS_ITERABLE;
const InternedStringEx = Self::String.bits(); const InternedStringEx = Self::String.bits();
const StringEx = Self::String.bits() | Self::RefCounted.bits(); const StringEx = Self::String.bits() | Self::RefCounted.bits();
@ -237,6 +238,7 @@ pub enum DataType {
Double, Double,
String, String,
Array, Array,
Iterable,
Object(Option<&'static str>), Object(Option<&'static str>),
Resource, Resource,
Reference, Reference,
@ -277,6 +279,7 @@ impl DataType {
DataType::Mixed => IS_MIXED, DataType::Mixed => IS_MIXED,
DataType::Bool => _IS_BOOL, DataType::Bool => _IS_BOOL,
DataType::Ptr => IS_PTR, DataType::Ptr => IS_PTR,
DataType::Iterable => IS_ITERABLE,
} }
} }
} }
@ -383,6 +386,7 @@ impl Display for DataType {
DataType::Mixed => write!(f, "Mixed"), DataType::Mixed => write!(f, "Mixed"),
DataType::Ptr => write!(f, "Pointer"), DataType::Ptr => write!(f, "Pointer"),
DataType::Indirect => write!(f, "Indirect"), DataType::Indirect => write!(f, "Indirect"),
DataType::Iterable => write!(f, "Iterable"),
} }
} }
} }

View File

@ -5,7 +5,7 @@ use std::{
collections::HashMap, collections::HashMap,
convert::{TryFrom, TryInto}, convert::{TryFrom, TryInto},
ffi::CString, ffi::CString,
fmt::Debug, fmt::{Debug, Display},
iter::FromIterator, iter::FromIterator,
u64, u64,
}; };
@ -463,7 +463,7 @@ impl ZendHashTable {
/// assert!(!ht.has_numerical_keys()); /// assert!(!ht.has_numerical_keys());
/// ``` /// ```
pub fn has_numerical_keys(&self) -> bool { pub fn has_numerical_keys(&self) -> bool {
!self.iter().any(|(_, k, _)| k.is_some()) !self.into_iter().any(|(k, _)| !k.is_long())
} }
/// Checks if the hashtable has numerical, sequential keys. /// Checks if the hashtable has numerical, sequential keys.
@ -490,31 +490,9 @@ impl ZendHashTable {
/// ``` /// ```
pub fn has_sequential_keys(&self) -> bool { pub fn has_sequential_keys(&self) -> bool {
!self !self
.iter() .into_iter()
.enumerate() .enumerate()
.any(|(i, (k, strk, _))| i as u64 != k || strk.is_some()) .any(|(i, (k, _))| ArrayKey::Long(i as i64) != k)
}
/// Returns an iterator over the key(s) and value contained inside the
/// hashtable.
///
/// # Example
///
/// ```no_run
/// use ext_php_rs::types::ZendHashTable;
///
/// let mut ht = ZendHashTable::new();
///
/// for (idx, key, val) in ht.iter() {
/// // ^ Index if inserted at an index.
/// // ^ Optional string key, if inserted like a hashtable.
/// // ^ Inserted value.
///
/// dbg!(idx, key, val);
/// }
#[inline]
pub fn iter(&self) -> Iter {
Iter::new(self)
} }
/// Returns an iterator over the values contained inside the hashtable, as /// Returns an iterator over the values contained inside the hashtable, as
@ -534,6 +512,28 @@ impl ZendHashTable {
pub fn values(&self) -> Values { pub fn values(&self) -> Values {
Values::new(self) Values::new(self)
} }
/// Returns an iterator over the key(s) and value contained inside the
/// hashtable.
///
/// # Example
///
/// ```no_run
/// use ext_php_rs::types::ZendHashTable;
///
/// let mut ht = ZendHashTable::new();
///
/// for (key, val) in ht.iter() {
/// // ^ Index if inserted at an index.
/// // ^ Optional string key, if inserted like a hashtable.
/// // ^ Inserted value.
///
/// dbg!(key, val);
/// }
#[inline]
pub fn iter(&self) -> Iter {
self.into_iter()
}
} }
unsafe impl ZBoxable for ZendHashTable { unsafe impl ZBoxable for ZendHashTable {
@ -546,10 +546,7 @@ unsafe impl ZBoxable for ZendHashTable {
impl Debug for ZendHashTable { impl Debug for ZendHashTable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_map() f.debug_map()
.entries( .entries(self.into_iter().map(|(k, v)| (k.to_string(), v)))
self.iter()
.map(|(k, k2, v)| (k2.unwrap_or_else(|| k.to_string()), v)),
)
.finish() .finish()
} }
} }
@ -574,10 +571,54 @@ impl ToOwned for ZendHashTable {
/// Immutable iterator upon a reference to a hashtable. /// Immutable iterator upon a reference to a hashtable.
pub struct Iter<'a> { pub struct Iter<'a> {
ht: &'a ZendHashTable, ht: &'a ZendHashTable,
current_num: u64, current_num: i64,
pos: HashPosition, pos: HashPosition,
} }
#[derive(Debug, PartialEq)]
pub enum ArrayKey {
Long(i64),
String(String),
}
/// Represent the key of a PHP array, which can be either a long or a string.
impl ArrayKey {
/// Check if the key is an integer.
///
/// # Returns
///
/// Returns true if the key is an integer, false otherwise.
pub fn is_long(&self) -> bool {
match self {
ArrayKey::Long(_) => true,
ArrayKey::String(_) => false,
}
}
}
impl Display for ArrayKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ArrayKey::Long(key) => write!(f, "{}", key),
ArrayKey::String(key) => write!(f, "{}", key),
}
}
}
impl<'a> FromZval<'a> for ArrayKey {
const TYPE: DataType = DataType::String;
fn from_zval(zval: &'a Zval) -> Option<Self> {
if let Some(key) = zval.long() {
return Some(ArrayKey::Long(key));
}
if let Some(key) = zval.string() {
return Some(ArrayKey::String(key));
}
None
}
}
impl<'a> Iter<'a> { impl<'a> Iter<'a> {
/// Creates a new iterator over a hashtable. /// Creates a new iterator over a hashtable.
/// ///
@ -593,49 +634,39 @@ impl<'a> Iter<'a> {
} }
} }
impl<'a> IntoIterator for &'a ZendHashTable {
type Item = (ArrayKey, &'a Zval);
type IntoIter = Iter<'a>;
/// Returns an iterator over the key(s) and value contained inside the
/// hashtable.
///
/// # Example
///
/// ```no_run
/// use ext_php_rs::types::ZendHashTable;
///
/// let mut ht = ZendHashTable::new();
///
/// for (key, val) in ht.iter() {
/// // ^ Index if inserted at an index.
/// // ^ Optional string key, if inserted like a hashtable.
/// // ^ Inserted value.
///
/// dbg!(key, val);
/// }
#[inline]
fn into_iter(self) -> Self::IntoIter {
Iter::new(self)
}
}
impl<'a> Iterator for Iter<'a> { impl<'a> Iterator for Iter<'a> {
type Item = (u64, Option<String>, &'a Zval); type Item = (ArrayKey, &'a Zval);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let key_type = unsafe { self.next_zval()
zend_hash_get_current_key_type_ex( .map(|(k, v)| (ArrayKey::from_zval(&k).expect("Invalid array key!"), v))
self.ht as *const ZendHashTable as *mut ZendHashTable,
&mut self.pos as *mut HashPosition,
)
};
if key_type == -1 {
return None;
}
let key = Zval::new();
unsafe {
zend_hash_get_current_key_zval_ex(
self.ht as *const ZendHashTable as *mut ZendHashTable,
&key as *const Zval as *mut Zval,
&mut self.pos as *mut HashPosition,
);
}
let value = unsafe {
&*zend_hash_get_current_data_ex(
self.ht as *const ZendHashTable as *mut ZendHashTable,
&mut self.pos as *mut HashPosition,
)
};
let r: (u64, Option<String>, &Zval) = match key.is_long() {
true => (key.long().unwrap_or(0) as u64, None, value),
false => (self.current_num, key.try_into().ok(), value),
};
unsafe {
zend_hash_move_forward_ex(
self.ht as *const ZendHashTable as *mut ZendHashTable,
&mut self.pos as *mut HashPosition,
)
};
self.current_num += 1;
Some(r)
} }
fn count(self) -> usize fn count(self) -> usize
@ -666,6 +697,7 @@ impl<'a> DoubleEndedIterator for Iter<'a> {
} }
let key = Zval::new(); let key = Zval::new();
unsafe { unsafe {
zend_hash_get_current_key_zval_ex( zend_hash_get_current_key_zval_ex(
self.ht as *const ZendHashTable as *mut ZendHashTable, self.ht as *const ZendHashTable as *mut ZendHashTable,
@ -679,9 +711,10 @@ impl<'a> DoubleEndedIterator for Iter<'a> {
&mut self.pos as *mut HashPosition, &mut self.pos as *mut HashPosition,
) )
}; };
let r: (u64, Option<String>, &Zval) = match key.is_long() {
true => (key.long().unwrap_or(0) as u64, None, value), let key = match ArrayKey::from_zval(&key) {
false => (self.current_num, key.try_into().ok(), value), Some(key) => key,
None => ArrayKey::Long(self.current_num),
}; };
unsafe { unsafe {
@ -692,7 +725,52 @@ impl<'a> DoubleEndedIterator for Iter<'a> {
}; };
self.current_num -= 1; self.current_num -= 1;
Some(r) Some((key, value))
}
}
impl<'a, 'b> Iter<'a> {
pub fn next_zval(&'b mut self) -> Option<(Zval, &'a Zval)> {
let key_type = unsafe {
zend_hash_get_current_key_type_ex(
self.ht as *const ZendHashTable as *mut ZendHashTable,
&mut self.pos as *mut HashPosition,
)
};
if key_type == -1 {
return None;
}
let mut key = Zval::new();
unsafe {
zend_hash_get_current_key_zval_ex(
self.ht as *const ZendHashTable as *mut ZendHashTable,
&key as *const Zval as *mut Zval,
&mut self.pos as *mut HashPosition,
);
}
let value = unsafe {
&*zend_hash_get_current_data_ex(
self.ht as *const ZendHashTable as *mut ZendHashTable,
&mut self.pos as *mut HashPosition,
)
};
if !key.is_long() && !key.is_string() {
key.set_long(self.current_num)
}
unsafe {
zend_hash_move_forward_ex(
self.ht as *const ZendHashTable as *mut ZendHashTable,
&mut self.pos as *mut HashPosition,
)
};
self.current_num += 1;
Some((key, value))
} }
} }
@ -715,7 +793,7 @@ impl<'a> Iterator for Values<'a> {
type Item = &'a Zval; type Item = &'a Zval;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(|(_, _, zval)| zval) self.0.next().map(|(_, zval)| zval)
} }
fn count(self) -> usize fn count(self) -> usize
@ -734,7 +812,7 @@ impl<'a> ExactSizeIterator for Values<'a> {
impl<'a> DoubleEndedIterator for Values<'a> { impl<'a> DoubleEndedIterator for Values<'a> {
fn next_back(&mut self) -> Option<Self::Item> { fn next_back(&mut self) -> Option<Self::Item> {
self.0.next_back().map(|(_, _, zval)| zval) self.0.next_back().map(|(_, zval)| zval)
} }
} }
@ -780,9 +858,9 @@ where
fn try_from(value: &'a ZendHashTable) -> Result<Self> { fn try_from(value: &'a ZendHashTable) -> Result<Self> {
let mut hm = HashMap::with_capacity(value.len()); let mut hm = HashMap::with_capacity(value.len());
for (idx, key, val) in value.iter() { for (key, val) in value {
hm.insert( hm.insert(
key.unwrap_or_else(|| idx.to_string()), key.to_string(),
V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?, V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?,
); );
} }
@ -849,7 +927,7 @@ where
fn try_from(value: &'a ZendHashTable) -> Result<Self> { fn try_from(value: &'a ZendHashTable) -> Result<Self> {
let mut vec = Vec::with_capacity(value.len()); let mut vec = Vec::with_capacity(value.len());
for (_, _, val) in value.iter() { for (_, val) in value {
vec.push(T::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?); vec.push(T::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?);
} }

66
src/types/iterable.rs Normal file
View File

@ -0,0 +1,66 @@
use super::array::Iter as ZendHashTableIter;
use super::iterator::Iter as ZendIteratorIter;
use crate::convert::FromZval;
use crate::flags::DataType;
use crate::types::{ZendHashTable, ZendIterator, Zval};
/// This type represents a PHP iterable, which can be either an array or an
/// object implementing the Traversable interface.
#[derive(Debug)]
pub enum Iterable<'a> {
Array(&'a ZendHashTable),
Traversable(&'a mut ZendIterator),
}
impl<'a> Iterable<'a> {
/// Creates a new rust iterator from a PHP iterable.
/// May return None if a Traversable cannot be rewound.
pub fn iter(&mut self) -> Option<Iter> {
match self {
Iterable::Array(array) => Some(Iter::Array(array.iter())),
Iterable::Traversable(traversable) => Some(Iter::Traversable(traversable.iter()?)),
}
}
}
impl<'a> IntoIterator for &'a mut Iterable<'a> {
type Item = (Zval, &'a Zval);
type IntoIter = Iter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter().expect("Could not rewind iterator!")
}
}
impl<'a> FromZval<'a> for Iterable<'a> {
const TYPE: DataType = DataType::Iterable;
fn from_zval(zval: &'a Zval) -> Option<Self> {
if let Some(array) = zval.array() {
return Some(Iterable::Array(array));
}
if let Some(traversable) = zval.traversable() {
return Some(Iterable::Traversable(traversable));
}
None
}
}
/// Rust iterator over a PHP iterable.
pub enum Iter<'a> {
Array(ZendHashTableIter<'a>),
Traversable(ZendIteratorIter<'a>),
}
impl<'a> Iterator for Iter<'a> {
type Item = (Zval, &'a Zval);
fn next(&mut self) -> Option<Self::Item> {
match self {
Iter::Array(array) => array.next_zval(),
Iter::Traversable(traversable) => traversable.next(),
}
}
}

327
src/types/iterator.rs Normal file
View File

@ -0,0 +1,327 @@
use crate::convert::FromZvalMut;
use crate::ffi::{zend_object_iterator, ZEND_RESULT_CODE_SUCCESS};
use crate::flags::DataType;
use crate::types::Zval;
use crate::zend::ExecutorGlobals;
use std::fmt::{Debug, Formatter};
/// A PHP Iterator.
///
/// In PHP, iterators are represented as zend_object_iterator. This allows user
/// to iterate over objects implementing Traversable interface using foreach.
///
/// Use ZendIterable to iterate over both iterators and arrays.
pub type ZendIterator = zend_object_iterator;
impl ZendIterator {
/// Creates a new rust iterator from a zend_object_iterator.
///
/// Returns a iterator over the zend_object_iterator, or None if the
/// iterator cannot be rewound.
pub fn iter(&mut self) -> Option<Iter> {
self.index = 0;
if self.rewind() {
return Some(Iter { zi: self });
}
None
}
/// Check if the current position of the iterator is valid.
///
/// As an example this will call the user defined valid method of the
/// ['\Iterator'] interface. see <https://www.php.net/manual/en/iterator.valid.php>
pub fn valid(&mut self) -> bool {
if let Some(valid) = unsafe { (*self.funcs).valid } {
let valid = unsafe { valid(&mut *self) == ZEND_RESULT_CODE_SUCCESS };
if ExecutorGlobals::has_exception() {
return false;
}
valid
} else {
true
}
}
/// Rewind the iterator to the first element.
///
/// As an example this will call the user defined rewind method of the
/// ['\Iterator'] interface. see <https://www.php.net/manual/en/iterator.rewind.php>
///
/// # Returns
///
/// Returns true if the iterator was successfully rewind, false otherwise.
/// (when there is an exception during rewind)
pub fn rewind(&mut self) -> bool {
if let Some(rewind) = unsafe { (*self.funcs).rewind } {
unsafe {
rewind(&mut *self);
}
}
!ExecutorGlobals::has_exception()
}
/// Move the iterator forward to the next element.
///
/// As an example this will call the user defined next method of the
/// ['\Iterator'] interface. see <https://www.php.net/manual/en/iterator.next.php>
///
/// # Returns
///
/// Returns true if the iterator was successfully move, false otherwise.
/// (when there is an exception during next)
pub fn move_forward(&mut self) -> bool {
if let Some(move_forward) = unsafe { (*self.funcs).move_forward } {
unsafe {
move_forward(&mut *self);
}
}
!ExecutorGlobals::has_exception()
}
/// Get the current data of the iterator.
///
/// # Returns
///
/// Returns a reference to the current data of the iterator if available
/// , ['None'] otherwise.
pub fn get_current_data<'a>(&mut self) -> Option<&'a Zval> {
let get_current_data = unsafe { (*self.funcs).get_current_data }?;
let value = unsafe { &*get_current_data(&mut *self) };
if ExecutorGlobals::has_exception() {
return None;
}
Some(value)
}
/// Get the current key of the iterator.
///
/// # Returns
///
/// Returns a new ['Zval'] containing the current key of the iterator if
/// available , ['None'] otherwise.
pub fn get_current_key(&mut self) -> Option<Zval> {
let get_current_key = unsafe { (*self.funcs).get_current_key? };
let mut key = Zval::new();
unsafe {
get_current_key(&mut *self, &mut key);
}
if ExecutorGlobals::has_exception() {
return None;
}
Some(key)
}
}
impl<'a> IntoIterator for &'a mut ZendIterator {
type Item = (Zval, &'a Zval);
type IntoIter = Iter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter().expect("Could not rewind iterator!")
}
}
impl Debug for ZendIterator {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ZendIterator").finish()
}
}
/// Immutable iterator upon a reference to a PHP iterator.
pub struct Iter<'a> {
zi: &'a mut ZendIterator,
}
impl<'a> Iterator for Iter<'a> {
type Item = (Zval, &'a Zval);
fn next(&mut self) -> Option<Self::Item> {
// Call next when index > 0, so next is really called at the start of each
// iteration, which allow to work better with generator iterator
if self.zi.index > 0 && !self.zi.move_forward() {
return None;
}
if !self.zi.valid() {
return None;
}
self.zi.index += 1;
let real_index = self.zi.index - 1;
let key = match self.zi.get_current_key() {
None => {
let mut z = Zval::new();
z.set_long(real_index as i64);
z
}
Some(key) => key,
};
self.zi.get_current_data().map(|value| (key, value))
}
}
impl<'a> FromZvalMut<'a> for &'a mut ZendIterator {
const TYPE: DataType = DataType::Object(Some("Traversable"));
fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
zval.object()?.get_class_entry().get_iterator(zval, false)
}
}
#[cfg(test)]
#[cfg(feature = "embed")]
mod tests {
use crate::embed::Embed;
#[test]
fn test_generator() {
Embed::run(|| {
let result = Embed::run_script("src/types/iterator.test.php");
assert!(result.is_ok());
let generator = Embed::eval("$generator;");
assert!(generator.is_ok());
let zval = generator.unwrap();
assert!(zval.is_traversable());
let iterator = zval.traversable().unwrap();
assert!(iterator.valid());
{
let mut iter = iterator.iter().unwrap();
let (key, value) = iter.next().unwrap();
assert_eq!(key.long(), Some(0));
assert!(value.is_long());
assert_eq!(value.long().unwrap(), 1);
let (key, value) = iter.next().unwrap();
assert_eq!(key.long(), Some(1));
assert!(value.is_long());
assert_eq!(value.long().unwrap(), 2);
let (key, value) = iter.next().unwrap();
assert_eq!(key.long(), Some(2));
assert!(value.is_long());
assert_eq!(value.long().unwrap(), 3);
let (key, value) = iter.next().unwrap();
assert!(key.is_object());
assert!(value.is_object());
let next = iter.next();
assert!(next.is_none());
}
});
}
#[test]
fn test_iterator() {
Embed::run(|| {
let result = Embed::run_script("src/types/iterator.test.php");
assert!(result.is_ok());
let generator = Embed::eval("$iterator;");
assert!(generator.is_ok());
let zval = generator.unwrap();
assert!(zval.is_traversable());
let iterator = zval.traversable().unwrap();
assert!(iterator.valid());
{
let mut iter = iterator.iter().unwrap();
let (key, value) = iter.next().unwrap();
assert!(!key.is_long());
assert_eq!(key.str(), Some("key"));
assert!(value.is_string());
assert_eq!(value.str(), Some("foo"));
let (key, value) = iter.next().unwrap();
assert!(key.is_long());
assert_eq!(key.long(), Some(10));
assert!(value.is_string());
assert_eq!(value.string().unwrap(), "bar");
let (key, value) = iter.next().unwrap();
assert_eq!(key.long(), Some(2));
assert!(value.is_string());
assert_eq!(value.string().unwrap(), "baz");
let (key, value) = iter.next().unwrap();
assert!(key.is_object());
assert!(value.is_object());
let next = iter.next();
assert!(next.is_none());
}
// Test rewind
{
let mut iter = iterator.iter().unwrap();
let (key, value) = iter.next().unwrap();
assert_eq!(key.str(), Some("key"));
assert!(value.is_string());
assert_eq!(value.string().unwrap(), "foo");
let (key, value) = iter.next().unwrap();
assert_eq!(key.long(), Some(10));
assert!(value.is_string());
assert_eq!(value.string().unwrap(), "bar");
let (key, value) = iter.next().unwrap();
assert_eq!(key.long(), Some(2));
assert!(value.is_string());
assert_eq!(value.string().unwrap(), "baz");
let (key, value) = iter.next().unwrap();
assert!(key.is_object());
assert!(value.is_object());
let next = iter.next();
assert!(next.is_none());
}
});
}
}

View File

@ -0,0 +1,52 @@
<?php
function create_generator() {
yield 1;
yield 2;
yield 3;
yield new class {} => new class {};
}
class TestIterator implements \Iterator {
private $count = 0;
public function current(): mixed
{
return match ($this->count) {
0 => 'foo',
1 => 'bar',
2 => 'baz',
3 => new class {},
default => null,
};
}
public function next(): void
{
$this->count++;
}
public function key(): mixed
{
return match ($this->count) {
0 => 'key',
1 => 10,
2 => 2,
3 => new class {},
default => null,
};
}
public function valid(): bool
{
return $this->count < 4;
}
public function rewind(): void
{
$this->count = 0;
}
}
$generator = create_generator();
$iterator = new TestIterator();

View File

@ -6,6 +6,8 @@
mod array; mod array;
mod callable; mod callable;
mod class_object; mod class_object;
mod iterable;
mod iterator;
mod long; mod long;
mod object; mod object;
mod string; mod string;
@ -14,6 +16,8 @@ mod zval;
pub use array::ZendHashTable; pub use array::ZendHashTable;
pub use callable::ZendCallable; pub use callable::ZendCallable;
pub use class_object::ZendClassObject; pub use class_object::ZendClassObject;
pub use iterable::Iterable;
pub use iterator::ZendIterator;
pub use long::ZendLong; pub use long::ZendLong;
pub use object::{PropertyQuery, ZendObject}; pub use object::{PropertyQuery, ZendObject};
pub use string::ZendStr; pub use string::ZendStr;

View File

@ -133,6 +133,15 @@ impl ZendObject {
(self.ce as *const ClassEntry).eq(&(T::get_metadata().ce() as *const _)) (self.ce as *const ClassEntry).eq(&(T::get_metadata().ce() as *const _))
} }
/// Returns whether this object is an instance of \Traversable
///
/// # Panics
///
/// Panics if the class entry is invalid.
pub fn is_traversable(&self) -> bool {
self.instance_of(ce::traversable())
}
#[inline(always)] #[inline(always)]
pub fn try_call_method(&self, name: &str, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> { pub fn try_call_method(&self, name: &str, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
let mut retval = Zval::new(); let mut retval = Zval::new();
@ -317,8 +326,8 @@ impl Debug for ZendObject {
); );
if let Ok(props) = self.get_properties() { if let Ok(props) = self.get_properties() {
for (id, key, val) in props.iter() { for (key, val) in props.iter() {
dbg.field(key.unwrap_or_else(|| id.to_string()).as_str(), val); dbg.field(key.to_string().as_str(), val);
} }
} }

View File

@ -4,6 +4,8 @@
use std::{convert::TryInto, ffi::c_void, fmt::Debug, ptr}; use std::{convert::TryInto, ffi::c_void, fmt::Debug, ptr};
use crate::types::iterable::Iterable;
use crate::types::ZendIterator;
use crate::{ use crate::{
binary::Pack, binary::Pack,
binary_slice::PackSlice, binary_slice::PackSlice,
@ -12,7 +14,7 @@ use crate::{
error::{Error, Result}, error::{Error, Result},
ffi::{ ffi::{
_zval_struct__bindgen_ty_1, _zval_struct__bindgen_ty_2, zend_is_callable, _zval_struct__bindgen_ty_1, _zval_struct__bindgen_ty_2, zend_is_callable,
zend_is_identical, zend_resource, zend_value, zval, zval_ptr_dtor, zend_is_identical, zend_is_iterable, zend_resource, zend_value, zval, zval_ptr_dtor,
}, },
flags::DataType, flags::DataType,
flags::ZvalTypeFlags, flags::ZvalTypeFlags,
@ -256,6 +258,25 @@ impl Zval {
ZendCallable::new(self).ok() ZendCallable::new(self).ok()
} }
/// Returns an iterator over the zval if it is traversable.
pub fn traversable(&self) -> Option<&mut ZendIterator> {
if self.is_traversable() {
self.object()?.get_class_entry().get_iterator(self, false)
} else {
None
}
}
/// Returns an iterable over the zval if it is an array or traversable. (is
/// iterable)
pub fn iterable(&self) -> Option<Iterable> {
if self.is_iterable() {
Iterable::from_zval(self)
} else {
None
}
}
/// Returns the value of the zval if it is a pointer. /// Returns the value of the zval if it is a pointer.
/// ///
/// # Safety /// # Safety
@ -371,6 +392,21 @@ impl Zval {
unsafe { zend_is_identical(self_p as *mut Self, other_p as *mut Self) } unsafe { zend_is_identical(self_p as *mut Self, other_p as *mut Self) }
} }
/// Returns true if the zval is traversable, false otherwise.
pub fn is_traversable(&self) -> bool {
match self.object() {
None => false,
Some(obj) => obj.is_traversable(),
}
}
/// Returns true if the zval is iterable (array or traversable), false
/// otherwise.
pub fn is_iterable(&self) -> bool {
let ptr: *const Self = self;
unsafe { zend_is_iterable(ptr as *mut Self) }
}
/// Returns true if the zval contains a pointer, false otherwise. /// Returns true if the zval contains a pointer, false otherwise.
pub fn is_ptr(&self) -> bool { pub fn is_ptr(&self) -> bool {
self.get_type() == DataType::Ptr self.get_type() == DataType::Ptr
@ -626,6 +662,7 @@ impl Debug for Zval {
DataType::Void => field!(Option::<()>::None), DataType::Void => field!(Option::<()>::None),
DataType::Bool => field!(self.bool()), DataType::Bool => field!(self.bool()),
DataType::Indirect => field!(self.indirect()), DataType::Indirect => field!(self.indirect()),
DataType::Iterable => field!(self.iterable()),
// SAFETY: We are not accessing the pointer. // SAFETY: We are not accessing the pointer.
DataType::Ptr => field!(unsafe { self.ptr::<c_void>() }), DataType::Ptr => field!(unsafe { self.ptr::<c_void>() }),
}; };

View File

@ -1,5 +1,6 @@
//! Builder and objects for creating classes in the PHP world. //! Builder and objects for creating classes in the PHP world.
use crate::types::{ZendIterator, Zval};
use crate::{ use crate::{
boxed::ZBox, boxed::ZBox,
ffi::zend_class_entry, ffi::zend_class_entry,
@ -97,6 +98,24 @@ impl ClassEntry {
} }
} }
/// Returns the iterator for the class for a specific instance
///
/// Returns [`None`] if there is no associated iterator for the class.
pub fn get_iterator<'a>(&self, zval: &'a Zval, by_ref: bool) -> Option<&'a mut ZendIterator> {
let ptr: *const Self = self;
let zval_ptr: *const Zval = zval;
let iterator = unsafe {
(*ptr).get_iterator?(
ptr as *mut ClassEntry,
zval_ptr as *mut Zval,
if by_ref { 1 } else { 0 },
)
};
unsafe { iterator.as_mut() }
}
pub fn name(&self) -> Option<&str> { pub fn name(&self) -> Option<&str> {
unsafe { self.name.as_ref().and_then(|s| s.as_str().ok()) } unsafe { self.name.as_ref().and_then(|s| s.as_str().ok()) }
} }

View File

@ -9,6 +9,7 @@ use std::str;
use parking_lot::{const_rwlock, RwLock, RwLockReadGuard, RwLockWriteGuard}; use parking_lot::{const_rwlock, RwLock, RwLockReadGuard, RwLockWriteGuard};
use crate::boxed::ZBox; use crate::boxed::ZBox;
use crate::exception::PhpResult;
#[cfg(php82)] #[cfg(php82)]
use crate::ffi::zend_atomic_bool_store; use crate::ffi::zend_atomic_bool_store;
use crate::ffi::{ use crate::ffi::{
@ -83,9 +84,8 @@ impl ExecutorGlobals {
pub fn ini_values(&self) -> HashMap<String, Option<String>> { pub fn ini_values(&self) -> HashMap<String, Option<String>> {
let hash_table = unsafe { &*self.ini_directives }; let hash_table = unsafe { &*self.ini_directives };
let mut ini_hash_map: HashMap<String, Option<String>> = HashMap::new(); let mut ini_hash_map: HashMap<String, Option<String>> = HashMap::new();
for (_index, key, value) in hash_table.iter() { for (key, value) in hash_table.iter() {
if let Some(key) = key { ini_hash_map.insert(key.to_string(), unsafe {
ini_hash_map.insert(key, unsafe {
let ini_entry = &*value.ptr::<zend_ini_entry>().expect("Invalid ini entry"); let ini_entry = &*value.ptr::<zend_ini_entry>().expect("Invalid ini entry");
if ini_entry.value.is_null() { if ini_entry.value.is_null() {
None None
@ -99,7 +99,6 @@ impl ExecutorGlobals {
} }
}); });
} }
}
ini_hash_map ini_hash_map
} }
@ -115,6 +114,13 @@ impl ExecutorGlobals {
/// could lead to a deadlock if the globals are already borrowed immutably /// could lead to a deadlock if the globals are already borrowed immutably
/// or mutably. /// or mutably.
pub fn take_exception() -> Option<ZBox<ZendObject>> { pub fn take_exception() -> Option<ZBox<ZendObject>> {
{
// This avoid a write lock if there is no exception.
if Self::get().exception.is_null() {
return None;
}
}
let mut globals = Self::get_mut(); let mut globals = Self::get_mut();
let mut exception_ptr = std::ptr::null_mut(); let mut exception_ptr = std::ptr::null_mut();
@ -124,6 +130,25 @@ impl ExecutorGlobals {
Some(unsafe { ZBox::from_raw(exception_ptr.as_mut()?) }) Some(unsafe { ZBox::from_raw(exception_ptr.as_mut()?) })
} }
/// Checks if the executor globals contain an exception.
pub fn has_exception() -> bool {
!Self::get().exception.is_null()
}
/// Attempts to extract the last PHP exception captured by the interpreter.
/// Returned inside a [`PhpResult`].
///
/// This function requires the executor globals to be mutably held, which
/// could lead to a deadlock if the globals are already borrowed immutably
/// or mutably.
pub fn throw_if_exception() -> PhpResult<()> {
if let Some(e) = Self::take_exception() {
Err(crate::error::Error::Exception(e).into())
} else {
Ok(())
}
}
/// Request an interrupt of the PHP VM. This will call the registered /// Request an interrupt of the PHP VM. This will call the registered
/// interrupt handler function. /// interrupt handler function.
/// set with [`crate::ffi::zend_interrupt_function`]. /// set with [`crate::ffi::zend_interrupt_function`].