diff --git a/allowed_bindings.rs b/allowed_bindings.rs index 7e16a51..9d09fb0 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -94,6 +94,7 @@ bind! { zend_internal_arg_info, zend_is_callable, zend_is_identical, + zend_is_iterable, zend_long, zend_lookup_class_ex, zend_module_entry, @@ -163,6 +164,7 @@ bind! { IS_UNDEF, IS_VOID, IS_PTR, + IS_ITERABLE, MAY_BE_ANY, MAY_BE_BOOL, PHP_INI_USER, diff --git a/docsrs_bindings.rs b/docsrs_bindings.rs index 606ec0a..4a9ee7c 100644 --- a/docsrs_bindings.rs +++ b/docsrs_bindings.rs @@ -97,6 +97,7 @@ pub const IS_RESOURCE: u32 = 9; pub const IS_REFERENCE: u32 = 10; pub const IS_CONSTANT_AST: u32 = 11; pub const IS_CALLABLE: u32 = 12; +pub const IS_ITERABLE: u32 = 13; pub const IS_VOID: u32 = 14; pub const IS_MIXED: u32 = 16; pub const IS_INDIRECT: u32 = 12; @@ -1658,6 +1659,9 @@ extern "C" { 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_OR_NULL: _zend_expected_type = 1; pub const _zend_expected_type_Z_EXPECTED_BOOL: _zend_expected_type = 2; diff --git a/guide/src/types/iterable.md b/guide/src/types/iterable.md new file mode 100644 index 0000000..fec8072 --- /dev/null +++ b/guide/src/types/iterable.md @@ -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 + '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 +``` diff --git a/guide/src/types/iterator.md b/guide/src/types/iterator.md new file mode 100644 index 0000000..4c53e67 --- /dev/null +++ b/guide/src/types/iterator.md @@ -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 + 'world'; + yield 'rust' => 'php'; +}; + +test_iterator($generator()); +``` + +Output: + +```text +k: hello v: world +k: rust v: php +``` diff --git a/src/describe/stub.rs b/src/describe/stub.rs index 065b275..17cfbfd 100644 --- a/src/describe/stub.rs +++ b/src/describe/stub.rs @@ -172,6 +172,7 @@ impl ToStub for DataType { DataType::Reference => "reference", DataType::Callable => "callable", DataType::Bool => "bool", + DataType::Iterable => "iterable", _ => "mixed", } ) diff --git a/src/flags.rs b/src/flags.rs index 5036c7e..da3a684 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -8,10 +8,10 @@ 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_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, + 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, @@ -49,6 +49,7 @@ bitflags! { const ConstantExpression = IS_CONSTANT_AST; const Void = IS_VOID; const Ptr = IS_PTR; + const Iterable = IS_ITERABLE; const InternedStringEx = Self::String.bits(); const StringEx = Self::String.bits() | Self::RefCounted.bits(); @@ -237,6 +238,7 @@ pub enum DataType { Double, String, Array, + Iterable, Object(Option<&'static str>), Resource, Reference, @@ -277,6 +279,7 @@ impl DataType { DataType::Mixed => IS_MIXED, DataType::Bool => _IS_BOOL, DataType::Ptr => IS_PTR, + DataType::Iterable => IS_ITERABLE, } } } @@ -383,6 +386,7 @@ impl Display for DataType { DataType::Mixed => write!(f, "Mixed"), DataType::Ptr => write!(f, "Pointer"), DataType::Indirect => write!(f, "Indirect"), + DataType::Iterable => write!(f, "Iterable"), } } } diff --git a/src/types/array.rs b/src/types/array.rs index c40b8f3..19768f3 100644 --- a/src/types/array.rs +++ b/src/types/array.rs @@ -5,7 +5,7 @@ use std::{ collections::HashMap, convert::{TryFrom, TryInto}, ffi::CString, - fmt::Debug, + fmt::{Debug, Display}, iter::FromIterator, u64, }; @@ -463,7 +463,7 @@ impl ZendHashTable { /// assert!(!ht.has_numerical_keys()); /// ``` 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. @@ -490,31 +490,9 @@ impl ZendHashTable { /// ``` pub fn has_sequential_keys(&self) -> bool { !self - .iter() + .into_iter() .enumerate() - .any(|(i, (k, strk, _))| i as u64 != k || strk.is_some()) - } - - /// 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) + .any(|(i, (k, _))| ArrayKey::Long(i as i64) != k) } /// Returns an iterator over the values contained inside the hashtable, as @@ -534,6 +512,28 @@ impl ZendHashTable { pub fn values(&self) -> Values { 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 { @@ -546,10 +546,7 @@ unsafe impl ZBoxable for ZendHashTable { impl Debug for ZendHashTable { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_map() - .entries( - self.iter() - .map(|(k, k2, v)| (k2.unwrap_or_else(|| k.to_string()), v)), - ) + .entries(self.into_iter().map(|(k, v)| (k.to_string(), v))) .finish() } } @@ -574,10 +571,54 @@ impl ToOwned for ZendHashTable { /// Immutable iterator upon a reference to a hashtable. pub struct Iter<'a> { ht: &'a ZendHashTable, - current_num: u64, + current_num: i64, 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 { + 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> { /// 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> { - type Item = (u64, Option, &'a Zval); + type Item = (ArrayKey, &'a Zval); fn next(&mut self) -> Option { - 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 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, &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) + self.next_zval() + .map(|(k, v)| (ArrayKey::from_zval(&k).expect("Invalid array key!"), v)) } fn count(self) -> usize @@ -666,6 +697,7 @@ impl<'a> DoubleEndedIterator for Iter<'a> { } let key = Zval::new(); + unsafe { zend_hash_get_current_key_zval_ex( self.ht as *const ZendHashTable as *mut ZendHashTable, @@ -679,9 +711,10 @@ impl<'a> DoubleEndedIterator for Iter<'a> { &mut self.pos as *mut HashPosition, ) }; - let r: (u64, Option, &Zval) = match key.is_long() { - true => (key.long().unwrap_or(0) as u64, None, value), - false => (self.current_num, key.try_into().ok(), value), + + let key = match ArrayKey::from_zval(&key) { + Some(key) => key, + None => ArrayKey::Long(self.current_num), }; unsafe { @@ -692,7 +725,52 @@ impl<'a> DoubleEndedIterator for Iter<'a> { }; 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; fn next(&mut self) -> Option { - self.0.next().map(|(_, _, zval)| zval) + self.0.next().map(|(_, zval)| zval) } fn count(self) -> usize @@ -734,7 +812,7 @@ impl<'a> ExactSizeIterator for Values<'a> { impl<'a> DoubleEndedIterator for Values<'a> { fn next_back(&mut self) -> Option { - 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 { let mut hm = HashMap::with_capacity(value.len()); - for (idx, key, val) in value.iter() { + for (key, val) in value { hm.insert( - key.unwrap_or_else(|| idx.to_string()), + key.to_string(), V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?, ); } @@ -849,7 +927,7 @@ where fn try_from(value: &'a ZendHashTable) -> Result { 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()))?); } diff --git a/src/types/iterable.rs b/src/types/iterable.rs new file mode 100644 index 0000000..1440a67 --- /dev/null +++ b/src/types/iterable.rs @@ -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 { + 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 { + 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 { + match self { + Iter::Array(array) => array.next_zval(), + Iter::Traversable(traversable) => traversable.next(), + } + } +} diff --git a/src/types/iterator.rs b/src/types/iterator.rs new file mode 100644 index 0000000..b6591ab --- /dev/null +++ b/src/types/iterator.rs @@ -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 { + 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 + 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 + /// + /// # 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 + /// + /// # 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 { + 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 { + // 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 { + 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()); + } + }); + } +} diff --git a/src/types/iterator.test.php b/src/types/iterator.test.php new file mode 100644 index 0000000..891f0a9 --- /dev/null +++ b/src/types/iterator.test.php @@ -0,0 +1,52 @@ + 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(); diff --git a/src/types/mod.rs b/src/types/mod.rs index 888e056..846c8d1 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -6,6 +6,8 @@ mod array; mod callable; mod class_object; +mod iterable; +mod iterator; mod long; mod object; mod string; @@ -14,6 +16,8 @@ mod zval; pub use array::ZendHashTable; pub use callable::ZendCallable; pub use class_object::ZendClassObject; +pub use iterable::Iterable; +pub use iterator::ZendIterator; pub use long::ZendLong; pub use object::{PropertyQuery, ZendObject}; pub use string::ZendStr; diff --git a/src/types/object.rs b/src/types/object.rs index f17b65f..68ad535 100644 --- a/src/types/object.rs +++ b/src/types/object.rs @@ -133,6 +133,15 @@ impl ZendObject { (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)] pub fn try_call_method(&self, name: &str, params: Vec<&dyn IntoZvalDyn>) -> Result { let mut retval = Zval::new(); @@ -317,8 +326,8 @@ impl Debug for ZendObject { ); if let Ok(props) = self.get_properties() { - for (id, key, val) in props.iter() { - dbg.field(key.unwrap_or_else(|| id.to_string()).as_str(), val); + for (key, val) in props.iter() { + dbg.field(key.to_string().as_str(), val); } } diff --git a/src/types/zval.rs b/src/types/zval.rs index b0ceec7..f031214 100644 --- a/src/types/zval.rs +++ b/src/types/zval.rs @@ -4,6 +4,8 @@ use std::{convert::TryInto, ffi::c_void, fmt::Debug, ptr}; +use crate::types::iterable::Iterable; +use crate::types::ZendIterator; use crate::{ binary::Pack, binary_slice::PackSlice, @@ -12,7 +14,7 @@ use crate::{ error::{Error, Result}, ffi::{ _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::ZvalTypeFlags, @@ -256,6 +258,25 @@ impl Zval { 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 { + if self.is_iterable() { + Iterable::from_zval(self) + } else { + None + } + } + /// Returns the value of the zval if it is a pointer. /// /// # Safety @@ -371,6 +392,21 @@ impl Zval { 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. pub fn is_ptr(&self) -> bool { self.get_type() == DataType::Ptr @@ -626,6 +662,7 @@ impl Debug for Zval { DataType::Void => field!(Option::<()>::None), DataType::Bool => field!(self.bool()), DataType::Indirect => field!(self.indirect()), + DataType::Iterable => field!(self.iterable()), // SAFETY: We are not accessing the pointer. DataType::Ptr => field!(unsafe { self.ptr::() }), }; diff --git a/src/zend/class.rs b/src/zend/class.rs index 4c3c23d..d9765a3 100644 --- a/src/zend/class.rs +++ b/src/zend/class.rs @@ -1,5 +1,6 @@ //! Builder and objects for creating classes in the PHP world. +use crate::types::{ZendIterator, Zval}; use crate::{ boxed::ZBox, 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> { unsafe { self.name.as_ref().and_then(|s| s.as_str().ok()) } } diff --git a/src/zend/globals.rs b/src/zend/globals.rs index 046195f..2e3789e 100644 --- a/src/zend/globals.rs +++ b/src/zend/globals.rs @@ -9,6 +9,7 @@ use std::str; use parking_lot::{const_rwlock, RwLock, RwLockReadGuard, RwLockWriteGuard}; use crate::boxed::ZBox; +use crate::exception::PhpResult; #[cfg(php82)] use crate::ffi::zend_atomic_bool_store; use crate::ffi::{ @@ -83,22 +84,20 @@ impl ExecutorGlobals { pub fn ini_values(&self) -> HashMap> { let hash_table = unsafe { &*self.ini_directives }; let mut ini_hash_map: HashMap> = HashMap::new(); - for (_index, key, value) in hash_table.iter() { - if let Some(key) = key { - ini_hash_map.insert(key, unsafe { - let ini_entry = &*value.ptr::().expect("Invalid ini entry"); - if ini_entry.value.is_null() { - None - } else { - Some( - (*ini_entry.value) - .as_str() - .expect("Ini value is not a string") - .to_owned(), - ) - } - }); - } + for (key, value) in hash_table.iter() { + ini_hash_map.insert(key.to_string(), unsafe { + let ini_entry = &*value.ptr::().expect("Invalid ini entry"); + if ini_entry.value.is_null() { + None + } else { + Some( + (*ini_entry.value) + .as_str() + .expect("Ini value is not a string") + .to_owned(), + ) + } + }); } ini_hash_map } @@ -115,6 +114,13 @@ impl ExecutorGlobals { /// could lead to a deadlock if the globals are already borrowed immutably /// or mutably. pub fn take_exception() -> Option> { + { + // 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 exception_ptr = std::ptr::null_mut(); @@ -124,6 +130,25 @@ impl ExecutorGlobals { 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 /// interrupt handler function. /// set with [`crate::ffi::zend_interrupt_function`].