• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

extphprs / ext-php-rs / 30648853137

31 Jul 2026 04:51PM UTC coverage: 66.524% (+0.01%) from 66.511%
30648853137

push

github

web-flow
fix(types)!: stop deriving mutable references from shared borrows (#757)

14 of 48 new or added lines in 9 files covered. (29.17%)

10 existing lines in 4 files now uncovered.

8861 of 13320 relevant lines covered (66.52%)

33.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/types/class_object.rs
1
//! Represents an object in PHP. Allows for overriding the internal object used
2
//! by classes, allowing users to store Rust data inside a PHP object.
3

4
use std::{
5
    fmt::Debug,
6
    mem,
7
    ops::{Deref, DerefMut},
8
    os::raw::c_char,
9
    ptr::{self, NonNull},
10
};
11

12
use crate::{
13
    boxed::{ZBox, ZBoxable},
14
    class::RegisteredClass,
15
    convert::{FromZendObject, FromZendObjectMut, FromZval, FromZvalMut, IntoZval},
16
    error::{Error, Result},
17
    ffi::{
18
        ext_php_rs_zend_object_alloc, ext_php_rs_zend_object_release, object_properties_init,
19
        zend_object, zend_object_std_init, zend_objects_clone_members,
20
    },
21
    flags::DataType,
22
    rc::PhpRc,
23
    types::{ZendObject, Zval},
24
    zend::ClassEntry,
25
};
26

27
/// Representation of a Zend class object in memory.
28
#[repr(C)]
29
#[derive(Debug)]
30
pub struct ZendClassObject<T> {
31
    /// The object stored inside the class object.
32
    pub obj: Option<T>,
33
    /// The standard zend object.
34
    pub std: ZendObject,
35
}
36

37
impl<T: RegisteredClass> ZendClassObject<T> {
38
    /// Creates a new [`ZendClassObject`] of type `T`, where `T` is a
39
    /// [`RegisteredClass`] in PHP, storing the given value `val` inside the
40
    /// object.
41
    ///
42
    /// # Parameters
43
    ///
44
    /// * `val` - The value to store inside the object.
45
    ///
46
    /// # Panics
47
    ///
48
    /// Panics if memory was unable to be allocated for the new object.
49
    pub fn new(val: T) -> ZBox<Self> {
×
50
        // SAFETY: We are providing a value to initialize the object with.
51
        unsafe { Self::internal_new(Some(val), None) }
×
52
    }
×
53

54
    /// Creates a new [`ZendClassObject`] of type `T`, with an uninitialized
55
    /// internal object.
56
    ///
57
    /// # Safety
58
    ///
59
    /// As the object is uninitialized, the caller must ensure the following
60
    /// until the internal object is initialized:
61
    ///
62
    /// * The object is never dereferenced to `T`.
63
    /// * The [`Clone`] implementation is never called.
64
    /// * The [`Debug`] implementation is never called.
65
    ///
66
    /// If any of these conditions are not met while not initialized, the
67
    /// corresponding function will panic. Converting the object into its
68
    /// inner pointer with the [`into_raw`] function is valid, however.
69
    ///
70
    /// [`into_raw`]: #method.into_raw
71
    ///
72
    /// # Panics
73
    ///
74
    /// Panics if memory was unable to be allocated for the new object.
75
    pub unsafe fn new_uninit(ce: Option<&'static ClassEntry>) -> ZBox<Self> {
×
76
        unsafe { Self::internal_new(None, ce) }
×
77
    }
×
78

79
    /// Creates a new [`ZendObject`] of type `T`, storing the given (and
80
    /// potentially uninitialized) `val` inside the object.
81
    ///
82
    /// # Parameters
83
    ///
84
    /// * `val` - Value to store inside the object. See safety section.
85
    /// * `init` - Whether the given `val` was initialized.
86
    ///
87
    /// # Safety
88
    ///
89
    /// Providing an initialized variant of [`MaybeUninit<T>`] is safe.
90
    ///
91
    /// Providing an uninitialized variant of [`MaybeUninit<T>`] is unsafe. As
92
    /// the object is uninitialized, the caller must ensure the following
93
    /// until the internal object is initialized:
94
    ///
95
    /// * The object is never dereferenced to `T`.
96
    /// * The [`Clone`] implementation is never called.
97
    /// * The [`Debug`] implementation is never called.
98
    ///
99
    /// If any of these conditions are not met while not initialized, the
100
    /// corresponding function will panic. Converting the object into its
101
    /// inner with the [`into_raw`] function is valid, however. You can
102
    /// initialize the object with the [`initialize`] function.
103
    ///
104
    /// [`into_raw`]: #method.into_raw
105
    /// [`initialize`]: #method.initialize
106
    ///
107
    /// # Panics
108
    ///
109
    /// Panics if memory was unable to be allocated for the new object.
110
    unsafe fn internal_new(val: Option<T>, ce: Option<&'static ClassEntry>) -> ZBox<Self> {
×
111
        let size = mem::size_of::<ZendClassObject<T>>();
×
112
        let meta = T::get_metadata();
×
113
        let ce = ptr::from_ref(ce.unwrap_or_else(|| meta.ce())).cast_mut();
×
114
        let obj =
×
115
            unsafe { ext_php_rs_zend_object_alloc(size as _, ce).cast::<ZendClassObject<T>>() };
×
116
        let obj = unsafe {
×
117
            obj.as_mut()
×
118
                .expect("Failed to allocate for new Zend object")
×
119
        };
120

121
        unsafe { zend_object_std_init(&raw mut obj.std, ce) };
×
122
        unsafe { object_properties_init(&raw mut obj.std, ce) };
×
123

124
        // SAFETY: `obj` is non-null and well aligned as it is a reference.
125
        // As the data in `obj.obj` is uninitialized, we don't want to drop
126
        // the data, but directly override it.
127
        unsafe { ptr::write(&raw mut obj.obj, val) };
×
128

129
        obj.std.handlers = meta.handlers();
×
130
        unsafe { ZBox::from_raw(obj) }
×
131
    }
×
132

133
    /// Initializes the class object with the value `val`.
134
    ///
135
    /// # Parameters
136
    ///
137
    /// * `val` - The value to initialize the object with.
138
    ///
139
    /// # Returns
140
    ///
141
    /// Returns the old value in an [`Option`] if the object had already been
142
    /// initialized, [`None`] otherwise.
143
    pub fn initialize(&mut self, val: T) -> Option<T> {
×
144
        self.obj.replace(val)
×
145
    }
×
146

147
    /// Returns a reference to the [`ZendClassObject`] of a given zend
148
    /// object `obj`. Returns [`None`] if the given object is not of the
149
    /// type `T`.
150
    ///
151
    /// # Parameters
152
    ///
153
    /// * `obj` - The zend object to get the [`ZendClassObject`] for.
154
    ///
155
    /// # Panics
156
    ///
157
    /// * If the std offset over/underflows `isize`.
158
    #[must_use]
159
    pub fn from_zend_obj(std: &zend_object) -> Option<&Self> {
×
160
        // SAFETY: `std` is a live `zend_object` for the lifetime of the borrow,
161
        // and the resolved pointer is only ever read through a shared reference.
NEW
162
        unsafe { Self::resolve(ptr::from_ref(std), true)?.as_ref() }
×
UNCOV
163
    }
×
164

165
    /// Returns a mutable reference to the [`ZendClassObject`] of a given zend
166
    /// object `obj`. Returns [`None`] if the given object is not of the
167
    /// type `T`.
168
    ///
169
    /// # Parameters
170
    ///
171
    /// * `obj` - The zend object to get the [`ZendClassObject`] for.
172
    ///
173
    /// # Panics
174
    ///
175
    /// * If the std offset over/underflows `isize`.
UNCOV
176
    pub fn from_zend_obj_mut(std: &mut zend_object) -> Option<&mut Self> {
×
177
        // SAFETY: the pointer is derived from a `&mut zend_object`, so recovering
178
        // write permission with `cast_mut` is sound and the returned reference is
179
        // the only live mutable borrow of the containing class object.
NEW
180
        unsafe { Self::resolve(ptr::from_mut(std), true)?.cast_mut().as_mut() }
×
UNCOV
181
    }
×
182

183
    /// Returns a mutable reference to the [`ZendClassObject`] of a given zend
184
    /// object `obj`, even if the Rust object is not yet initialized.
185
    ///
186
    /// This is used internally by the constructor to get access to the object
187
    /// before calling `initialize()`.
188
    ///
189
    /// # Safety
190
    ///
191
    /// The caller must ensure that the returned object is not dereferenced
192
    /// to `T` until after `initialize()` is called. Only `initialize()` should
193
    /// be called on the returned object.
194
    ///
195
    /// # Parameters
196
    ///
197
    /// * `obj` - The zend object to get the [`ZendClassObject`] for.
198
    ///
199
    /// # Panics
200
    ///
201
    /// * If the std offset over/underflows `isize`.
UNCOV
202
    pub(crate) fn from_zend_obj_mut_uninit(std: &mut zend_object) -> Option<&mut Self> {
×
203
        // SAFETY: the pointer is derived from a `&mut zend_object`, so recovering
204
        // write permission with `cast_mut` is sound and the returned reference is
205
        // the only live mutable borrow of the containing class object.
206
        unsafe {
NEW
207
            Self::resolve(ptr::from_mut(std), false)?
×
NEW
208
                .cast_mut()
×
NEW
209
                .as_mut()
×
210
        }
UNCOV
211
    }
×
212

213
    /// Resolves the [`ZendClassObject`] containing `std`, validating that the
214
    /// object was created by this type's `create_object` handler.
215
    ///
216
    /// Returns a `*const` so the caller decides whether to form a shared or a
217
    /// mutable reference. Callers that need `&mut Self` must pass a pointer
218
    /// derived from a `&mut zend_object` and recover write permission with
219
    /// [`pointer::cast_mut`], so that the mutable access is never derived from a
220
    /// shared reborrow.
221
    ///
222
    /// # Safety
223
    ///
224
    /// `std` must be non-null and point to a live `zend_object` for the duration
225
    /// of the call.
226
    ///
227
    /// # Panics
228
    ///
229
    /// * If the std offset over/underflows `isize`.
NEW
230
    unsafe fn resolve(std: *const zend_object, require_initialized: bool) -> Option<*const Self> {
×
231
        // First, check if this object was created by our create_object handler.
232
        // We do this by comparing the handlers pointer. Objects created by PHP's
233
        // mock frameworks or subclasses won't have our custom handlers, and their
234
        // memory layout won't match ZendClassObject<T>.
235
        let expected_handlers = T::get_metadata().handlers();
×
NEW
236
        if !ptr::eq(unsafe { (*std).handlers }, expected_handlers) {
×
237
            return None;
×
238
        }
×
239

NEW
240
        let this = unsafe {
×
241
            let offset = isize::try_from(Self::std_offset()).expect("Offset overflow");
×
NEW
242
            std.cast::<c_char>().offset(0 - offset).cast::<Self>()
×
243
        };
NEW
244
        let this_ref = unsafe { this.as_ref()? };
×
245

NEW
246
        if !this_ref.std.instance_of(T::get_metadata().ce()) {
×
247
            return None;
×
248
        }
×
249

250
        // Check if the Rust object is initialized. Objects created via create_object
251
        // for subclasses that don't call parent::__construct() will have obj = None.
252
        // We must reject these to avoid panics when dereferencing.
NEW
253
        if require_initialized && this_ref.obj.is_none() {
×
254
            return None;
×
255
        }
×
256

NEW
257
        Some(this)
×
258
    }
×
259

260
    /// Returns a mutable reference to the underlying Zend object.
261
    pub fn get_mut_zend_obj(&mut self) -> &mut zend_object {
×
262
        &mut self.std
×
263
    }
×
264

265
    /// Returns the offset of the `std` property in the class object.
266
    pub(crate) fn std_offset() -> usize {
×
267
        unsafe {
268
            let null = NonNull::<Self>::dangling();
×
269
            let base = ptr::from_ref::<Self>(null.as_ref());
×
270
            let std = &raw const null.as_ref().std;
×
271

272
            (std as usize) - (base as usize)
×
273
        }
274
    }
×
275
}
276

277
impl<'a, T: RegisteredClass> FromZval<'a> for &'a ZendClassObject<T> {
278
    const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME));
