ext_php_rs/types/
zval.rs

1//! The base value in PHP. A Zval can contain any PHP type, and the type that it
2//! contains is determined by a property inside the struct. The content of the
3//! Zval is stored in a union.
4
5use std::{convert::TryInto, ffi::c_void, fmt::Debug, ptr};
6
7use crate::types::ZendIterator;
8use crate::types::iterable::Iterable;
9use crate::{
10    binary::Pack,
11    binary_slice::PackSlice,
12    boxed::ZBox,
13    convert::{FromZval, FromZvalMut, IntoZval, IntoZvalDyn},
14    error::{Error, Result},
15    ffi::{
16        _zval_struct__bindgen_ty_1, _zval_struct__bindgen_ty_2, ext_php_rs_zend_string_release,
17        zend_array_dup, zend_is_callable, zend_is_identical, zend_is_iterable, zend_resource,
18        zend_value, zval, zval_ptr_dtor,
19    },
20    flags::DataType,
21    flags::ZvalTypeFlags,
22    rc::PhpRc,
23    types::{ZendCallable, ZendHashTable, ZendLong, ZendObject, ZendStr},
24};
25
26/// A zend value. This is the primary storage container used throughout the Zend
27/// engine.
28///
29/// A zval can be thought of as a Rust enum, a type that can contain different
30/// values such as integers, strings, objects etc.
31pub type Zval = zval;
32
33// TODO(david): can we make zval send+sync? main problem is that refcounted
34// types do not have atomic refcounters, so technically two threads could
35// reference the same object and attempt to modify refcounter at the same time.
36// need to look into how ZTS works.
37
38// unsafe impl Send for Zval {}
39// unsafe impl Sync for Zval {}
40
41impl Zval {
42    /// Creates a new, empty zval.
43    #[must_use]
44    pub const fn new() -> Self {
45        Self {
46            value: zend_value {
47                ptr: ptr::null_mut(),
48            },
49            #[allow(clippy::used_underscore_items)]
50            u1: _zval_struct__bindgen_ty_1 {
51                type_info: DataType::Null.as_u32(),
52            },
53            #[allow(clippy::used_underscore_items)]
54            u2: _zval_struct__bindgen_ty_2 { next: 0 },
55        }
56    }
57
58    /// Creates a null zval
59    #[must_use]
60    pub fn null() -> Zval {
61        let mut zval = Zval::new();
62        zval.set_null();
63        zval
64    }
65
66    /// Creates a zval containing an empty array.
67    #[must_use]
68    pub fn new_array() -> Zval {
69        let mut zval = Zval::new();
70        zval.set_hashtable(ZendHashTable::new());
71        zval
72    }
73
74    /// Dereference the zval, if it is a reference.
75    #[must_use]
76    pub fn dereference(&self) -> &Self {
77        self.reference().or_else(|| self.indirect()).unwrap_or(self)
78    }
79
80    /// Dereference the zval mutable, if it is a reference.
81    ///
82    /// # Panics
83    ///
84    /// Panics if a mutable reference to the zval is not possible.
85    pub fn dereference_mut(&mut self) -> &mut Self {
86        // TODO: probably more ZTS work is needed here
87        if self.is_reference() {
88            #[allow(clippy::unwrap_used)]
89            return self.reference_mut().unwrap();
90        }
91        if self.is_indirect() {
92            #[allow(clippy::unwrap_used)]
93            return self.indirect_mut().unwrap();
94        }
95        self
96    }
97
98    /// Returns the value of the zval if it is a long.
99    #[must_use]
100    pub fn long(&self) -> Option<ZendLong> {
101        if self.is_long() {
102            Some(unsafe { self.value.lval })
103        } else {
104            None
105        }
106    }
107
108    /// Returns the value of the zval if it is a bool.
109    #[must_use]
110    pub fn bool(&self) -> Option<bool> {
111        if self.is_true() {
112            Some(true)
113        } else if self.is_false() {
114            Some(false)
115        } else {
116            None
117        }
118    }
119
120    /// Returns the value of the zval if it is a double.
121    #[must_use]
122    pub fn double(&self) -> Option<f64> {
123        if self.is_double() {
124            Some(unsafe { self.value.dval })
125        } else {
126            None
127        }
128    }
129
130    /// Returns the value of the zval as a zend string, if it is a string.
131    ///
132    /// Note that this functions output will not be the same as
133    /// [`string()`](#method.string), as this function does not attempt to
134    /// convert other types into a [`String`].
135    #[must_use]
136    pub fn zend_str(&self) -> Option<&ZendStr> {
137        if self.is_string() {
138            unsafe { self.value.str_.as_ref() }
139        } else {
140            None
141        }
142    }
143
144    /// Returns the value of the zval if it is a string.
145    ///
146    /// [`str()`]: #method.str
147    pub fn string(&self) -> Option<String> {
148        self.str().map(ToString::to_string)
149    }
150
151    /// Returns the value of the zval if it is a string.
152    ///
153    /// Note that this functions output will not be the same as
154    /// [`string()`](#method.string), as this function does not attempt to
155    /// convert other types into a [`String`], as it could not pass back a
156    /// [`&str`] in those cases.
157    #[must_use]
158    pub fn str(&self) -> Option<&str> {
159        self.zend_str().and_then(|zs| zs.as_str().ok())
160    }
161
162    /// Returns the value of the zval if it is a string and can be unpacked into
163    /// a vector of a given type. Similar to the [`unpack`] function in PHP,
164    /// except you can only unpack one type.
165    ///
166    /// # Safety
167    ///
168    /// There is no way to tell if the data stored in the string is actually of
169    /// the given type. The results of this function can also differ from
170    /// platform-to-platform due to the different representation of some
171    /// types on different platforms. Consult the [`pack`] function
172    /// documentation for more details.
173    ///
174    /// [`pack`]: https://www.php.net/manual/en/function.pack.php
175    /// [`unpack`]: https://www.php.net/manual/en/function.unpack.php
176    pub fn binary<T: Pack>(&self) -> Option<Vec<T>> {
177        self.zend_str().map(T::unpack_into)
178    }
179
180    /// Returns the value of the zval if it is a string and can be unpacked into
181    /// a slice of a given type. Similar to the [`unpack`] function in PHP,
182    /// except you can only unpack one type.
183    ///
184    /// This function is similar to [`Zval::binary`] except that a slice is
185    /// returned instead of a vector, meaning the contents of the string is
186    /// not copied.
187    ///
188    /// # Safety
189    ///
190    /// There is no way to tell if the data stored in the string is actually of
191    /// the given type. The results of this function can also differ from
192    /// platform-to-platform due to the different representation of some
193    /// types on different platforms. Consult the [`pack`] function
194    /// documentation for more details.
195    ///
196    /// [`pack`]: https://www.php.net/manual/en/function.pack.php
197    /// [`unpack`]: https://www.php.net/manual/en/function.unpack.php
198    pub fn binary_slice<T: PackSlice>(&self) -> Option<&[T]> {
199        self.zend_str().map(T::unpack_into)
200    }
201
202    /// Returns the value of the zval if it is a resource.
203    #[must_use]
204    pub fn resource(&self) -> Option<*mut zend_resource> {
205        // TODO: Can we improve this function? I haven't done much research into
206        // resources so I don't know if this is the optimal way to return this.
207        if self.is_resource() {
208            Some(unsafe { self.value.res })
209        } else {
210            None
211        }
212    }
213
214    /// Returns an immutable reference to the underlying zval hashtable if the
215    /// zval contains an array.
216    #[must_use]
217    pub fn array(&self) -> Option<&ZendHashTable> {
218        if self.is_array() {
219            unsafe { self.value.arr.as_ref() }
220        } else {
221            None
222        }
223    }
224
225    /// Returns a mutable reference to the underlying zval hashtable if the zval
226    /// contains an array.
227    ///
228    /// # Array Separation
229    ///
230    /// PHP arrays use copy-on-write (COW) semantics. Before returning a mutable
231    /// reference, this method checks if the array is shared (refcount > 1) and
232    /// if so, creates a private copy. This is equivalent to PHP's
233    /// `SEPARATE_ARRAY()` macro and prevents the "Assertion failed:
234    /// `zend_gc_refcount` == 1" error that occurs when modifying shared arrays.
235    pub fn array_mut(&mut self) -> Option<&mut ZendHashTable> {
236        if self.is_array() {
237            unsafe {
238                let arr = self.value.arr;
239                // Check if the array is shared (refcount > 1)
240                // If so, we need to separate it (copy-on-write)
241                if (*arr).gc.refcount > 1 {
242                    // Decrement the refcount of the original array
243                    (*arr).gc.refcount -= 1;
244                    // Duplicate the array to get our own private copy
245                    let new_arr = zend_array_dup(arr);
246                    // Update the zval to point to the new array
247                    self.value.arr = new_arr;
248                }
249                self.value.arr.as_mut()
250            }
251        } else {
252            None
253        }
254    }
255
256    /// Returns the value of the zval if it is an object.
257    #[must_use]
258    pub fn object(&self) -> Option<&ZendObject> {
259        if self.is_object() {
260            unsafe { self.value.obj.as_ref() }
261        } else {
262            None
263        }
264    }
265
266    /// Returns a mutable reference to the object contained in the [`Zval`], if
267    /// any.
268    pub fn object_mut(&mut self) -> Option<&mut ZendObject> {
269        if self.is_object() {
270            unsafe { self.value.obj.as_mut() }
271        } else {
272            None
273        }
274    }
275
276    /// Attempts to call a method on the object contained in the zval.
277    ///
278    /// # Errors
279    ///
280    /// * Returns an error if the [`Zval`] is not an object.
281    // TODO: Measure this
282    #[allow(clippy::inline_always)]
283    #[inline(always)]
284    pub fn try_call_method(&self, name: &str, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
285        self.object()
286            .ok_or(Error::Object)?
287            .try_call_method(name, params)
288    }
289
290    /// Returns the value of the zval if it is an internal indirect reference.
291    #[must_use]
292    pub fn indirect(&self) -> Option<&Zval> {
293        if self.is_indirect() {
294            Some(unsafe { &*(self.value.zv.cast::<Zval>()) })
295        } else {
296            None
297        }
298    }
299
300    /// Returns a mutable reference to the zval if it is an internal indirect
301    /// reference.
302    // TODO: Verify if this is safe to use, as it allows mutating the
303    // hashtable while only having a reference to it. #461
304    #[allow(clippy::mut_from_ref)]
305    #[must_use]
306    pub fn indirect_mut(&self) -> Option<&mut Zval> {
307        if self.is_indirect() {
308            Some(unsafe { &mut *(self.value.zv.cast::<Zval>()) })
309        } else {
310            None
311        }
312    }
313
314    /// Returns the value of the zval if it is a reference.
315    #[must_use]
316    pub fn reference(&self) -> Option<&Zval> {
317        if self.is_reference() {
318            Some(&unsafe { self.value.ref_.as_ref() }?.val)
319        } else {
320            None
321        }
322    }
323
324    /// Returns a mutable reference to the underlying zval if it is a reference.
325    pub fn reference_mut(&mut self) -> Option<&mut Zval> {
326        if self.is_reference() {
327            Some(&mut unsafe { self.value.ref_.as_mut() }?.val)
328        } else {
329            None
330        }
331    }
332
333    /// Returns the value of the zval if it is callable.
334    #[must_use]
335    pub fn callable(&self) -> Option<ZendCallable<'_>> {
336        // The Zval is checked if it is callable in the `new` function.
337        ZendCallable::new(self).ok()
338    }
339
340    /// Returns an iterator over the zval if it is traversable.
341    #[must_use]
342    pub fn traversable(&self) -> Option<&mut ZendIterator> {
343        if self.is_traversable() {
344            self.object()?.get_class_entry().get_iterator(self, false)
345        } else {
346            None
347        }
348    }
349
350    /// Returns an iterable over the zval if it is an array or traversable. (is
351    /// iterable)
352    #[must_use]
353    pub fn iterable(&self) -> Option<Iterable<'_>> {
354        if self.is_iterable() {
355            Iterable::from_zval(self)
356        } else {
357            None
358        }
359    }
360
361    /// Returns the value of the zval if it is a pointer.
362    ///
363    /// # Safety
364    ///
365    /// The caller must ensure that the pointer contained in the zval is in fact
366    /// a pointer to an instance of `T`, as the zval has no way of defining
367    /// the type of pointer.
368    #[must_use]
369    pub unsafe fn ptr<T>(&self) -> Option<*mut T> {
370        if self.is_ptr() {
371            Some(unsafe { self.value.ptr.cast::<T>() })
372        } else {
373            None
374        }
375    }
376
377    /// Attempts to call the zval as a callable with a list of arguments to pass
378    /// to the function. Note that a thrown exception inside the callable is
379    /// not detectable, therefore you should check if the return value is
380    /// valid rather than unwrapping. Returns a result containing the return
381    /// value of the function, or an error.
382    ///
383    /// You should not call this function directly, rather through the
384    /// [`call_user_func`] macro.
385    ///
386    /// # Parameters
387    ///
388    /// * `params` - A list of parameters to call the function with.
389    ///
390    /// # Errors
391    ///
392    /// * Returns an error if the [`Zval`] is not callable.
393    // TODO: Measure this
394    #[allow(clippy::inline_always)]
395    #[inline(always)]
396    pub fn try_call(&self, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
397        self.callable().ok_or(Error::Callable)?.try_call(params)
398    }
399
400    /// Returns the type of the Zval.
401    #[must_use]
402    pub fn get_type(&self) -> DataType {
403        DataType::from(u32::from(unsafe { self.u1.v.type_ }))
404    }
405
406    /// Returns true if the zval is a long, false otherwise.
407    #[must_use]
408    pub fn is_long(&self) -> bool {
409        self.get_type() == DataType::Long
410    }
411
412    /// Returns true if the zval is null, false otherwise.
413    #[must_use]
414    pub fn is_null(&self) -> bool {
415        self.get_type() == DataType::Null
416    }
417
418    /// Returns true if the zval is true, false otherwise.
419    #[must_use]
420    pub fn is_true(&self) -> bool {
421        self.get_type() == DataType::True
422    }
423
424    /// Returns true if the zval is false, false otherwise.
425    #[must_use]
426    pub fn is_false(&self) -> bool {
427        self.get_type() == DataType::False
428    }
429
430    /// Returns true if the zval is a bool, false otherwise.
431    #[must_use]
432    pub fn is_bool(&self) -> bool {
433        self.is_true() || self.is_false()
434    }
435
436    /// Returns true if the zval is a double, false otherwise.
437    #[must_use]
438    pub fn is_double(&self) -> bool {
439        self.get_type() == DataType::Double
440    }
441
442    /// Returns true if the zval is a string, false otherwise.
443    #[must_use]
444    pub fn is_string(&self) -> bool {
445        self.get_type() == DataType::String
446    }
447
448    /// Returns true if the zval is a resource, false otherwise.
449    #[must_use]
450    pub fn is_resource(&self) -> bool {
451        self.get_type() == DataType::Resource
452    }
453
454    /// Returns true if the zval is an array, false otherwise.
455    #[must_use]
456    pub fn is_array(&self) -> bool {
457        self.get_type() == DataType::Array
458    }
459
460    /// Returns true if the zval is an object, false otherwise.
461    #[must_use]
462    pub fn is_object(&self) -> bool {
463        matches!(self.get_type(), DataType::Object(_))
464    }
465
466    /// Returns true if the zval is a reference, false otherwise.
467    #[must_use]
468    pub fn is_reference(&self) -> bool {
469        self.get_type() == DataType::Reference
470    }
471
472    /// Returns true if the zval is a reference, false otherwise.
473    #[must_use]
474    pub fn is_indirect(&self) -> bool {
475        self.get_type() == DataType::Indirect
476    }
477
478    /// Returns true if the zval is callable, false otherwise.
479    #[must_use]
480    pub fn is_callable(&self) -> bool {
481        let ptr: *const Self = self;
482        unsafe { zend_is_callable(ptr.cast_mut(), 0, std::ptr::null_mut()) }
483    }
484
485    /// Checks if the zval is identical to another one.
486    /// This works like `===` in php.
487    ///
488    /// # Parameters
489    ///
490    /// * `other` - The the zval to check identity against.
491    #[must_use]
492    pub fn is_identical(&self, other: &Self) -> bool {
493        let self_p: *const Self = self;
494        let other_p: *const Self = other;
495        unsafe { zend_is_identical(self_p.cast_mut(), other_p.cast_mut()) }
496    }
497
498    /// Returns true if the zval is traversable, false otherwise.
499    #[must_use]
500    pub fn is_traversable(&self) -> bool {
501        match self.object() {
502            None => false,
503            Some(obj) => obj.is_traversable(),
504        }
505    }
506
507    /// Returns true if the zval is iterable (array or traversable), false
508    /// otherwise.
509    #[must_use]
510    pub fn is_iterable(&self) -> bool {
511        let ptr: *const Self = self;
512        unsafe { zend_is_iterable(ptr.cast_mut()) }
513    }
514
515    /// Returns true if the zval contains a pointer, false otherwise.
516    #[must_use]
517    pub fn is_ptr(&self) -> bool {
518        self.get_type() == DataType::Ptr
519    }
520
521    /// Returns true if the zval is a scalar value (integer, float, string, or bool),
522    /// false otherwise.
523    ///
524    /// This is equivalent to PHP's `is_scalar()` function.
525    #[must_use]
526    pub fn is_scalar(&self) -> bool {
527        matches!(
528            self.get_type(),
529            DataType::Long | DataType::Double | DataType::String | DataType::True | DataType::False
530        )
531    }
532
533    /// Sets the value of the zval as a string. Returns nothing in a result when
534    /// successful.
535    ///
536    /// # Parameters
537    ///
538    /// * `val` - The value to set the zval as.
539    /// * `persistent` - Whether the string should persist between requests.
540    ///
541    /// # Persistent Strings
542    ///
543    /// When `persistent` is `true`, the string is allocated from PHP's
544    /// persistent heap (using `malloc`) rather than the request-bound heap.
545    /// This is typically used for strings that need to survive across multiple
546    /// PHP requests, such as class names, function names, or module-level data.
547    ///
548    /// **Important:** The string will still be freed when the Zval is dropped.
549    /// The `persistent` flag only affects which memory allocator is used. If
550    /// you need a string to outlive the Zval, consider using
551    /// [`std::mem::forget`] on the Zval or storing the string elsewhere.
552    ///
553    /// For most use cases (return values, function arguments, temporary
554    /// storage), you should use `persistent: false`.
555    ///
556    /// # Errors
557    ///
558    /// Never returns an error.
559    // TODO: Check if we can drop the result here.
560    pub fn set_string(&mut self, val: &str, persistent: bool) -> Result<()> {
561        self.set_zend_string(ZendStr::new(val, persistent));
562        Ok(())
563    }
564
565    /// Sets the value of the zval as a Zend string.
566    ///
567    /// The Zval takes ownership of the string. When the Zval is dropped,
568    /// the string will be released.
569    ///
570    /// # Parameters
571    ///
572    /// * `val` - String content.
573    pub fn set_zend_string(&mut self, val: ZBox<ZendStr>) {
574        self.change_type(ZvalTypeFlags::StringEx);
575        self.value.str_ = val.into_raw();
576    }
577
578    /// Sets the value of the zval as a binary string, which is represented in
579    /// Rust as a vector.
580    ///
581    /// # Parameters
582    ///
583    /// * `val` - The value to set the zval as.
584    pub fn set_binary<T: Pack>(&mut self, val: Vec<T>) {
585        self.change_type(ZvalTypeFlags::StringEx);
586        let ptr = T::pack_into(val);
587        self.value.str_ = ptr;
588    }
589
590    /// Sets the value of the zval as an interned string. Returns nothing in a
591    /// result when successful.
592    ///
593    /// Interned strings are stored once and are immutable. PHP stores them in
594    /// an internal hashtable. Unlike regular strings, interned strings are not
595    /// reference counted and should not be freed by `zval_ptr_dtor`.
596    ///
597    /// # Parameters
598    ///
599    /// * `val` - The value to set the zval as.
600    /// * `persistent` - Whether the string should persist between requests.
601    ///
602    /// # Errors
603    ///
604    /// Never returns an error.
605    // TODO: Check if we can drop the result here.
606    pub fn set_interned_string(&mut self, val: &str, persistent: bool) -> Result<()> {
607        // Use InternedStringEx (without RefCounted) because interned strings
608        // should not have their refcount modified by zval_ptr_dtor.
609        self.change_type(ZvalTypeFlags::InternedStringEx);
610        self.value.str_ = ZendStr::new_interned(val, persistent).into_raw();
611        Ok(())
612    }
613
614    /// Sets the value of the zval as a long.
615    ///
616    /// # Parameters
617    ///
618    /// * `val` - The value to set the zval as.
619    pub fn set_long<T: Into<ZendLong>>(&mut self, val: T) {
620        self.internal_set_long(val.into());
621    }
622
623    fn internal_set_long(&mut self, val: ZendLong) {
624        self.change_type(ZvalTypeFlags::Long);
625        self.value.lval = val;
626    }
627
628    /// Sets the value of the zval as a double.
629    ///
630    /// # Parameters
631    ///
632    /// * `val` - The value to set the zval as.
633    pub fn set_double<T: Into<f64>>(&mut self, val: T) {
634        self.internal_set_double(val.into());
635    }
636
637    fn internal_set_double(&mut self, val: f64) {
638        self.change_type(ZvalTypeFlags::Double);
639        self.value.dval = val;
640    }
641
642    /// Sets the value of the zval as a boolean.
643    ///
644    /// # Parameters
645    ///
646    /// * `val` - The value to set the zval as.
647    pub fn set_bool<T: Into<bool>>(&mut self, val: T) {
648        self.internal_set_bool(val.into());
649    }
650
651    fn internal_set_bool(&mut self, val: bool) {
652        self.change_type(if val {
653            ZvalTypeFlags::True
654        } else {
655            ZvalTypeFlags::False
656        });
657    }
658
659    /// Sets the value of the zval as null.
660    ///
661    /// This is the default of a zval.
662    pub fn set_null(&mut self) {
663        self.change_type(ZvalTypeFlags::Null);
664    }
665
666    /// Sets the value of the zval as a resource.
667    ///
668    /// # Parameters
669    ///
670    /// * `val` - The value to set the zval as.
671    pub fn set_resource(&mut self, val: *mut zend_resource) {
672        self.change_type(ZvalTypeFlags::ResourceEx);
673        self.value.res = val;
674    }
675
676    /// Sets the value of the zval as a reference to an object.
677    ///
678    /// # Parameters
679    ///
680    /// * `val` - The value to set the zval as.
681    pub fn set_object(&mut self, val: &mut ZendObject) {
682        self.change_type(ZvalTypeFlags::ObjectEx);
683        val.inc_count(); // TODO(david): not sure if this is needed :/
684        self.value.obj = ptr::from_ref(val).cast_mut();
685    }
686
687    /// Sets the value of the zval as an array. Returns nothing in a result on
688    /// success.
689    ///
690    /// # Parameters
691    ///
692    /// * `val` - The value to set the zval as.
693    ///
694    /// # Errors
695    ///
696    /// * Returns an error if the conversion to a hashtable fails.
697    pub fn set_array<T: TryInto<ZBox<ZendHashTable>, Error = Error>>(
698        &mut self,
699        val: T,
700    ) -> Result<()> {
701        self.set_hashtable(val.try_into()?);
702        Ok(())
703    }
704
705    /// Sets the value of the zval as an array. Returns nothing in a result on
706    /// success.
707    ///
708    /// # Parameters
709    ///
710    /// * `val` - The value to set the zval as.
711    pub fn set_hashtable(&mut self, val: ZBox<ZendHashTable>) {
712        // Handle immutable shared arrays (e.g., the empty array) similar to
713        // ZVAL_EMPTY_ARRAY. Immutable arrays should not be reference counted.
714        let type_info = if val.is_immutable() {
715            ZvalTypeFlags::Array
716        } else {
717            ZvalTypeFlags::ArrayEx
718        };
719        self.change_type(type_info);
720        self.value.arr = val.into_raw();
721    }
722
723    /// Sets the value of the zval as a pointer.
724    ///
725    /// # Parameters
726    ///
727    /// * `ptr` - The pointer to set the zval as.
728    pub fn set_ptr<T>(&mut self, ptr: *mut T) {
729        self.u1.type_info = ZvalTypeFlags::Ptr.bits();
730        self.value.ptr = ptr.cast::<c_void>();
731    }
732
733    /// Used to drop the Zval but keep the value of the zval intact.
734    ///
735    /// This is important when copying the value of the zval, as the actual
736    /// value will not be copied, but the pointer to the value (string for
737    /// example) will be copied.
738    pub(crate) fn release(mut self) {
739        // NOTE(david): don't use `change_type` here as we are wanting to keep the
740        // contents intact.
741        self.u1.type_info = ZvalTypeFlags::Null.bits();
742    }
743
744    /// Changes the type of the zval, freeing the current contents when
745    /// applicable.
746    ///
747    /// # Parameters
748    ///
749    /// * `ty` - The new type of the zval.
750    fn change_type(&mut self, ty: ZvalTypeFlags) {
751        // SAFETY: we have exclusive mutable access to this zval so can free the
752        // contents.
753        //
754        // For strings, we use zend_string_release directly instead of zval_ptr_dtor
755        // to correctly handle persistent strings. zend_string_release properly checks
756        // the IS_STR_PERSISTENT flag and uses the correct deallocator (free vs efree).
757        // This fixes heap corruption issues when dropping Zvals containing persistent
758        // strings (see issue #424).
759        if self.is_string() {
760            unsafe {
761                if let Some(str_ptr) = self.value.str_.as_mut() {
762                    ext_php_rs_zend_string_release(str_ptr);
763                }
764            }
765        } else {
766            unsafe { zval_ptr_dtor(self) };
767        }
768        self.u1.type_info = ty.bits();
769    }
770
771    /// Extracts some type from a `Zval`.
772    ///
773    /// This is a wrapper function around `TryFrom`.
774    #[must_use]
775    pub fn extract<'a, T>(&'a self) -> Option<T>
776    where
777        T: FromZval<'a>,
778    {
779        FromZval::from_zval(self)
780    }
781
782    /// Creates a shallow clone of the [`Zval`].
783    ///
784    /// This copies the contents of the [`Zval`], and increments the reference
785    /// counter of the underlying value (if it is reference counted).
786    ///
787    /// For example, if the zval contains a long, it will simply copy the value.
788    /// However, if the zval contains an object, the new zval will point to the
789    /// same object, and the objects reference counter will be incremented.
790    ///
791    /// # Returns
792    ///
793    /// The cloned zval.
794    #[must_use]
795    pub fn shallow_clone(&self) -> Zval {
796        let mut new = Zval::new();
797        new.u1 = self.u1;
798        new.value = self.value;
799
800        // SAFETY: `u1` union is only used for easier bitmasking. It is valid to read
801        // from either of the variants.
802        //
803        // SAFETY: If the value if refcounted (`self.u1.type_info & Z_TYPE_FLAGS_MASK`)
804        // then it is valid to dereference `self.value.counted`.
805        unsafe {
806            let flags = ZvalTypeFlags::from_bits_retain(self.u1.type_info);
807            if flags.contains(ZvalTypeFlags::RefCounted) {
808                (*self.value.counted).gc.refcount += 1;
809            }
810        }
811
812        new
813    }
814}
815
816impl Debug for Zval {
817    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
818        let mut dbg = f.debug_struct("Zval");
819        let ty = self.get_type();
820        dbg.field("type", &ty);
821
822        macro_rules! field {
823            ($value: expr) => {
824                dbg.field("val", &$value)
825            };
826        }
827
828        match ty {
829            DataType::Undef | DataType::Null | DataType::ConstantExpression | DataType::Void => {
830                field!(Option::<()>::None)
831            }
832            DataType::False => field!(false),
833            DataType::True => field!(true),
834            DataType::Long => field!(self.long()),
835            DataType::Double => field!(self.double()),
836            DataType::String | DataType::Mixed | DataType::Callable => field!(self.string()),
837            DataType::Array => field!(self.array()),
838            DataType::Object(_) => field!(self.object()),
839            DataType::Resource => field!(self.resource()),
840            DataType::Reference => field!(self.reference()),
841            DataType::Bool => field!(self.bool()),
842            DataType::Indirect => field!(self.indirect()),
843            DataType::Iterable => field!(self.iterable()),
844            // SAFETY: We are not accessing the pointer.
845            DataType::Ptr => field!(unsafe { self.ptr::<c_void>() }),
846        };
847
848        dbg.finish()
849    }
850}
851
852impl Drop for Zval {
853    fn drop(&mut self) {
854        self.change_type(ZvalTypeFlags::Null);
855    }
856}
857
858impl Default for Zval {
859    fn default() -> Self {
860        Self::new()
861    }
862}
863
864impl IntoZval for Zval {
865    const TYPE: DataType = DataType::Mixed;
866    const NULLABLE: bool = true;
867
868    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
869        *zv = self;
870        Ok(())
871    }
872}
873
874impl<'a> FromZval<'a> for &'a Zval {
875    const TYPE: DataType = DataType::Mixed;
876
877    fn from_zval(zval: &'a Zval) -> Option<Self> {
878        Some(zval)
879    }
880}
881
882impl<'a> FromZvalMut<'a> for &'a mut Zval {
883    const TYPE: DataType = DataType::Mixed;
884
885    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
886        Some(zval)
887    }
888}
889
890#[cfg(test)]
891#[cfg(feature = "embed")]
892mod tests {
893    use super::*;
894    use crate::embed::Embed;
895
896    #[test]
897    fn test_zval_null() {
898        Embed::run(|| {
899            let zval = Zval::null();
900            assert!(zval.is_null());
901        });
902    }
903
904    #[test]
905    fn test_is_scalar() {
906        Embed::run(|| {
907            // Test scalar types - should return true
908            let mut zval_long = Zval::new();
909            zval_long.set_long(42);
910            assert!(zval_long.is_scalar());
911
912            let mut zval_double = Zval::new();
913            zval_double.set_double(1.5);
914            assert!(zval_double.is_scalar());
915
916            let mut zval_true = Zval::new();
917            zval_true.set_bool(true);
918            assert!(zval_true.is_scalar());
919
920            let mut zval_false = Zval::new();
921            zval_false.set_bool(false);
922            assert!(zval_false.is_scalar());
923
924            let mut zval_string = Zval::new();
925            zval_string
926                .set_string("hello", false)
927                .expect("set_string should succeed");
928            assert!(zval_string.is_scalar());
929
930            // Test non-scalar types - should return false
931            let zval_null = Zval::null();
932            assert!(!zval_null.is_scalar());
933
934            let zval_array = Zval::new_array();
935            assert!(!zval_array.is_scalar());
936        });
937    }
938}