Skip to main content

ext_php_rs/
class.rs

1//! Types and traits used for registering classes with PHP.
2
3use std::{
4    marker::PhantomData,
5    sync::{
6        Mutex, PoisonError,
7        atomic::{AtomicPtr, Ordering},
8    },
9};
10
11use once_cell::sync::OnceCell;
12
13use crate::{
14    args::ArgInfoTables as OwnedArgInfo,
15    builders::{ClassBuilder, FunctionBuilder},
16    convert::IntoZvalDyn,
17    describe::DocComments,
18    exception::PhpException,
19    flags::{ClassFlags, ConstantFlags, MethodFlags, PropertyFlags},
20    internal::property::PropertyDescriptor,
21    zend::{ClassEntry, ExecuteData, ZendObjectHandlers},
22};
23
24/// A type alias for a tuple containing a function pointer to a class entry
25/// and a string representing the class name used in stubs.
26pub type ClassEntryInfo = (fn() -> &'static ClassEntry, &'static str);
27
28/// Implemented on Rust types which are exported to PHP. Allows users to get and
29/// set PHP properties on the object.
30pub trait RegisteredClass: Sized + 'static {
31    /// PHP class name of the registered class.
32    const CLASS_NAME: &'static str;
33
34    /// Function to be called when building the class. Allows user to modify the
35    /// class at runtime (add runtime constants etc).
36    const BUILDER_MODIFIER: Option<fn(ClassBuilder) -> ClassBuilder>;
37
38    /// Parent class entry. Optional.
39    const EXTENDS: Option<ClassEntryInfo>;
40
41    /// Interfaces implemented by the class.
42    const IMPLEMENTS: &'static [ClassEntryInfo];
43
44    /// PHP flags applied to the class.
45    const FLAGS: ClassFlags = ClassFlags::empty();
46
47    /// Doc comments for the class.
48    const DOC_COMMENTS: DocComments = &[];
49
50    /// Returns a reference to the class metadata, which stores the class entry,
51    /// handlers, and property descriptors.
52    ///
53    /// This must be statically allocated, and is usually done through the
54    /// [`macro@php_class`] macro.
55    ///
56    /// [`macro@php_class`]: crate::php_class
57    fn get_metadata() -> &'static ClassMetadata<Self>;
58
59    /// Returns method properties (from `#[php(getter)]`/`#[php(setter)]`).
60    /// Resolved via autoref specialization in the macro-generated code.
61    #[must_use]
62    fn method_properties() -> &'static [PropertyDescriptor<Self>] {
63        &[]
64    }
65
66    /// Returns the method builders required to build the class.
67    fn method_builders() -> Vec<(FunctionBuilder<'static>, MethodFlags)>;
68
69    /// Returns the class constructor (if any).
70    fn constructor() -> Option<ConstructorMeta<Self>>;
71
72    /// Returns the constants provided by the class.
73    fn constants() -> &'static [(
74        &'static str,
75        &'static dyn IntoZvalDyn,
76        DocComments,
77        ConstantFlags,
78    )];
79
80    /// Returns the static properties provided by the class.
81    ///
82    /// Static properties are declared at the class level and managed by PHP,
83    /// not by Rust handlers. Each tuple contains (name, flags, default, docs).
84    /// The default value is optional - `None` means null default.
85    #[must_use]
86    fn static_properties() -> &'static [(
87        &'static str,
88        PropertyFlags,
89        Option<&'static (dyn IntoZvalDyn + Sync)>,
90        DocComments,
91    )] {
92        &[]
93    }
94
95    /// Returns interfaces from `#[php_impl_interface]` trait implementations.
96    ///
97    /// This method is generated by the `#[php_class]` macro and uses the
98    /// `inventory` crate to collect interface registrations across crate
99    /// boundaries at link time.
100    ///
101    /// The default implementation returns an empty vector. The macro overrides
102    /// this to iterate over `InterfaceRegistration` entries matching this type.
103    #[must_use]
104    fn interface_implementations() -> Vec<ClassEntryInfo> {
105        Vec::new()
106    }
107
108    /// Returns methods from `#[php_impl_interface]` trait implementations.
109    ///
110    /// This method is generated by the `#[php_class]` macro and uses the
111    /// `inventory` crate to collect method registrations across crate
112    /// boundaries at link time.
113    ///
114    /// The default implementation returns an empty vector. The macro overrides
115    /// this to iterate over `MethodRegistration` entries matching this type.
116    #[must_use]
117    fn interface_method_implementations() -> Vec<(FunctionBuilder<'static>, MethodFlags)> {
118        Vec::new()
119    }
120
121    /// Returns a default instance of the class for immediate initialization.
122    ///
123    /// This is used when PHP creates an object without calling the constructor,
124    /// such as when throwing exceptions via `zend_throw_exception_ex`. For types
125    /// that derive `Default`, this will return `Some(Self::default())`, allowing
126    /// the object to be properly initialized even without a constructor call.
127    #[must_use]
128    fn default_init() -> Option<Self> {
129        None
130    }
131
132    /// Returns a clone of this object for PHP's `clone` operator.
133    ///
134    /// When PHP executes `clone $obj`, this method is called to produce a copy
135    /// of the Rust data. For types that derive `Clone`, the `#[php_class]` macro
136    /// generates an implementation returning `Some(self.clone())`. Types that
137    /// don't implement `Clone` use the default (returns `None`), which causes
138    /// PHP to throw an error when attempting to clone.
139    #[must_use]
140    fn clone_obj(&self) -> Option<Self> {
141        None
142    }
143}
144
145/// Stores metadata about a classes Rust constructor, including the function
146/// pointer and the arguments of the function.
147pub struct ConstructorMeta<T> {
148    /// Constructor function.
149    pub constructor: fn(&mut ExecuteData) -> ConstructorResult<T>,
150    /// Function called to build the constructor function. Usually adds
151    /// arguments.
152    pub build_fn: fn(FunctionBuilder) -> FunctionBuilder,
153    /// Add constructor modification
154    pub flags: Option<MethodFlags>,
155}
156
157/// Result returned from a constructor of a class.
158pub enum ConstructorResult<T> {
159    /// Successfully constructed the class, contains the new class object.
160    Ok(T),
161    /// An exception occurred while constructing the class.
162    Exception(PhpException),
163    /// Invalid arguments were given to the constructor.
164    ArgError,
165}
166
167impl<T, E> From<std::result::Result<T, E>> for ConstructorResult<T>
168where
169    E: Into<PhpException>,
170{
171    fn from(result: std::result::Result<T, E>) -> Self {
172        match result {
173            Ok(x) => Self::Ok(x),
174            Err(e) => Self::Exception(e.into()),
175        }
176    }
177}
178
179impl<T> From<T> for ConstructorResult<T> {
180    fn from(result: T) -> Self {
181        Self::Ok(result)
182    }
183}
184
185/// Owns the `zend_internal_arg_info` tables of a class's methods.
186struct ArgInfoTables(
187    #[expect(dead_code, reason = "held for the engine, never read back")] OwnedArgInfo,
188);
189
190// SAFETY: the tables are filled once during MINIT, before PHP spawns any request
191// thread, and are never read back through this handle. The engine only ever
192// `memcpy`s out of them (`zend_register_functions`, Zend/zend_API.c on 8.1
193// through 8.5) and never writes through the pointers they contain.
194unsafe impl Send for ArgInfoTables {}
195// SAFETY: see above.
196unsafe impl Sync for ArgInfoTables {}
197
198/// Stores the class entry, handlers, and property descriptors for a Rust type
199/// which has been exported to PHP. Usually allocated statically.
200pub struct ClassMetadata<T: 'static> {
201    handlers: OnceCell<ZendObjectHandlers>,
202    field_properties: &'static [PropertyDescriptor<T>],
203    method_properties: OnceCell<&'static [PropertyDescriptor<T>]>,
204    mangled_names: OnceCell<Box<[Box<str>]>>,
205    /// Argument info tables of this class's methods. `zend_register_functions`
206    /// borrows them until the module shuts down, so they are owned here rather
207    /// than orphaned: this static is the class's equivalent of a C extension's
208    /// `static zend_internal_arg_info[]`. A SAPI that starts the module again in
209    /// the same process registers the class again, with new tables.
210    arg_info: Mutex<Option<ArgInfoTables>>,
211    ce: AtomicPtr<ClassEntry>,
212
213    // `AtomicPtr` is used here because it is `Send + Sync`.
214    // fn() -> T could have been used but that is incompatible with const fns at
215    // the moment.
216    phantom: PhantomData<AtomicPtr<T>>,
217}
218
219impl<T: 'static> ClassMetadata<T> {
220    /// Creates a new class metadata instance with the given field properties.
221    #[must_use]
222    pub const fn new(field_properties: &'static [PropertyDescriptor<T>]) -> Self {
223        Self {
224            handlers: OnceCell::new(),
225            field_properties,
226            method_properties: OnceCell::new(),
227            mangled_names: OnceCell::new(),
228            arg_info: Mutex::new(None),
229            ce: AtomicPtr::new(std::ptr::null_mut()),
230            phantom: PhantomData,
231        }
232    }
233}
234
235impl<T: 'static> Default for ClassMetadata<T> {
236    fn default() -> Self {
237        Self::new(&[])
238    }
239}
240
241impl<T: RegisteredClass> ClassMetadata<T> {
242    /// Returns an immutable reference to the object handlers contained inside
243    /// the class metadata.
244    pub fn handlers(&self) -> &ZendObjectHandlers {
245        self.handlers.get_or_init(ZendObjectHandlers::new::<T>)
246    }
247
248    /// Checks if the class entry has been stored, returning a boolean.
249    pub fn has_ce(&self) -> bool {
250        !self.ce.load(Ordering::Acquire).is_null()
251    }
252
253    /// Retrieves a reference to the stored class entry.
254    ///
255    /// The class entry lives until the module shuts down. A SAPI that starts
256    /// the module again in the same process (`FrankenPHP` worker restarts)
257    /// registers a new one, so do not keep the reference across a restart:
258    /// call this again instead.
259    ///
260    /// # Panics
261    ///
262    /// Panics if there is no class entry stored inside the class metadata.
263    #[expect(
264        clippy::expect_used,
265        reason = "the class entry is written once during MINIT, before any Rust code can reach the metadata"
266    )]
267    pub fn ce(&self) -> &'static ClassEntry {
268        // SAFETY: There are only two values that can be stored in the atomic
269        // ptr: null or a static reference to a class entry. On the null case,
270        // `as_ref()` will return `None` and the function will panic.
271        unsafe { self.ce.load(Ordering::Acquire).as_ref() }
272            .expect("Attempted to retrieve class entry before it has been stored.")
273    }
274
275    /// Stores a reference to a class entry inside the class metadata.
276    ///
277    /// Called during MINIT, before any concurrent access. A SAPI can shut the
278    /// module down and start it again in the same process (`FrankenPHP` does on
279    /// a worker restart or `opcache_reset()`): MINIT then registers the class
280    /// again, and the new entry replaces the one freed with the old module.
281    pub fn set_ce(&self, ce: &'static mut ClassEntry) {
282        self.ce.store(ce, Ordering::Release);
283    }
284
285    /// Takes ownership of the argument info tables of this class's methods.
286    ///
287    /// `zend_register_functions` keeps `zend_internal_function.arg_info`
288    /// pointing into these until the module shuts down, so they must outlive
289    /// registration. Called from `ClassBuilder::register`, once per MINIT: when
290    /// the SAPI starts the module again in the same process, the new tables
291    /// replace those of the class freed with the old module.
292    pub fn set_arg_info(&self, arg_info: OwnedArgInfo) {
293        *self.arg_info.lock().unwrap_or_else(PoisonError::into_inner) =
294            Some(ArgInfoTables(arg_info));
295    }
296
297    /// Finds a property descriptor by name.
298    ///
299    /// Checks field properties first, then method properties. Linear scan is
300    /// used since typical PHP classes have few properties and cache locality
301    /// beats hashing at small N.
302    #[must_use]
303    #[inline]
304    pub fn find_property(&self, name: &str) -> Option<&PropertyDescriptor<T>> {
305        self.field_properties
306            .iter()
307            .find(|p| p.name == name)
308            .or_else(|| self.method_properties().iter().find(|p| p.name == name))
309    }
310
311    /// Returns the field properties (from `#[php(prop)]` struct fields).
312    #[must_use]
313    #[inline]
314    pub fn field_properties(&self) -> &'static [PropertyDescriptor<T>] {
315        self.field_properties
316    }
317
318    /// Returns the method properties (from `#[php(getter)]`/`#[php(setter)]`).
319    /// Lazily initialized on first access from `RegisteredClass::method_properties()`.
320    #[must_use]
321    #[inline]
322    pub fn method_properties(&self) -> &'static [PropertyDescriptor<T>] {
323        self.method_properties.get_or_init(T::method_properties)
324    }
325
326    /// Iterates over all properties (field + method).
327    pub fn all_properties(&self) -> impl Iterator<Item = &PropertyDescriptor<T>> {
328        self.field_properties
329            .iter()
330            .chain(self.method_properties().iter())
331    }
332
333    /// Returns the PHP-convention mangled names of every property, in
334    /// [`all_properties`](Self::all_properties) order.
335    ///
336    /// Private properties mangle to `"\0Class\0name"`, protected ones to
337    /// `"\0*\0name"`, public ones keep their name. Computed once per class on
338    /// first access, one allocation for the process lifetime.
339    #[must_use]
340    #[inline]
341    pub fn mangled_names(&self) -> &[Box<str>] {
342        self.mangled_names.get_or_init(|| {
343            self.all_properties()
344                .map(|desc| {
345                    if desc.flags.contains(PropertyFlags::Private) {
346                        format!("\0{}\0{}", T::CLASS_NAME, desc.name).into_boxed_str()
347                    } else if desc.flags.contains(PropertyFlags::Protected) {
348                        format!("\0*\0{}", desc.name).into_boxed_str()
349                    } else {
350                        desc.name.into()
351                    }
352                })
353                .collect()
354        })
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use std::panic::{RefUnwindSafe, UnwindSafe};
361
362    use super::ClassMetadata;
363
364    fn assert_unwind_safe<T: RefUnwindSafe + UnwindSafe>() {}
365
366    #[test]
367    fn class_metadata_is_unwind_safe() {
368        assert_unwind_safe::<ClassMetadata<()>>();
369    }
370}