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