Add ability to add proper object constructor (#83)

This commit is contained in:
David 2021-10-03 19:53:54 +13:00 committed by GitHub
parent c16c5d48cb
commit 125ec3bc94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 551 additions and 286 deletions

View File

@ -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<Self, E>`, where
`E: Into<PhpException>`.
[#83]: https://github.com/davidcole1340/ext-php-rs/pull/83
## Version 0.5.1
- `PhpException` no longer requires a lifetime [#80].

View File

@ -14,6 +14,7 @@ pub struct Class {
pub parent: Option<String>,
pub interfaces: Vec<String>,
pub methods: Vec<crate::method::Method>,
pub constructor: Option<crate::method::Method>,
pub constants: Vec<crate::constant::Constant>,
pub properties: HashMap<String, Property>,
}

View File

@ -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, &quote! { 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<Item = &'a Arg>,
optional: &Option<String>,
ret: &TokenStream,
) -> Result<TokenStream> {
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<TokenStream> {
args.iter().map(|arg| arg.get_accessor()).collect()
args.iter()
.map(|arg| arg.get_accessor(&quote! { return; }))
.collect()
}
pub fn get_return_type(output_type: &ReturnType) -> Result<Option<(String, bool)>> {
@ -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
}
}
}

View File

@ -82,6 +82,7 @@ pub enum ParsedAttribute {
prop_name: Option<String>,
ty: PropAttrTy,
},
Constructor,
}
#[derive(Default, Debug, FromMeta)]
@ -151,7 +152,14 @@ pub fn parser(args: AttributeArgs, input: ItemImpl) -> Result<TokenStream> {
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<ParsedAttribute> {
ty: PropAttrTy::Setter,
}
}
"constructor" => ParsedAttribute::Constructor,
attr => bail!("Invalid attribute `#[{}]`.", attr),
})
}

View File