279

280
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
281
        Self::from_zend_object(zval.object()?).ok()
×
282
    }
×
283
}
284

285
impl<'a, T: RegisteredClass> FromZendObject<'a> for &'a ZendClassObject<T> {
286
    fn from_zend_object(obj: &'a ZendObject) -> Result<Self> {
×
287
        // TODO(david): replace with better error
288
        ZendClassObject::from_zend_obj(obj).ok_or(Error::InvalidScope)
×
289
    }
×
290
}
291

292
impl<'a, T: RegisteredClass> FromZvalMut<'a> for &'a mut ZendClassObject<T> {
293
    const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME));
294

295
    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
×
296
        Self::from_zend_object_mut(zval.object_mut()?).ok()
×
297
    }
×
298
}
299

300
impl<'a, T: RegisteredClass> FromZendObjectMut<'a> for &'a mut ZendClassObject<T> {
301
    fn from_zend_object_mut(obj: &'a mut ZendObject) -> Result<Self> {
×
302
        ZendClassObject::from_zend_obj_mut(obj).ok_or(Error::InvalidScope)
×
303
    }
×
304
}
305

306
unsafe impl<T: RegisteredClass> ZBoxable for ZendClassObject<T> {
307
    fn free(&mut self) {
×
308
        // SAFETY: All constructors guarantee that `self` contains a valid pointer.
309
        // Further, all constructors guarantee that the `std` field of
310
        // `ZendClassObject` will be initialized.
311
        unsafe { ext_php_rs_zend_object_release(&raw mut self.std) }
×
312
    }
×
313
}
314

