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}