@ -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<Par
let mut visibility = Visibility::Public;
let mut as_prop = None;
let mut identifier = None;
let mut is_constructor = false;
for attr in input.attrs.iter() {
match parse_attribute(attr)? {
@ -89,6 +93,7 @@ pub fn parser(input: &mut ImplItemMethod, rename_rule: RenameRule) -> Result<Par
});
as_prop = Some((prop_name, ty))
}
ParsedAttribute::Constructor => is_constructor = true,
}
}
@ -102,6 +107,20 @@ pub fn parser(input: &mut ImplItemMethod, rename_rule: RenameRule) -> Result<Par
..
} = &sig;
let name = identifier.unwrap_or_else(|| rename_rule.rename(ident.to_string()));
if name == "__construct" {
is_constructor = true;
}
if is_constructor && (!matches!(visibility, Visibility::Public) || as_prop.is_some()) {
bail!("`#[constructor]` attribute cannot be combined with the visibility or getter/setter attributes.");
}
let bail = if is_constructor {
quote! { return ConstructorResult::ArgError; }
} else {
quote! { return; }
};
let internal_ident = Ident::new(&format!("_internal_php_{}", ident), Span::call_site());
let args = build_args(inputs, &defaults)?;
let optional = function::find_optional_parameter(
@ -112,34 +131,55 @@ pub fn parser(input: &mut ImplItemMethod, rename_rule: RenameRule) -> Result<Par
optional,
);
let (arg_definitions, is_static) = build_arg_definitions(&args);
let arg_parser = build_arg_parser(args.iter(), &optional)?;
let arg_accessors = build_arg_accessors(&args);
let arg_parser = build_arg_parser(args.iter(), &optional, &bail)?;
let arg_accessors = build_arg_accessors(&args, &bail);
let this = if is_static {
quote! { Self:: }
} else {
quote! { this. }
};
let func = quote! {
#input
let func = if is_constructor {
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 fn #internal_ident(
ex: &mut ::ext_php_rs::php::execution_data::ExecutionData
) -> ::ext_php_rs::php::types::object::ConstructorResult<Self> {
use ::ext_php_rs::php::types::zval::IntoZval;
use ::ext_php_rs::php::types::object::ConstructorResult;
#(#arg_definitions)*
#arg_parser
#(#arg_definitions)*
#arg_parser
let result = #this #ident(#(#arg_accessors, )*);
Self::#ident(#(#arg_accessors,)*).into()
}
}
} else {
quote! {
#input
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");
#[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
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");
}
}
}
};
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<Par
visibility,
};
Ok(ParsedMethod::new(func, method, as_prop))
Ok(ParsedMethod::new(func, method, as_prop, is_constructor))
}
fn build_args(
@ -216,6 +256,7 @@ fn build_arg_definitions(args: &[Arg]) -> (Vec<TokenStream>, bool) {
fn build_arg_parser<'a>(
args: impl Iterator<Item = &'a Arg>,
optional: &Option<String>,
ret: &TokenStream,
) -> Result<TokenStream> {
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<TokenStream> {
fn build_arg_accessors(args: &[Arg], ret: &TokenStream) -> Vec<TokenStream> {
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<Item = TokenStream> + '_ {
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::<Vec<_>>();
let args = self.get_arg_definitions();
let output = self.output.as_ref().map(|(ty, nullable)| {
let ty: Type = syn::parse_str(ty).unwrap();

View File

@ -94,12 +94,34 @@ pub fn generate_registered_class_impl(class: &Class) -> Result<TokenStream> {
.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<Self>
> = #constructor;
fn get_metadata() -> &'static ::ext_php_rs::php::types::object::ClassMetadata<Self> {
&#meta

View File

@ -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,

View File

@ -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<Self, E>`, where `E: Into<PhpException>`. 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);
}

View File

@ -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<Self, E>`][`Result`], where `E: Into<PhpException>`.
///
/// # 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]

View File

@ -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<T: RegisteredClass>(mut self) -> Self {
unsafe extern "C" fn create_object<T: RegisteredClass>(
_: *mut ClassEntry,
) -> *mut ZendObject {
let ptr = ZendClassObject::<T>::new_ptr(None);
(*ptr).get_mut_zend_obj()
extern "C" fn create_object<T: RegisteredClass>(_: *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::<T>::new_uninit() };
obj.into_inner().get_mut_zend_obj()
}
assert_eq!(self.name.as_str(), T::CLASS_NAME);
extern "C" fn constructor<T: RegisteredClass>(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::<T>() {
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);
}
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::<T>);
self
self.method(
{
let mut func = FunctionBuilder::new("__construct", constructor::<T>);
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.

View File

@ -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<T: RegisteredClass>(&self) -> Option<ClassObject<'static, T>> {
let ptr = ZendClassObject::<T>::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<T: RegisteredClass>(&self) -> Option<&mut ZendClassObject<T>> {
// 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)`

View File

@ -43,9 +43,7 @@ static CLOSURE_META: ClassMetadata<Closure> = 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<Box<dyn PhpClosure>>,
}
pub struct Closure(Box<dyn PhpClosure>);
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<dyn PhpClosure>),
}
Self(Box::new(func) as Box<dyn PhpClosure>)
}
/// 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::<Self>() }.expect("asdf");
let this = ex
.get_object::<Self>()
.expect("Internal closure function called on non-closure class");
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());
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<R> PhpClosure for Box<dyn Fn() -> 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<R> PhpClosure for Box<dyn FnMut() -> 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<dyn FnOnce($($gen),*) -> 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<dyn $fnty($($gen),*) -> 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);
)*

View File

@ -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<Self> {
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<T>,
free: bool,
}
/// The owned variant of [`ZendClassObject`].
pub struct ClassObject<T>(NonNull<ZendClassObject<T>>);
impl<T: RegisteredClass> 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<T: RegisteredClass> ClassObject<T> {
/// 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.")
};
/// * `val` - The value to store inside the object.
///
/// # 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) }
}
Self { ptr, free: true }
/// 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 [`ZendObject`] of type `T`, storing the given (and potentially uninitialized) `val`
/// inside the object.
///
/// # Parameters
///
/// * `val` - Value to store inside the object. See safety section.
/// * `init` - Whether the given `val` was initialized.
///
/// # Safety
///
/// Providing an initialized variant of [`MaybeUninit<T>`] is safe.
///
/// Providing an uninitalized variant of [`MaybeUninit<T>`] 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`.
///
/// 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 if memory was unable to be allocated for the new object.
unsafe fn internal_new(val: MaybeUninit<T>, init: bool) -> Self {
let size = mem::size_of::<ZendClassObject<T>>();
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<T>;
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())
}
/// 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<T> {
// 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)
}
/// 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<T> {
let mut this = ManuallyDrop::new(self);
// SAFETY: All constructors of `ClassObject` guarantee that it will contain a valid pointer.
unsafe { this.0.as_mut() }
}
/// 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.
///
/// Used to transfer ownership of the object to PHP.
pub(crate) fn into_raw(mut self) -> *mut ZendClassObject<T> {
self.free = false;
self.ptr
}
/// Returns an immutable reference to the underlying class object.
pub(crate) fn internal(&self) -> &ZendClassObject<T> {
self.ptr
}
/// Returns a mutable reference to the underlying class object.
pub(crate) fn internal_mut(&mut self) -> &mut ZendClassObject<T> {
self.ptr
}
/// Creates a new instance of [`ClassObject`] around a pre-existing class object.
///
/// # Parameters
///
/// * `ptr` - Pointer to the class object.
/// * `free` - Whether to release the underlying object which `ptr` points to.
///
/// # 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).
///
/// Caller must also guarantee that it is expected to free `ptr` after dropping the
/// resulting [`ClassObject`] to prevent use-after-free situations.
///
/// # Panics
///
/// Panics when the given `ptr` is null.
pub(crate) unsafe fn from_zend_class_object(ptr: *mut ZendClassObject<T>, free: bool) -> Self {
Self {
ptr: ptr.as_mut().expect("Given pointer was null"),
free,
}
OwnedZendObject((&mut this.deref_mut().std).into())
}
}
impl<T: RegisteredClass> Drop for ClassObject<'_, T> {
fn drop(&mut self) {
if self.free {
unsafe { ext_php_rs_zend_object_release(&mut (*self.ptr).std) };
}
impl<T: RegisteredClass + Default> Default for ClassObject<T> {
fn default() -> Self {
Self::new(T::default())
}
}
impl<T: RegisteredClass> Deref for ClassObject<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
// SAFETY: Class object constructor guarantees memory is allocated.
unsafe { &*self.ptr.obj.as_ptr() }
}
}
impl<T: RegisteredClass> 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<T: RegisteredClass + Clone> Clone for ClassObject<'_, T> {
impl<T: RegisteredClass + Clone> Clone for ClassObject<T> {
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<T: RegisteredClass + Debug> Debug for ClassObject<'_, T> {
impl<T: Debug> Debug for ClassObject<T> {
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<T: RegisteredClass> IntoZval for ClassObject<'_, T> {
impl<T: RegisteredClass> IntoZval for ClassObject<T> {
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<T> Deref for ClassObject<T> {
type Target = ZendClassObject<T>;
fn from_zval(zval: &'a Zval) -> Option<Self> {
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<T> DerefMut for ClassObject<T> {
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<T> Drop for ClassObject<T> {
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<T> {
/// Constructor function.
pub constructor: fn(&mut ExecutionData) -> ConstructorResult<T>,
/// 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<ConstructorMeta<Self>> = 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<T> {
pub struct ZendClassObject<T> {
obj: MaybeUninit<T>,
init: bool,
std: zend_object,
}
impl<T: RegisteredClass> ZendClassObject<T> {
/// 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<T>) -> *mut Self {
let size = mem::size_of::<Self>();
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.");
/// 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<T> {
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;
zend_object_std_init(&mut obj.std, ce);
object_properties_init(&mut obj.std, ce);
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<T: RegisteredClass> ZendClassObject<T> {
/// 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::<T>() {
@ -643,16 +722,30 @@ impl<T: RegisteredClass> ZendClassObject<T> {
}
}
/// 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<T: RegisteredClass> ZendClassObject<T> {
}
/// 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<T> Drop for ZendClassObject<T> {
}
}
impl<T> Deref for ZendClassObject<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
// SAFETY: All constructors guarantee that `obj` is valid.
unsafe { self.obj.assume_init_ref() }
}
}
impl<T> DerefMut for ZendClassObject<T> {
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<T> {
handlers_init: AtomicBool,
@ -762,6 +871,34 @@ impl<T: RegisteredClass> ClassMetadata<T> {
}
}
/// Result returned from a constructor of a class.
pub enum ConstructorResult<T> {
/// 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<T, E> From<std::result::Result<T, E>> for ConstructorResult<T>
where
E: Into<PhpException>,
{
fn from(result: std::result::Result<T, E>) -> Self {
match result {
Ok(x) => Self::Ok(x),
Err(e) => Self::Exception(e.into()),
}
}
}
impl<T> From<T> for ConstructorResult<T> {
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<T: RegisteredClass>(object: *mut zend_object) {
let obj = ZendClassObject::<T>::from_zend_obj_ptr(object)
let obj = object
.as_mut()
.and_then(|obj| ZendClassObject::<T>::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::<T>::from_zend_obj_ptr(obj))
.as_mut()
.and_then(|obj| ZendClassObject::<T>::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::<T>::from_zend_obj_ptr(obj))
.as_mut()
.and_then(|obj| ZendClassObject::<T>::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::<T>::from_zend_obj_ptr(obj))
.as_mut()
.and_then(|obj| ZendClassObject::<T>::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<c_int> {
let obj = object
.as_ref()
.and_then(|obj| ZendClassObject::<T>::from_zend_obj_ptr(obj))
.as_mut()
.and_then(|obj| ZendClassObject::<T>::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<Self> {
// TODO(david): Error is kinda wrong, should have something like `WrongObject`
let cobj = ZendClassObject::<T>::from_zend_obj_ptr(obj).ok_or(Error::InvalidPointer)?;
let cobj = ZendClassObject::<T>::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<Self> {
// TODO(david): Error is kinda wrong, should have something like `WrongObject`
let cobj = ZendClassObject::<T>::from_zend_obj_ptr(obj).ok_or(Error::InvalidPointer)?;
Ok(unsafe { cobj.obj.assume_init_mut() })
}
}
// 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<Self> {
// // TODO(david): Error is kinda wrong, should have something like `WrongObject`
// let cobj = ZendClassObject::<T>::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));
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> {
Self::from_zend_object(zval.object()?).ok()
}
}
// fn from_zval(zval: &'a Zval) -> Option<Self> {
// Self::from_zend_object(zval.object()?).ok()
// }
// }
impl<T: RegisteredClass> IntoZendObject for T {
fn into_zend_object(self) -> Result<OwnedZendObject> {

View File

@ -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() }