diff --git a/CHANGELOG.md b/CHANGELOG.md index 9709a72e91..9161829e8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +- Constructors that return `Self` can now be added to classes. [#83] + - `Default` is no longer required to be implemented on classes, however, a + constructor must be specified if you want to construct the class from PHP. + - Constructors can return `Self` or `Result`, where + `E: Into`. + +[#83]: https://github.com/davidcole1340/ext-php-rs/pull/83 + ## Version 0.5.1 - `PhpException` no longer requires a lifetime [#80]. diff --git a/ext-php-rs-derive/src/class.rs b/ext-php-rs-derive/src/class.rs index bae91eafe0..61b62f86fb 100644 --- a/ext-php-rs-derive/src/class.rs +++ b/ext-php-rs-derive/src/class.rs @@ -14,6 +14,7 @@ pub struct Class { pub parent: Option, pub interfaces: Vec, pub methods: Vec, + pub constructor: Option, pub constants: Vec, pub properties: HashMap, } diff --git a/ext-php-rs-derive/src/function.rs b/ext-php-rs-derive/src/function.rs index 56976269d0..edc7727233 100644 --- a/ext-php-rs-derive/src/function.rs +++ b/ext-php-rs-derive/src/function.rs @@ -56,7 +56,7 @@ pub fn parser(args: AttributeArgs, input: ItemFn) -> Result<(TokenStream, Functi let args = build_args(inputs, &attr_args.defaults)?; let optional = find_optional_parameter(args.iter(), attr_args.optional); let arg_definitions = build_arg_definitions(&args); - let arg_parser = build_arg_parser(args.iter(), &optional)?; + let arg_parser = build_arg_parser(args.iter(), &optional, "e! { return; })?; let arg_accessors = build_arg_accessors(&args); let return_type = get_return_type(output)?; @@ -157,6 +157,7 @@ pub fn find_optional_parameter<'a>( pub fn build_arg_parser<'a>( args: impl Iterator, optional: &Option, + ret: &TokenStream, ) -> Result { let mut rest_optional = false; @@ -194,13 +195,15 @@ pub fn build_arg_parser<'a>( .parse(); if parser.is_err() { - return; + #ret } }) } fn build_arg_accessors(args: &[Arg]) -> Vec { - args.iter().map(|arg| arg.get_accessor()).collect() + args.iter() + .map(|arg| arg.get_accessor("e! { return; })) + .collect() } pub fn get_return_type(output_type: &ReturnType) -> Result> { @@ -286,7 +289,7 @@ impl Arg { } /// Returns a [`TokenStream`] containing the line required to retrieve the value from the argument. - pub fn get_accessor(&self) -> TokenStream { + pub fn get_accessor(&self, ret: &TokenStream) -> TokenStream { let name = &self.name; let name_ident = self.get_name_ident(); @@ -310,7 +313,7 @@ impl Arg { ) .throw() .expect(concat!("Failed to throw exception: Invalid value given for argument `", #name, "`.")); - return; + #ret } } } diff --git a/ext-php-rs-derive/src/impl_.rs b/ext-php-rs-derive/src/impl_.rs index 43cdbd102e..ea8f31b8d9 100644 --- a/ext-php-rs-derive/src/impl_.rs +++ b/ext-php-rs-derive/src/impl_.rs @@ -82,6 +82,7 @@ pub enum ParsedAttribute { prop_name: Option, ty: PropAttrTy, }, + Constructor, } #[derive(Default, Debug, FromMeta)] @@ -151,7 +152,14 @@ pub fn parser(args: AttributeArgs, input: ItemImpl) -> Result { PropAttrTy::Setter => prop.add_setter(ident)?, } } - class.methods.push(parsed_method.method); + if parsed_method.constructor { + if class.constructor.is_some() { + bail!("You cannot have two constructors on the same class."); + } + class.constructor = Some(parsed_method.method); + } else { + class.methods.push(parsed_method.method); + } parsed_method.tokens } item => item.to_token_stream(), @@ -239,6 +247,7 @@ pub fn parse_attribute(attr: &Attribute) -> Result { ty: PropAttrTy::Setter, } } + "constructor" => ParsedAttribute::Constructor, attr => bail!("Invalid attribute `#[{}]`.", attr), }) } diff --git a/ext-php-rs-derive/src/method.rs b/ext-php-rs-derive/src/method.rs index 7efd92a990..73c68edeec 100644 --- a/ext-php-rs-derive/src/method.rs +++ b/ext-php-rs-derive/src/method.rs @@ -42,6 +42,7 @@ pub struct ParsedMethod { pub tokens: TokenStream, pub method: Method, pub property: Option<(String, PropAttrTy)>, + pub constructor: bool, } impl ParsedMethod { @@ -49,11 +50,13 @@ impl ParsedMethod { tokens: TokenStream, method: Method, property: Option<(String, PropAttrTy)>, + constructor: bool, ) -> Self { Self { tokens, method, property, + constructor, } } } @@ -64,6 +67,7 @@ pub fn parser(input: &mut ImplItemMethod, rename_rule: RenameRule) -> Result Result is_constructor = true, } } @@ -102,6 +107,20 @@ pub fn parser(input: &mut ImplItemMethod, rename_rule: RenameRule) -> Result Result ::ext_php_rs::php::types::object::ConstructorResult { + use ::ext_php_rs::php::types::zval::IntoZval; + use ::ext_php_rs::php::types::object::ConstructorResult; + + #(#arg_definitions)* + #arg_parser + + Self::#ident(#(#arg_accessors,)*).into() + } + } + } else { + quote! { + #input - #[doc(hidden)] - pub extern "C" fn #internal_ident(ex: &mut ::ext_php_rs::php::execution_data::ExecutionData, retval: &mut ::ext_php_rs::php::types::zval::Zval) { - use ::ext_php_rs::php::types::zval::IntoZval; + #[doc(hidden)] + pub extern "C" fn #internal_ident( + ex: &mut ::ext_php_rs::php::execution_data::ExecutionData, + retval: &mut ::ext_php_rs::php::types::zval::Zval + ) { + use ::ext_php_rs::php::types::zval::IntoZval; - #(#arg_definitions)* - #arg_parser + #(#arg_definitions)* + #arg_parser - let result = #this #ident(#(#arg_accessors, )*); + let result = #this #ident(#(#arg_accessors, )*); - if let Err(e) = result.set_zval(retval, false) { - let e: ::ext_php_rs::php::exceptions::PhpException = e.into(); - e.throw().expect("Failed to throw exception"); + if let Err(e) = result.set_zval(retval, false) { + let e: ::ext_php_rs::php::exceptions::PhpException = e.into(); + e.throw().expect("Failed to throw exception"); + } } } }; - let name = identifier.unwrap_or_else(|| rename_rule.rename(ident.to_string())); let method = Method { name, ident: internal_ident.to_string(), @@ -151,7 +191,7 @@ pub fn parser(input: &mut ImplItemMethod, rename_rule: RenameRule) -> Result (Vec, bool) { fn build_arg_parser<'a>( args: impl Iterator, optional: &Option, + ret: &TokenStream, ) -> Result { function::build_arg_parser( args.filter_map(|arg| match arg { @@ -223,13 +264,14 @@ fn build_arg_parser<'a>( _ => None, }), optional, + ret, ) } -fn build_arg_accessors(args: &[Arg]) -> Vec { +fn build_arg_accessors(args: &[Arg], ret: &TokenStream) -> Vec { args.iter() .filter_map(|arg| match arg { - Arg::Typed(arg) => Some(arg.get_accessor()), + Arg::Typed(arg) => Some(arg.get_accessor(ret)), _ => None, }) .collect() @@ -241,27 +283,27 @@ impl Method { Ident::new(&self.ident, Span::call_site()) } + pub fn get_arg_definitions(&self) -> impl Iterator + '_ { + self.args.iter().filter_map(move |arg| match arg { + Arg::Typed(arg) => { + let def = arg.get_arg_definition(); + let prelude = self.optional.as_ref().and_then(|opt| { + if opt.eq(&arg.name) { + Some(quote! { .not_required() }) + } else { + None + } + }); + Some(quote! { #prelude.arg(#def) }) + } + _ => None, + }) + } + pub fn get_builder(&self, class_path: &Ident) -> TokenStream { let name = &self.name; let name_ident = self.get_name_ident(); - let args = self - .args - .iter() - .filter_map(|arg| match arg { - Arg::Typed(arg) => { - let def = arg.get_arg_definition(); - let prelude = self.optional.as_ref().and_then(|opt| { - if opt.eq(&arg.name) { - Some(quote! { .not_required() }) - } else { - None - } - }); - Some(quote! { #prelude.arg(#def) }) - } - _ => None, - }) - .collect::>(); + let args = self.get_arg_definitions(); let output = self.output.as_ref().map(|(ty, nullable)| { let ty: Type = syn::parse_str(ty).unwrap(); diff --git a/ext-php-rs-derive/src/module.rs b/ext-php-rs-derive/src/module.rs index a0a89ef1f9..fcaf7645e7 100644 --- a/ext-php-rs-derive/src/module.rs +++ b/ext-php-rs-derive/src/module.rs @@ -94,12 +94,34 @@ pub fn generate_registered_class_impl(class: &Class) -> Result { .properties .iter() .map(|(name, prop)| prop.as_prop_tuple(name)); + let constructor = if let Some(constructor) = &class.constructor { + let func = Ident::new(&constructor.ident, Span::call_site()); + let args = constructor.get_arg_definitions(); + quote! { + Some(::ext_php_rs::php::types::object::ConstructorMeta { + constructor: Self::#func, + build_fn: { + use ext_php_rs::php::function::FunctionBuilder; + fn build_fn(func: FunctionBuilder) -> FunctionBuilder { + func + #(#args)* + } + build_fn + } + }) + } + } else { + quote! { None } + }; Ok(quote! { static #meta: ::ext_php_rs::php::types::object::ClassMetadata<#self_ty> = ::ext_php_rs::php::types::object::ClassMetadata::new(); impl ::ext_php_rs::php::types::object::RegisteredClass for #self_ty { const CLASS_NAME: &'static str = #class_name; + const CONSTRUCTOR: ::std::option::Option< + ::ext_php_rs::php::types::object::ConstructorMeta + > = #constructor; fn get_metadata() -> &'static ::ext_php_rs::php::types::object::ClassMetadata { &#meta diff --git a/guide/src/macros/classes.md b/guide/src/macros/classes.md index 3be3f897f0..8ff62736dd 100644 --- a/guide/src/macros/classes.md +++ b/guide/src/macros/classes.md @@ -4,15 +4,6 @@ Structs can be exported to PHP as classes with the `#[php_class]` attribute macro. This attribute derives the `RegisteredClass` trait on your struct, as well as registering the class to be registered with the `#[php_module]` macro. -The implementation of `RegisteredClass` requires the implementation of `Default` -on the struct. This is because the struct is initialized before the constructor -is called, therefore it must have default values for all properties. - -Note that Rust struct properties **are not** PHP properties, so if you want the -user to be able to access these, you must provide getters and/or setters. -Properties are supported internally, however, they are not usable through the -automatic macros. Support for properties is planned. - ## Options The attribute takes some options to modify the output of the class: @@ -33,21 +24,22 @@ placed underneath the `#[php_class]` attribute. You may also use the `#[prop]` attribute on a struct field to use the field as a PHP property. By default, the field will be accessible from PHP publically with -the same name as the field. You can rename the property with options: +the same name as the field. Property types must implement `IntoZval` and +`FromZval`. + +You can rename the property with options: - `rename` - Allows you to rename the property, e.g. `#[prop(rename = "new_name")]` ## Example -This example creates a PHP class `Human`, adding a PHP property `address` with -an empty string as the default value. +This example creates a PHP class `Human`, adding a PHP property `address`. ```rust # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_class] -#[derive(Default)] pub struct Human { name: String, age: i32, diff --git a/guide/src/macros/impl.md b/guide/src/macros/impl.md index b490978f3c..00b823353c 100644 --- a/guide/src/macros/impl.md +++ b/guide/src/macros/impl.md @@ -42,6 +42,22 @@ The rest of the options are passed as separate attributes: The `#[defaults]` and `#[optional]` attributes operate the same as the equivalent function attribute parameters. +### Constructors + +By default, if a class does not have a constructor, it is not constructable from +PHP. It can only be returned from a Rust function to PHP. + +Constructors are Rust methods whick can take any amount of parameters and +returns either `Self` or `Result`, where `E: Into`. When +the error variant of `Result` is encountered, it is thrown as an exception and +the class is not constructed. + +Constructors are designated by either naming the method `__construct` or by +annotating a method with the `#[constructor]` attribute. Note that when using +the attribute, the function is not exported to PHP like a regular method. + +Constructors cannot use the visibility or rename attributes listed above. + ## Constants Constants are defined as regular Rust `impl` constants. Any type that implements @@ -89,9 +105,9 @@ constant for the maximum age of a `Human`. impl Human { const MAX_AGE: i32 = 100; - pub fn __construct(&mut self, name: String, age: i32) { - self.name = name; - self.age = age; + // No `#[constructor]` attribute required here - the name is `__construct`. + pub fn __construct(name: String, age: i32) -> Self { + Self { name, age, address: String::new() } } #[getter] @@ -110,8 +126,6 @@ impl Human { } pub fn introduce(&self) { - use ext_php_rs::php::types::object::RegisteredClass; - println!("My name is {} and I am {} years old. I live at {}.", self.name, self.age, self.address); } diff --git a/src/lib.rs b/src/lib.rs index 5a76d144e1..cecf559c76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -256,12 +256,21 @@ pub use ext_php_rs_derive::php_function; /// Methods can take a immutable or a mutable reference to `self`, but cannot consume `self`. They /// can also take no reference to `self` which indicates a static method. /// +/// ## Constructors +/// +/// You may add *one* constructor to the impl block. This method must be called `__construct` or be +/// tagged with the `#[constructor]` attribute, and it will not be exported to PHP like a regular +/// method. +/// +/// The constructor method must not take a reference to `self` and must return `Self` or +/// [`Result`][`Result`], where `E: Into`. +/// /// # Example /// /// ```no_run /// # use ext_php_rs::prelude::*; /// #[php_class] -/// #[derive(Debug, Default)] +/// #[derive(Debug)] /// pub struct Human { /// name: String, /// age: i32, @@ -274,9 +283,8 @@ pub use ext_php_rs_derive::php_function; /// /// #[optional(age)] /// #[defaults(age = 0)] -/// pub fn __construct(&mut self, name: String, age: i32) { -/// self.name = name; -/// self.age = age; +/// pub fn __construct(name: String, age: i32) -> Self { +/// Self { name, age } /// } /// /// pub fn get_name(&self) -> String { @@ -340,9 +348,8 @@ pub use ext_php_rs_derive::php_module; /// Annotates a struct that will be exported to PHP as a class. /// -/// The struct that this attribute is used on must implement [`Default`], as this is used to -/// initialize the struct before the constructor is called. You may define a constructor with -/// the [`macro@php_impl`] macro which can modify the properties of the struct. +/// By default, the class cannot be constructed from PHP. You must add a constructor method in the +/// [`macro@php_impl`] impl block to be able to construct the object from PHP. /// /// This attribute takes a set of optional arguments: /// @@ -372,7 +379,6 @@ pub use ext_php_rs_derive::php_module; /// ``` /// # use ext_php_rs::prelude::*; /// #[php_class] -/// #[derive(Default)] /// pub struct Example { /// x: i32, /// y: String, @@ -394,7 +400,6 @@ pub use ext_php_rs_derive::php_module; /// /// #[php_class(name = "Redis\\Exception\\RedisException")] /// #[extends(ClassEntry::exception())] -/// #[derive(Default)] /// pub struct Example; /// /// #[php_function] diff --git a/src/php/class.rs b/src/php/class.rs index 5eccfd2c9b..7b100826dd 100644 --- a/src/php/class.rs +++ b/src/php/class.rs @@ -2,7 +2,12 @@ use crate::{ errors::{Error, Result}, - php::types::object::{ZendClassObject, ZendObject}, + php::{ + exceptions::PhpException, + execution_data::ExecutionData, + function::FunctionBuilder, + types::object::{ClassObject, ConstructorMeta, ConstructorResult, ZendObject}, + }, }; use std::{alloc::Layout, convert::TryInto, ffi::CString, fmt::Debug}; @@ -269,16 +274,61 @@ impl ClassBuilder { /// Panics if the class name associated with `T` is not the same as the class name specified /// when creating the builder. pub fn object_override(mut self) -> Self { - unsafe extern "C" fn create_object( - _: *mut ClassEntry, - ) -> *mut ZendObject { - let ptr = ZendClassObject::::new_ptr(None); - (*ptr).get_mut_zend_obj() + extern "C" fn create_object(_: *mut ClassEntry) -> *mut ZendObject { + // SAFETY: After calling this function, PHP will always call the constructor defined below, + // which assumes that the object is uninitialized. + let obj = unsafe { ClassObject::::new_uninit() }; + obj.into_inner().get_mut_zend_obj() + } + + extern "C" fn constructor(ex: &mut ExecutionData, _: &mut Zval) { + let ConstructorMeta { constructor, .. } = match T::CONSTRUCTOR { + Some(c) => c, + None => { + PhpException::default("You cannot instantiate this class from PHP.".into()) + .throw() + .expect("Failed to throw exception when constructing class"); + return; + } + }; + + let this = match constructor(ex) { + ConstructorResult::Ok(this) => this, + ConstructorResult::Exception(e) => { + e.throw() + .expect("Failed to throw exception while constructing class"); + return; + } + ConstructorResult::ArgError => return, + }; + let this_obj = match ex.get_object::() { + Some(obj) => obj, + None => { + PhpException::default("Failed to retrieve reference to `this` object.".into()) + .throw() + .expect("Failed to throw exception while constructing class"); + return; + } + }; + this_obj.initialize(this); } - assert_eq!(self.name.as_str(), T::CLASS_NAME); + debug_assert_eq!( + self.name.as_str(), + T::CLASS_NAME, + "Class name in builder does not match class name in `impl RegisteredClass`." + ); self.object_override = Some(create_object::); - self + self.method( + { + let mut func = FunctionBuilder::new("__construct", constructor::); + if let Some(ConstructorMeta { build_fn, .. }) = T::CONSTRUCTOR { + func = build_fn(func); + } + func.build().expect("Failed to build constructor function") + }, + MethodFlags::Public, + ) } /// Builds the class, returning a reference to the class entry. diff --git a/src/php/execution_data.rs b/src/php/execution_data.rs index a2d342fd40..30cc7a035f 100644 --- a/src/php/execution_data.rs +++ b/src/php/execution_data.rs @@ -1,13 +1,10 @@ //! Functions for interacting with the execution data passed to PHP functions\ //! introduced in Rust. -use crate::{ - bindings::{zend_execute_data, ZEND_MM_ALIGNMENT, ZEND_MM_ALIGNMENT_MASK}, - errors::{Error, Result}, -}; +use crate::bindings::{zend_execute_data, ZEND_MM_ALIGNMENT, ZEND_MM_ALIGNMENT_MASK}; use super::types::{ - object::{ClassObject, RegisteredClass, ZendClassObject, ZendObject}, + object::{RegisteredClass, ZendClassObject, ZendObject}, zval::Zval, }; @@ -17,25 +14,18 @@ pub type ExecutionData = zend_execute_data; impl ExecutionData { /// Attempts to retrieve a reference to the underlying class object of the Zend object. /// - /// Returns a [`ClassObject`] if the execution data contained a valid object, otherwise - /// returns [`None`]. - /// - /// # Safety - /// - /// The caller must guarantee that the function is called on an instance of [`ExecutionData`] - /// that: - /// - /// 1. Contains an object. - /// 2. The object was originally derived from `T`. - pub unsafe fn get_object(&self) -> Option> { - let ptr = ZendClassObject::::from_zend_obj_ptr(self.This.object()?)?; - Some(ClassObject::from_zend_class_object(ptr, false)) + /// Returns a [`ZendClassObject`] if the execution data contained a valid object of type `T`, + /// otherwise returns [`None`]. + pub fn get_object(&self) -> Option<&mut ZendClassObject> { + // TODO(david): This should be a `&mut self` function but we need to fix arg parser first. + ZendClassObject::from_zend_obj_mut(self.get_self()?) } /// Attempts to retrieve the 'this' object, which can be used in class methods /// to retrieve the underlying Zend object. - pub fn get_self(&self) -> Result<&mut ZendObject> { - unsafe { self.This.value.obj.as_mut() }.ok_or(Error::InvalidScope) + pub fn get_self(&self) -> Option<&mut ZendObject> { + // TODO(david): This should be a `&mut self` function but we need to fix arg parser first. + unsafe { self.This.value.obj.as_mut() } } /// Translation of macro `ZEND_CALL_ARG(call, n)` diff --git a/src/php/types/closure.rs b/src/php/types/closure.rs index a82e01adae..2917485fcd 100644 --- a/src/php/types/closure.rs +++ b/src/php/types/closure.rs @@ -43,9 +43,7 @@ static CLOSURE_META: ClassMetadata = ClassMetadata::new(); /// /// When the `__invoke` method is called from PHP, the `invoke` method is called on the `dyn PhpClosure`\ /// trait object, and from there everything is basically the same as a regular PHP function. -pub struct Closure { - func: Option>, -} +pub struct Closure(Box); unsafe impl Send for Closure {} unsafe impl Sync for Closure {} @@ -74,9 +72,7 @@ impl Closure { where T: PhpClosure + 'static, { - Self { - func: Some(Box::new(func) as Box), - } + Self(Box::new(func) as Box) } /// Wraps a [`FnOnce`] Rust closure into a type which can be returned to PHP. If the closure @@ -136,20 +132,11 @@ impl Closure { /// External function used by the Zend interpreter to call the closure. extern "C" fn invoke(ex: &mut ExecutionData, ret: &mut Zval) { - let mut this = unsafe { ex.get_object::() }.expect("asdf"); - - match this.func.as_mut() { - Some(closure) => closure.invoke(ex, ret), - None => panic!("You cannot instantiate a `RustClosure` from PHP."), - } - } -} - -impl Default for Closure { - fn default() -> Self { - PhpException::default("You cannot instantiate a default instance of `RustClosure`.".into()); + let this = ex + .get_object::() + .expect("Internal closure function called on non-closure class"); - Self { func: None } + this.0.invoke(ex, ret) } } @@ -174,7 +161,7 @@ impl RegisteredClass for Closure { /// This trait is automatically implemented on functions with up to 8 parameters. pub unsafe trait PhpClosure { /// Invokes the closure. - fn invoke(&mut self, ex: &mut ExecutionData, ret: &mut Zval); + fn invoke(&mut self, ex: &ExecutionData, ret: &mut Zval); } /// Implemented on [`FnOnce`] types which can be used as PHP closures. See [`Closure`]. @@ -190,7 +177,7 @@ unsafe impl PhpClosure for Box R> where R: IntoZval, { - fn invoke(&mut self, _: &mut ExecutionData, ret: &mut Zval) { + fn invoke(&mut self, _: &ExecutionData, ret: &mut Zval) { if let Err(e) = self().set_zval(ret, false) { let _ = PhpException::default(format!("Failed to return closure result to PHP: {}", e)) .throw(); @@ -202,7 +189,7 @@ unsafe impl PhpClosure for Box R> where R: IntoZval, { - fn invoke(&mut self, _: &mut ExecutionData, ret: &mut Zval) { + fn invoke(&mut self, _: &ExecutionData, ret: &mut Zval) { if let Err(e) = self().set_zval(ret, false) { let _ = PhpException::default(format!("Failed to return closure result to PHP: {}", e)) .throw(); @@ -241,7 +228,7 @@ macro_rules! php_closure_impl { impl<$($gen),*, Ret> PhpOnceClosure for Box Ret> where - $($gen: FromZval<'static> + 'static,)* + $(for<'a> $gen: FromZval<'a> + 'static,)* Ret: IntoZval + 'static, { fn into_closure(self) -> Closure { @@ -268,10 +255,10 @@ macro_rules! php_closure_impl { ($fnty: ident; $($gen: ident),*) => { unsafe impl<$($gen),*, Ret> PhpClosure for Box Ret> where - $($gen: FromZval<'static>,)* + $(for<'a> $gen: FromZval<'a>,)* Ret: IntoZval { - fn invoke(&mut self, ex: &mut ExecutionData, ret: &mut Zval) { + fn invoke(&mut self, ex: &ExecutionData, ret: &mut Zval) { $( let mut $gen = Arg::new(stringify!($gen), $gen::TYPE); )* diff --git a/src/php/types/object.rs b/src/php/types/object.rs index 27ae381b46..ee148a0932 100644 --- a/src/php/types/object.rs +++ b/src/php/types/object.rs @@ -28,8 +28,10 @@ use crate::{ php::{ class::ClassEntry, enums::DataType, - exceptions::PhpResult, + exceptions::{PhpException, PhpResult}, + execution_data::ExecutionData, flags::ZvalTypeFlags, + function::FunctionBuilder, globals::ExecutorGlobals, types::{array::OwnedHashTable, string::ZendString}, }, @@ -279,6 +281,14 @@ impl ZendObject { } } +impl<'a> FromZval<'a> for &'a ZendObject { + const TYPE: DataType = DataType::Object(None); + + fn from_zval(zval: &'a Zval) -> Option { + zval.object() + } +} + /// `FromZendObject` is implemented by types which can be extracted from a Zend object. /// /// Normal usage is through the helper method `ZendObject::extract`: @@ -378,162 +388,228 @@ impl<'a, T: RegisteredClass> IntoZval for ClassRef<'a, T> { } } -pub struct ClassObject<'a, T: RegisteredClass> { - ptr: &'a mut ZendClassObject, - free: bool, -} +/// The owned variant of [`ZendClassObject`]. +pub struct ClassObject(NonNull>); -impl Default for ClassObject<'_, T> { - fn default() -> Self { - let ptr = unsafe { - ZendClassObject::new_ptr(None) - .as_mut() - .expect("Failed to allocate memory for class object.") - }; - - Self { ptr, free: true } - } -} - -impl<'a, T: RegisteredClass + 'a> ClassObject<'a, T> { - /// Creates a class object from a pre-existing Rust object. +impl ClassObject { + /// Creates a new [`ZendObject`] of type `T`, where `T` is a [`RegisteredClass`] in PHP, storing the given + /// value `val` inside the object. /// /// # Parameters /// - /// * `obj` - The object to create a class object for. - pub fn new(obj: T) -> Self { - let ptr = unsafe { - ZendClassObject::new_ptr(Some(obj)) - .as_mut() - .expect("Failed to allocate memory for class object.") - }; - - Self { ptr, free: true } - } - - /// Converts the class object into an owned [`ZendObject`], removing any reference to - /// the embedded struct type `T`. - pub fn into_owned_object(self) -> OwnedZendObject { - let mut this = ManuallyDrop::new(self); - OwnedZendObject((&mut this.ptr.std).into()) - } - - /// Consumes the class object, releasing the internal pointer without releasing the internal object. + /// * `val` - The value to store inside the object. /// - /// Used to transfer ownership of the object to PHP. - pub(crate) fn into_raw(mut self) -> *mut ZendClassObject { - self.free = false; - self.ptr - } - - /// Returns an immutable reference to the underlying class object. - pub(crate) fn internal(&self) -> &ZendClassObject { - self.ptr + /// # Panics + /// + /// Panics if memory was unable to be allocated for the new object. + pub fn new(val: T) -> Self { + unsafe { Self::internal_new(MaybeUninit::new(val), true) } } - /// Returns a mutable reference to the underlying class object. - pub(crate) fn internal_mut(&mut self) -> &mut ZendClassObject { - self.ptr + /// Creates a new [`ZendObject`] of type `T`, with an uninitialized internal object. + /// + /// # Safety + /// + /// As the object is uninitialized, the caller must ensure the following until the internal object is + /// initialized: + /// + /// * The [`Drop`] implementation is never called. + /// * The [`Clone`] implementation is never called. + /// * The [`Debug`] implementation is never called. + /// * The object is never dereferenced to `T`. + /// + /// If any of these conditions are not met while not initialized, the corresponding function will panic. + /// Converting the object into its inner with the [`into_inner`] function is valid, however. + /// + /// [`into_inner`]: #method.into_inner + /// + /// # Panics + /// + /// Panics if memory was unable to be allocated for the new object. + pub unsafe fn new_uninit() -> Self { + Self::internal_new(MaybeUninit::uninit(), false) } - /// Creates a new instance of [`ClassObject`] around a pre-existing class object. + /// Creates a new [`ZendObject`] of type `T`, storing the given (and potentially uninitialized) `val` + /// inside the object. /// /// # Parameters /// - /// * `ptr` - Pointer to the class object. - /// * `free` - Whether to release the underlying object which `ptr` points to. + /// * `val` - Value to store inside the object. See safety section. + /// * `init` - Whether the given `val` was initialized. /// /// # Safety /// - /// Caller must guarantee that `ptr` points to an aligned, non-null instance of - /// [`ZendClassObject`]. Caller must also guarantee that `ptr` will at least live for - /// the lifetime `'a` (as long as the resulting object lives). + /// Providing an initialized variant of [`MaybeUninit`] is safe. + /// + /// Providing an uninitalized variant of [`MaybeUninit`] is unsafe. As the object is uninitialized, + /// the caller must ensure the following until the internal object is initialized: + /// + /// * The [`Drop`] implementation is never called. + /// * The [`Clone`] implementation is never called. + /// * The [`Debug`] implementation is never called. + /// * The object is never dereferenced to `T`. /// - /// Caller must also guarantee that it is expected to free `ptr` after dropping the - /// resulting [`ClassObject`] to prevent use-after-free situations. + /// If any of these conditions are not met while not initialized, the corresponding function will panic. + /// Converting the object into its inner with the [`into_inner`] function is valid, however. You can initialize + /// the object with the [`initialize`] function. + /// + /// [`into_inner`]: #method.into_inner + /// [`initialize`]: #method.initialize /// /// # Panics /// - /// Panics when the given `ptr` is null. - pub(crate) unsafe fn from_zend_class_object(ptr: *mut ZendClassObject, free: bool) -> Self { - Self { - ptr: ptr.as_mut().expect("Given pointer was null"), - free, - } + /// Panics if memory was unable to be allocated for the new object. + unsafe fn internal_new(val: MaybeUninit, init: bool) -> Self { + let size = mem::size_of::>(); + let meta = T::get_metadata(); + let ce = meta.ce() as *const _ as *mut _; + let obj = ext_php_rs_zend_object_alloc(size as _, ce) as *mut ZendClassObject; + let obj = obj + .as_mut() + .expect("Failed to allocate for new Zend object"); + + zend_object_std_init(&mut obj.std, ce); + object_properties_init(&mut obj.std, ce); + + obj.obj = val; + obj.init = init; + obj.std.handlers = meta.handlers(); + Self(obj.into()) } -} -impl Drop for ClassObject<'_, T> { - fn drop(&mut self) { - if self.free { - unsafe { ext_php_rs_zend_object_release(&mut (*self.ptr).std) }; - } + /// Initializes the class object with the value `val`. + /// + /// # Parameters + /// + /// * `val` - The value to initialize the object with. + /// + /// # Returns + /// + /// Returns the old value in an [`Option`] if the object had already been initialized, [`None`] + /// otherwise. + pub fn initialize(&mut self, val: T) -> Option { + // Instead of using the `Deref` implementation, we write a wrapper function, as calling `deref()` + // on an uninitialized object will panic. + + // SAFETY: All constructors of `NewClassObject` guarantee that it will contain a valid pointer. + unsafe { self.0.as_mut() }.initialize(val) } -} -impl Deref for ClassObject<'_, T> { - type Target = T; + /// Converts the [`ClassObject`] into the inner pointer, which is a pointer to [`ZendClassObject`]. + /// The caller is responsible for freeing the returned pointer when finished. + pub fn into_inner(self) -> &'static mut ZendClassObject { + let mut this = ManuallyDrop::new(self); + // SAFETY: All constructors of `ClassObject` guarantee that it will contain a valid pointer. + unsafe { this.0.as_mut() } + } - fn deref(&self) -> &Self::Target { - // SAFETY: Class object constructor guarantees memory is allocated. - unsafe { &*self.ptr.obj.as_ptr() } + /// Converts the class object into an owned [`ZendObject`], removing any reference to + /// the embedded struct type `T`. + pub fn into_owned_object(self) -> OwnedZendObject { + let mut this = ManuallyDrop::new(self); + OwnedZendObject((&mut this.deref_mut().std).into()) } } -impl DerefMut for ClassObject<'_, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - // SAFETY: Class object constructor guarantees memory is allocated. - unsafe { &mut *self.ptr.obj.as_mut_ptr() } +impl Default for ClassObject { + fn default() -> Self { + Self::new(T::default()) } } -impl Clone for ClassObject<'_, T> { +impl Clone for ClassObject { fn clone(&self) -> Self { - // SAFETY: Class object constructor guarantees memory is allocated. - let mut new = Self::new(unsafe { &*self.internal().obj.as_ptr() }.clone()); - unsafe { - zend_objects_clone_members( - &mut new.internal_mut().std, - &self.internal().std as *const _ as *mut _, - ) + if !self.init { + panic!("Attempted to clone uninitialized class object"); } + + // SAFETY: All constructors of `NewClassObject` guarantee that it will contain a valid pointer. + // The constructor also guarantees that the internal `ZendClassObject` pointer will contain a valid, + // initialized `obj`, therefore we can dereference both safely. + let mut new = Self::new(unsafe { self.0.as_ref().obj.assume_init_ref().clone() }); + unsafe { zend_objects_clone_members(&mut new.std, &self.std as *const _ as *mut _) }; new } } -impl Debug for ClassObject<'_, T> { +impl Debug for ClassObject { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.internal().obj.fmt(f) + if !self.init { + panic!("Attempted to call `Debug` implementation on uninitialized class object"); + } + + // TODO(david): Implement `Debug` for `ZendClassObject`? + self.deref().obj.fmt(f) } } -impl IntoZval for ClassObject<'_, T> { +impl IntoZval for ClassObject { const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME)); fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { - unsafe { zv.set_object(&mut (*self.into_raw()).std) }; - + let obj = self.into_inner(); + zv.set_object(&mut obj.std); Ok(()) } } -impl<'a> FromZval<'a> for &'a ZendObject { - const TYPE: DataType = DataType::Object(None); +impl Deref for ClassObject { + type Target = ZendClassObject; - fn from_zval(zval: &'a Zval) -> Option { - zval.object() + fn deref(&self) -> &Self::Target { + // SAFETY: All constructors of `ClassObject` guarantee that it will contain a valid pointer. + let ptr = unsafe { self.0.as_ref() }; + if !ptr.init { + panic!("Attempted to access uninitialized class object"); + } + ptr } } +impl DerefMut for ClassObject { + fn deref_mut(&mut self) -> &mut Self::Target { + // SAFETY: All constructors of `ClassObject` guarantee that it will contain a valid pointer. + let ptr = unsafe { self.0.as_mut() }; + if !ptr.init { + panic!("Attempted to access uninitialized class object"); + } + ptr + } +} + +impl Drop for ClassObject { + fn drop(&mut self) { + if !self.init { + panic!("Attempted to drop uninitialized class object"); + } + + // SAFETY: All constructors guarantee that `self` contains a valid pointer. Further, all constructors + // guarantee that the `std` field of `ZendClassObject` will be initialized. + unsafe { ext_php_rs_zend_object_release(&mut self.0.as_mut().std) } + } +} + +/// Object constructor metadata. +pub struct ConstructorMeta { + /// Constructor function. + pub constructor: fn(&mut ExecutionData) -> ConstructorResult, + /// Function called to build the constructor function. Usually adds arguments. + pub build_fn: fn(FunctionBuilder) -> FunctionBuilder, +} + /// Implemented on Rust types which are exported to PHP. Allows users to get and set PHP properties on /// the object. -pub trait RegisteredClass: Default + Sized +pub trait RegisteredClass: Sized where Self: 'static, { /// PHP class name of the registered class. const CLASS_NAME: &'static str; + /// Optional class constructor. + const CONSTRUCTOR: Option> = None; + /// Returns a reference to the class metadata, which stores the class entry and handlers. /// /// This must be statically allocated, and is usually done through the [`macro@php_class`] @@ -587,39 +663,41 @@ where /// Returns a hash table containing the properties of the class. /// /// The key should be the name of the property and the value should be a reference to the property - /// with reference to `self`. The value is a trait object for [`Prop`]. - /// - /// [`Prop`]: super::props::Prop + /// with reference to `self`. The value is a [`Property`]. fn get_properties<'a>() -> HashMap<&'static str, Property<'a, Self>>; } -/// Representation of a Zend class object in memory. Usually seen through its managed variant +/// Representation of a Zend class object in memory. Usually seen through its owned variant /// of [`ClassObject`]. #[repr(C)] -pub(crate) struct ZendClassObject { +pub struct ZendClassObject { obj: MaybeUninit, + init: bool, std: zend_object, } impl ZendClassObject { - /// Allocates memory for a new PHP object. The memory is allocated using the Zend memory manager, - /// and therefore it is returned as a pointer. - pub(crate) fn new_ptr(val: Option) -> *mut Self { - let size = mem::size_of::(); - let meta = T::get_metadata(); - let ce = meta.ce() as *const _ as *mut _; - unsafe { - let obj = (ext_php_rs_zend_object_alloc(size as _, ce) as *mut Self) - .as_mut() - .expect("Failed to allocate memory for new class object."); - - zend_object_std_init(&mut obj.std, ce); - object_properties_init(&mut obj.std, ce); + /// Initializes the class object with the value `val`. + /// + /// # Parameters + /// + /// * `val` - The value to initialize the object with. + /// + /// # Returns + /// + /// Returns the old value in an [`Option`] if the object had already been initialized, [`None`] + /// otherwise. + pub fn initialize(&mut self, val: T) -> Option { + let old = Some(mem::replace(&mut self.obj, MaybeUninit::new(val))).and_then(|v| { + if self.init { + Some(unsafe { v.assume_init() }) + } else { + None + } + }); + self.init = true; - obj.obj = MaybeUninit::new(val.unwrap_or_default()); - obj.std.handlers = meta.handlers(); - obj - } + old } /// Returns a reference to the [`ZendClassObject`] of a given object `T`. Returns [`None`] @@ -634,6 +712,7 @@ impl ZendClassObject { /// Caller must guarantee that the given `obj` was created by Zend, which means that it /// is immediately followed by a [`zend_object`]. pub(crate) unsafe fn from_obj_ptr(obj: &T) -> Option<&mut Self> { + // TODO(david): Remove this function let ptr = (obj as *const T as *mut Self).as_mut()?; if ptr.std.is_instance::() { @@ -643,16 +722,30 @@ impl ZendClassObject { } } - /// Returns a reference to the [`ZendClassObject`] of a given zend object `obj`. Returns [`None`] - /// if the given object is not of the type `T`. + /// Returns a mutable reference to the [`ZendClassObject`] of a given zend object `obj`. + /// Returns [`None`] if the given object is not of the type `T`. /// /// # Parameters /// /// * `obj` - The zend object to get the [`ZendClassObject`] for. - pub(crate) fn from_zend_obj_ptr<'a>(obj: *const zend_object) -> Option<&'a mut Self> { - let ptr = obj as *const zend_object as *const i8; + pub fn from_zend_obj(std: &zend_object) -> Option<&Self> { + Some(Self::_from_zend_obj(std)?) + } + + /// Returns a mutable reference to the [`ZendClassObject`] of a given zend object `obj`. + /// Returns [`None`] if the given object is not of the type `T`. + /// + /// # Parameters + /// + /// * `obj` - The zend object to get the [`ZendClassObject`] for. + pub fn from_zend_obj_mut(std: &mut zend_object) -> Option<&mut Self> { + Self::_from_zend_obj(std) + } + + fn _from_zend_obj(std: &zend_object) -> Option<&mut Self> { + let std = std as *const zend_object as *const i8; let ptr = unsafe { - let ptr = ptr.offset(0 - Self::std_offset() as isize) as *const Self; + let ptr = std.offset(0 - Self::std_offset() as isize) as *const Self; (ptr as *mut Self).as_mut()? }; @@ -664,7 +757,7 @@ impl ZendClassObject { } /// Returns a mutable reference to the underlying Zend object. - pub(crate) fn get_mut_zend_obj(&mut self) -> &mut zend_object { + pub fn get_mut_zend_obj(&mut self) -> &mut zend_object { &mut self.std } @@ -687,6 +780,22 @@ impl Drop for ZendClassObject { } } +impl Deref for ZendClassObject { + type Target = T; + + fn deref(&self) -> &Self::Target { + // SAFETY: All constructors guarantee that `obj` is valid. + unsafe { self.obj.assume_init_ref() } + } +} + +impl DerefMut for ZendClassObject { + fn deref_mut(&mut self) -> &mut Self::Target { + // SAFETY: All constructors guarantee that `obj` is valid. + unsafe { self.obj.assume_init_mut() } + } +} + /// Stores the class entry and handlers for a Rust type which has been exported to PHP. pub struct ClassMetadata { handlers_init: AtomicBool, @@ -762,6 +871,34 @@ impl ClassMetadata { } } +/// Result returned from a constructor of a class. +pub enum ConstructorResult { + /// Successfully constructed the class, contains the new class object. + Ok(T), + /// An exception occured while constructing the class. + Exception(PhpException), + /// Invalid arguments were given to the constructor. + ArgError, +} + +impl From> for ConstructorResult +where + E: Into, +{ + fn from(result: std::result::Result) -> Self { + match result { + Ok(x) => Self::Ok(x), + Err(e) => Self::Exception(e.into()), + } + } +} + +impl From for ConstructorResult { + fn from(result: T) -> Self { + Self::Ok(result) + } +} + impl ZendObjectHandlers { /// Initializes a given set of object handlers by copying the standard object handlers into /// the memory location, as well as setting up the `T` type destructor. @@ -785,11 +922,15 @@ impl ZendObjectHandlers { } unsafe extern "C" fn free_obj(object: *mut zend_object) { - let obj = ZendClassObject::::from_zend_obj_ptr(object) + let obj = object + .as_mut() + .and_then(|obj| ZendClassObject::::from_zend_obj_mut(obj)) .expect("Invalid object pointer given for `free_obj`"); // Manually drop the object as it is wrapped with `MaybeUninit`. - ptr::drop_in_place(obj.obj.as_mut_ptr()); + if obj.init { + ptr::drop_in_place(obj.obj.as_mut_ptr()); + } zend_object_std_dtor(object) } @@ -810,8 +951,8 @@ impl ZendObjectHandlers { rv: *mut Zval, ) -> PhpResult<*mut Zval> { let obj = object - .as_ref() - .and_then(|obj| ZendClassObject::::from_zend_obj_ptr(obj)) + .as_mut() + .and_then(|obj| ZendClassObject::::from_zend_obj_mut(obj)) .ok_or("Invalid object pointer given")?; let prop_name = member .as_ref() @@ -857,8 +998,8 @@ impl ZendObjectHandlers { cache_slot: *mut *mut c_void, ) -> PhpResult<*mut Zval> { let obj = object - .as_ref() - .and_then(|obj| ZendClassObject::::from_zend_obj_ptr(obj)) + .as_mut() + .and_then(|obj| ZendClassObject::::from_zend_obj_mut(obj)) .ok_or("Invalid object pointer given")?; let prop_name = member .as_ref() @@ -895,8 +1036,8 @@ impl ZendObjectHandlers { props: &mut HashTable, ) -> PhpResult { let obj = object - .as_ref() - .and_then(|obj| ZendClassObject::::from_zend_obj_ptr(obj)) + .as_mut() + .and_then(|obj| ZendClassObject::::from_zend_obj_mut(obj)) .ok_or("Invalid object pointer given")?; let self_ = obj.obj.assume_init_mut(); let struct_props = T::get_properties(); @@ -940,8 +1081,8 @@ impl ZendObjectHandlers { cache_slot: *mut *mut c_void, ) -> PhpResult { let obj = object - .as_ref() - .and_then(|obj| ZendClassObject::::from_zend_obj_ptr(obj)) + .as_mut() + .and_then(|obj| ZendClassObject::::from_zend_obj_mut(obj)) .ok_or("Invalid object pointer given")?; let prop_name = member .as_ref() @@ -1005,7 +1146,7 @@ impl ZendObjectHandlers { impl<'a, T: RegisteredClass> FromZendObject<'a> for &'a T { fn from_zend_object(obj: &'a ZendObject) -> Result { // TODO(david): Error is kinda wrong, should have something like `WrongObject` - let cobj = ZendClassObject::::from_zend_obj_ptr(obj).ok_or(Error::InvalidPointer)?; + let cobj = ZendClassObject::::from_zend_obj(obj).ok_or(Error::InvalidPointer)?; Ok(unsafe { cobj.obj.assume_init_ref() }) } } @@ -1018,21 +1159,22 @@ impl<'a, T: RegisteredClass> FromZval<'a> for &'a T { } } -impl<'a, T: RegisteredClass> FromZendObject<'a> for &'a mut T { - fn from_zend_object(obj: &'a ZendObject) -> Result { - // TODO(david): Error is kinda wrong, should have something like `WrongObject` - let cobj = ZendClassObject::::from_zend_obj_ptr(obj).ok_or(Error::InvalidPointer)?; - Ok(unsafe { cobj.obj.assume_init_mut() }) - } -} - -impl<'a, T: RegisteredClass> FromZval<'a> for &'a mut T { - const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME)); - - fn from_zval(zval: &'a Zval) -> Option { - Self::from_zend_object(zval.object()?).ok() - } -} +// TODO(david): Need something like `FromZendObjectMut` and `FromZvalMut` +// impl<'a, T: RegisteredClass> FromZendObject<'a> for &'a mut T { +// fn from_zend_object(obj: &'a ZendObject) -> Result { +// // TODO(david): Error is kinda wrong, should have something like `WrongObject` +// let cobj = ZendClassObject::::from_zend_obj_mut(obj).ok_or(Error::InvalidPointer)?; +// Ok(unsafe { cobj.obj.assume_init_mut() }) +// } +// } +// +// impl<'a, T: RegisteredClass> FromZval<'a> for &'a mut T { +// const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME)); + +// fn from_zval(zval: &'a Zval) -> Option { +// Self::from_zend_object(zval.object()?).ok() +// } +// } impl IntoZendObject for T { fn into_zend_object(self) -> Result { diff --git a/src/php/types/zval.rs b/src/php/types/zval.rs index e4f661fce1..3087ec85cb 100644 --- a/src/php/types/zval.rs +++ b/src/php/types/zval.rs @@ -172,7 +172,7 @@ impl Zval { } } - /// Returns the value of the zval if it is an object. + /// Returns a mutable reference to the object contained in the [`Zval`], if any. pub fn object_mut(&mut self) -> Option<&mut ZendObject> { if self.is_object() { unsafe { self.value.obj.as_mut() }