315
impl<T> Deref for ZendClassObject<T> {
316
    type Target = T;
317

318
    fn deref(&self) -> &Self::Target {
×
319
        self.obj
×
320
            .as_ref()
×
321
            .expect("Attempted to access uninitialized class object")
×
322
    }
×
323
}
324

325
impl<T> DerefMut for ZendClassObject<T> {
326
    fn deref_mut(&mut self) -> &mut Self::Target {
×
327
        self.obj
×
328
            .as_mut()
×
329
            .expect("Attempted to access uninitialized class object")
×
330
    }
×
331
}
332

333
impl<T: RegisteredClass + Default> Default for ZBox<ZendClassObject<T>> {
334
    #[inline]
335
    fn default() -> Self {
×
336
        ZendClassObject::new(T::default())
×
337
    }
×
338
}
339

340
impl<T: RegisteredClass + Clone> Clone for ZBox<ZendClassObject<T>> {
341
    fn clone(&self) -> Self {
×
342
        // SAFETY: All constructors of `NewClassObject` guarantee that it will contain a
343
        // valid pointer. The constructor also guarantees that the internal
344
        // `ZendClassObject` pointer will contain a valid, initialized `obj`,
345
        // therefore we can dereference both safely.
346
        unsafe {
347
            let mut new = ZendClassObject::new((***self).clone());
×
348
            zend_objects_clone_members(&raw mut new.std, (&raw const self.std).cast_mut());
×
349
            new
×
350
        }
351
    }
×
352
}
353

354
impl<T: RegisteredClass> IntoZval for ZBox<ZendClassObject<T>> {
355
    const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME));
356
    const NULLABLE: bool = false;
357

358
    fn set_zval(mut self, zv: &mut Zval, _: bool) -> Result<()> {
×
359
        // `set_object` inc_counts on insertion, so dec_count first to keep the
360
        // net refcount at 1. Matches `ZBox<ZendObject>::set_zval` in object.rs.
361
        self.std.dec_count();
×
362
        let obj = self.into_raw();
×
363
        zv.set_object(&mut obj.std);
×
364
        Ok(())
×
365
    }
×
366
}
367

368
impl<T: RegisteredClass> IntoZval for &mut ZendClassObject<T> {
369
    const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME));
370
    const NULLABLE: bool = false;
371

372
    #[inline]
373
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
374
        zv.set_object(&mut self.std);
×
375
        Ok(())
×
376
    }
×
377
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc