From 783b1abd5bfc771247154c011efee6fd1ca6cdc0 Mon Sep 17 00:00:00 2001 From: David Cole Date: Sun, 3 Apr 2022 15:16:56 +1200 Subject: [PATCH 01/16] Start moving away from global macro state --- src/builders/class.rs | 4 +- src/builders/mod.rs | 2 +- src/builders/module.rs | 74 ++++++++++++++++++++++++++-- src/class.rs | 13 +++-- src/constant.rs | 3 +- src/internal/class.rs | 47 ++++++++++++++++++ src/internal/function.rs | 8 +++ src/{internal.rs => internal/mod.rs} | 3 ++ src/lib.rs | 4 ++ src/macros.rs | 7 +++ 10 files changed, 154 insertions(+), 11 deletions(-) create mode 100644 src/internal/class.rs create mode 100644 src/internal/function.rs rename src/{internal.rs => internal/mod.rs} (92%) diff --git a/src/builders/class.rs b/src/builders/class.rs index 91a6a010fc..82c4576e92 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -170,7 +170,7 @@ impl ClassBuilder { zend_fastcall! { extern fn constructor(ex: &mut ExecuteData, _: &mut Zval) { - let ConstructorMeta { constructor, .. } = match T::CONSTRUCTOR { + let ConstructorMeta { constructor, .. } = match T::constructor() { Some(c) => c, None => { PhpException::default("You cannot instantiate this class from PHP.".into()) @@ -211,7 +211,7 @@ impl ClassBuilder { self.method( { let mut func = FunctionBuilder::new("__construct", constructor::); - if let Some(ConstructorMeta { build_fn, .. }) = T::CONSTRUCTOR { + if let Some(ConstructorMeta { build_fn, .. }) = T::constructor() { func = build_fn(func); } func.build().expect("Failed to build constructor function") diff --git a/src/builders/mod.rs b/src/builders/mod.rs index 66ad38942b..4f4b6666e5 100644 --- a/src/builders/mod.rs +++ b/src/builders/mod.rs @@ -7,4 +7,4 @@ mod module; pub use class::ClassBuilder; pub use function::FunctionBuilder; -pub use module::ModuleBuilder; +pub use module::{ModuleBuilder, ModuleStartup}; diff --git a/src/builders/module.rs b/src/builders/module.rs index ee2b219fdd..c0e8341b02 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -1,11 +1,15 @@ use crate::{ + builders::ClassBuilder, + class::RegisteredClass, + constant::IntoConst, error::Result, ffi::{ext_php_rs_php_build_id, ZEND_MODULE_API_NO}, + flags::MethodFlags, zend::{FunctionEntry, ModuleEntry}, PHP_DEBUG, PHP_ZTS, }; -use std::{ffi::CString, mem, ptr}; +use std::{ffi::CString, fmt::Debug, mem, ptr}; /// Builds a Zend module extension to be registered with PHP. Must be called /// from within an external function called `get_module`, returning a mutable @@ -34,12 +38,14 @@ use std::{ffi::CString, mem, ptr}; /// .into_raw() /// } /// ``` -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct ModuleBuilder { name: String, version: String, module: ModuleEntry, functions: Vec, + constants: Vec<(String, Box)>, + classes: Vec, } impl ModuleBuilder { @@ -83,6 +89,8 @@ impl ModuleBuilder { build_id: unsafe { ext_php_rs_php_build_id() }, }, functions: vec![], + constants: vec![], + classes: vec![], } } @@ -161,17 +169,75 @@ impl ModuleBuilder { self } + /// Adds a constant to the extension. + /// + /// # Arguments + /// + /// * `const_` - Tuple containing the name and value of the constant. This + /// is a tuple to support the [`wrap_constant`] macro. + /// + /// [`wrap_constant`]: crate::wrap_constant + pub fn constant(mut self, const_: (&str, impl IntoConst + Send + 'static)) -> Self { + let (name, val) = const_; + self.constants + .push((name.into(), Box::new(val) as Box)); + self + } + + pub fn class(mut self) -> Self { + self.classes.push(|| { + let mut builder = ClassBuilder::new(T::CLASS_NAME); + for method in T::method_builders() { + builder = builder.method(method.build().expect(""), MethodFlags::Public); + } + if let Some(modifier) = T::BUILDER_MODIFIER { + builder = modifier(builder); + } + let ce = builder + .object_override::() + .build() + .expect("Failed to build class"); + T::get_metadata().set_ce(ce); + }); + self + } + /// Builds the extension and returns a `ModuleEntry`. /// /// Returns a result containing the module entry if successful. - pub fn build(mut self) -> Result { + pub fn build(mut self) -> Result<(ModuleEntry, ModuleStartup)> { self.functions.push(FunctionEntry::end()); self.module.functions = Box::into_raw(self.functions.into_boxed_slice()) as *const FunctionEntry; self.module.name = CString::new(self.name)?.into_raw(); self.module.version = CString::new(self.version)?.into_raw(); - Ok(self.module) + let startup = ModuleStartup { + constants: self.constants, + classes: self.classes, + }; + Ok((self.module, startup)) + } +} + +/// Artifacts from the [`ModuleBuilder`] that should be revisited inside the +/// extension startup function. +pub struct ModuleStartup { + constants: Vec<(String, Box)>, + classes: Vec, +} + +impl ModuleStartup { + /// Completes startup of the module. Should only be called inside the module + /// startup function. + pub fn startup(self, _ty: i32, mod_num: i32) -> Result<()> { + for (name, val) in self.constants { + val.register_constant(&name, mod_num)?; + } + for class in self.classes { + class() + } + Ok(()) } } diff --git a/src/class.rs b/src/class.rs index c02a53fa18..ca6ed21a89 100644 --- a/src/class.rs +++ b/src/class.rs @@ -9,7 +9,7 @@ use std::{ use once_cell::sync::OnceCell; use crate::{ - builders::FunctionBuilder, + builders::{ClassBuilder, FunctionBuilder}, exception::PhpException, props::Property, zend::{ClassEntry, ExecuteData, ZendObjectHandlers}, @@ -21,8 +21,9 @@ pub trait RegisteredClass: Sized + 'static { /// PHP class name of the registered class. const CLASS_NAME: &'static str; - /// Optional class constructor. - const CONSTRUCTOR: Option> = None; + /// Function to be called when building the class. Allows user to modify the + /// class at runtime (add runtime constants etc). + const BUILDER_MODIFIER: Option ClassBuilder>; /// Returns a reference to the class metadata, which stores the class entry /// and handlers. @@ -43,6 +44,12 @@ pub trait RegisteredClass: Sized + 'static { /// through the [`ClassMetadata::get_properties`] function, which builds the /// hashmap one and stores it in memory. fn get_properties<'a>() -> HashMap<&'static str, Property<'a, Self>>; + + /// Returns the method builders required to build the class. + fn method_builders() -> Vec>; + + /// Returns the class constructor (if any). + fn constructor() -> Option>; } /// Stores metadata about a classes Rust constructor, including the function diff --git a/src/constant.rs b/src/constant.rs index 03b5043226..983811d6ff 100644 --- a/src/constant.rs +++ b/src/constant.rs @@ -1,6 +1,7 @@ //! Types and traits for registering constants in PHP. use std::ffi::CString; +use std::fmt::Debug; use super::flags::GlobalConstantFlags; use crate::error::Result; @@ -10,7 +11,7 @@ use crate::ffi::{ }; /// Implemented on types which can be registered as a constant in PHP. -pub trait IntoConst: Sized { +pub trait IntoConst: Debug { /// Registers a global module constant in PHP, with the value as the content /// of self. This function _must_ be called in the module startup /// function, which is called after the module is initialized. The diff --git a/src/internal/class.rs b/src/internal/class.rs new file mode 100644 index 0000000000..376efb93c9 --- /dev/null +++ b/src/internal/class.rs @@ -0,0 +1,47 @@ +use std::{collections::HashMap, marker::PhantomData}; + +use crate::{ + builders::FunctionBuilder, + class::{ConstructorMeta, RegisteredClass}, + props::Property, +}; + +/// Collector used to collect methods for PHP classes. +pub struct PhpClassImplCollector(PhantomData); + +impl Default for PhpClassImplCollector { + #[inline] + fn default() -> Self { + Self(PhantomData) + } +} + +pub trait PhpClassImpl { + fn get_methods(self) -> Vec>; + fn get_method_props<'a>(self) -> HashMap<&'static str, Property<'a, T>>; + fn get_constructor(self) -> Option>; +} + +// Default implementation for classes without an `impl` block. Classes that do +// have an `impl` block will override this by implementing `PhpClassImpl` for +// `PhpClassImplCollector` (note the missing reference). This is +// `dtolnay` specialisation: https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md +impl PhpClassImpl for &'_ PhpClassImplCollector { + #[inline] + fn get_methods(self) -> Vec> { + println!("&get_methods"); + Default::default() + } + + #[inline] + fn get_method_props<'a>(self) -> HashMap<&'static str, Property<'a, T>> { + println!("&get_method_props"); + Default::default() + } + + #[inline] + fn get_constructor(self) -> Option> { + println!("&get_constructor"); + Default::default() + } +} diff --git a/src/internal/function.rs b/src/internal/function.rs new file mode 100644 index 0000000000..7c0cefbe06 --- /dev/null +++ b/src/internal/function.rs @@ -0,0 +1,8 @@ +use crate::{error::Result, zend::FunctionEntry}; + +/// Implemented on ZSTs that represent PHP functions. +pub trait PhpFunction { + /// Function used to 'build' the PHP function, returning a [`FunctionEntry`] + /// to pass to the PHP interpreter. + const FUNCTION_ENTRY: fn() -> Result; +} diff --git a/src/internal.rs b/src/internal/mod.rs similarity index 92% rename from src/internal.rs rename to src/internal/mod.rs index 44d9af34d8..559036fc17 100644 --- a/src/internal.rs +++ b/src/internal/mod.rs @@ -1,5 +1,8 @@ //! Internal, public functions that are called from downstream extensions. +pub mod class; +pub mod function; + /// Called by startup functions registered with the [`#[php_startup]`] macro. /// Initializes all classes that are defined by ext-php-rs (i.e. `Closure`). /// diff --git a/src/lib.rs b/src/lib.rs index ee1d68fc80..c17a9c8ba0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,6 +53,8 @@ pub mod prelude { pub use crate::php_println; pub use crate::php_startup; pub use crate::types::ZendCallable; + pub use crate::wrap_constant; + pub use crate::wrap_function; pub use crate::ZvalConvert; } @@ -712,3 +714,5 @@ pub use ext_php_rs_derive::ZvalConvert; /// The `vectorcall` ABI is currently only supported on Windows with nightly /// Rust and the `abi_vectorcall` feature enabled. pub use ext_php_rs_derive::zend_fastcall; + +pub use ext_php_rs_derive::wrap_function; diff --git a/src/macros.rs b/src/macros.rs index 47f255bed9..6387652221 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -408,5 +408,12 @@ macro_rules! php_println { }; } +#[macro_export] +macro_rules! wrap_constant { + ($name:ident) => { + (stringify!($name), $name) + }; +} + pub(crate) use into_zval; pub(crate) use try_from_zval; From 3388f5fc44a3511dd80cad55ba69b7dc877344cf Mon Sep 17 00:00:00 2001 From: David Cole Date: Sun, 3 Apr 2022 20:43:05 +1200 Subject: [PATCH 02/16] Rewrite `#[php_function]` attribute --- crates/macros/src/function.rs | 680 ++++++++++++++-------------------- src/binary.rs | 1 + src/convert.rs | 6 + src/macros.rs | 1 + src/types/array.rs | 3 + src/types/class_object.rs | 2 + src/types/long.rs | 1 + src/types/object.rs | 2 + src/types/string.rs | 1 + src/types/zval.rs | 1 + 10 files changed, 298 insertions(+), 400 deletions(-) diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index 5a389f1fe2..24d8f6a4dd 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -1,464 +1,344 @@ -use std::collections::HashMap; +use std::{borrow::Cow, collections::HashMap}; -use crate::helpers::get_docs; -use crate::{syn_ext::DropLifetimes, STATE}; -use anyhow::{anyhow, bail, Result}; use darling::{FromMeta, ToTokens}; -use proc_macro2::{Ident, Literal, Span, TokenStream}; +use proc_macro2::{Span, TokenStream}; use quote::quote; -use syn::{ - punctuated::Punctuated, AttributeArgs, FnArg, GenericArgument, ItemFn, Lit, PathArguments, - ReturnType, Signature, Token, Type, TypePath, -}; +use syn::{spanned::Spanned, AttributeArgs, FnArg, Ident, ItemFn, Lit, PatType, Type}; -#[derive(Default, Debug, FromMeta)] +use crate::{bail, syn_ext::DropLifetimes, Result}; + +#[derive(Debug, Default, FromMeta)] #[darling(default)] -pub struct AttrArgs { - optional: Option, - ignore_module: bool, - defaults: HashMap, +pub struct FnArgs { + /// The name of the function. name: Option, + /// The first optional argument of the function signature. + optional: Option, + /// Default values for optional arguments. + defaults: HashMap, } -#[derive(Debug, Clone)] -pub struct Arg { - pub name: String, - pub ty: String, - pub nullable: bool, - pub default: Option, - pub as_ref: bool, -} +pub fn wrap(input: syn::Path) -> Result { + let func_name = match input.get_ident() { + Some(ident) => ident, + None => bail!(input => "Pass a PHP function name into `wrap_function!()`."), + }; + let builder_func = Ident::new(&format!("_internal_{}", func_name), Span::call_site()); + let err = format!("Failed to build function `{}`.", func_name); -#[derive(Debug, Clone)] -pub struct Function { - pub name: String, - pub docs: Vec, - pub ident: String, - pub args: Vec, - pub optional: Option, - pub output: Option<(String, bool)>, + Ok(quote! {{ + (<#builder_func as ::ext_php_rs::internal::function::PhpFunction>::FUNCTION_ENTRY)() + .expect(#err) + }}) } -pub fn parser(args: AttributeArgs, input: ItemFn) -> Result<(TokenStream, Function)> { - let attr_args = match AttrArgs::from_list(&args) { - Ok(args) => args, - Err(e) => bail!("Unable to parse attribute arguments: {:?}", e), +pub fn parser(opts: AttributeArgs, input: ItemFn) -> Result { + let opts = match FnArgs::from_list(&opts) { + Ok(opts) => opts, + Err(e) => bail!("Failed to parse attribute options: {:?}", e), }; - - let ItemFn { sig, .. } = &input; - let Signature { - ident, - output, - inputs, - .. - } = &sig; - - let internal_ident = Ident::new(&format!("_internal_php_{ident}"), Span::call_site()); - 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, - "e! { return; }, - ParserType::Function, - )?; - let arg_accessors = build_arg_accessors(&args); - - let return_type = get_return_type(output)?; - - let func = quote! { - #input - - ::ext_php_rs::zend_fastcall! { - #[doc(hidden)] - pub extern fn #internal_ident(ex: &mut ::ext_php_rs::zend::ExecuteData, retval: &mut ::ext_php_rs::types::Zval) { - use ::ext_php_rs::convert::IntoZval; - - #(#arg_definitions)* - #arg_parser - - let result = #ident(#(#arg_accessors, )*); - - if let Err(e) = result.set_zval(retval, false) { - let e: ::ext_php_rs::exception::PhpException = e.into(); - e.throw().expect("Failed to throw exception"); - } - } - } - }; - - let mut state = STATE.lock(); - - if state.built_module && !attr_args.ignore_module { - bail!("The `#[php_module]` macro must be called last to ensure functions are registered. To ignore this error, pass the `ignore_module` option into this attribute invocation: `#[php_function(ignore_module)]`"); + let args = Args::parse_from_fnargs(input.sig.inputs.iter(), &opts.defaults)?; + if let Some(ReceiverArg { span, .. }) = args.receiver { + bail!(span => "Receiver arguments are invalid on PHP functions. See `#[php_impl]`."); } + let func = Function::parse(opts.name.as_deref(), &input, args)?; + let function_impl = func.php_function_impl(opts.optional.as_ref())?; - let function = Function { - name: attr_args.name.unwrap_or_else(|| ident.to_string()), - docs: get_docs(&input.attrs), - ident: internal_ident.to_string(), - args, - optional, - output: return_type, - }; - - state.functions.push(function.clone()); - - Ok((func, function)) + Ok(quote! { + #input + #function_impl + }) } -fn build_args( - inputs: &Punctuated, - defaults: &HashMap, -) -> Result> { - inputs - .iter() - .map(|arg| match arg { - FnArg::Receiver(_) => bail!( - "`self` is not permitted in PHP functions. See the `#[php_method]` attribute." - ), - FnArg::Typed(ty) => { - let name = match &*ty.pat { - syn::Pat::Ident(pat) => pat.ident.to_string(), - _ => bail!("Invalid parameter type."), - }; - Arg::from_type(name.clone(), &ty.ty, defaults.get(&name), false) - .ok_or_else(|| anyhow!("Invalid parameter type for parameter `{}`.", name)) - } - }) - .collect::>>() +struct Function<'a> { + name: Cow<'a, Ident>, + args: Args<'a>, + output: Option<&'a Type>, } -fn build_arg_definitions(args: &[Arg]) -> Vec { - args.iter() - .map(|ty| { - let ident = ty.get_name_ident(); - let definition = ty.get_arg_definition(); - quote! { - let mut #ident = #definition; - } +impl<'a> Function<'a> { + fn parse(name: Option<&str>, func: &'a ItemFn, args: Args<'a>) -> Result { + Ok(Self { + name: match name { + Some(name) => Cow::Owned(Ident::new(name, Span::call_site())), + None => Cow::Borrowed(&func.sig.ident), + }, + args, + output: match &func.sig.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, ty) => Some(&**ty), + }, }) - .collect() -} - -pub fn find_optional_parameter<'a>( - args: impl DoubleEndedIterator, - optional: Option, -) -> Option { - if optional.is_some() { - return optional; } - let mut optional = None; - - for arg in args.rev() { - if arg.nullable { - optional.replace(arg.name.clone()); - } else { - break; - } + fn internal_ident(&self) -> Ident { + Ident::new(&format!("_internal_{}", &self.name), Span::call_site()) } - optional -} - -pub enum ParserType { - Function, - Method, - StaticMethod, -} - -pub fn build_arg_parser<'a>( - args: impl Iterator, - optional: &Option, - ret: &TokenStream, - ty: ParserType, -) -> Result { - let mut rest_optional = false; - - let args = args - .map(|arg| { - let name = arg.get_name_ident(); - let prelude = optional.as_ref().and_then(|opt| if *opt == arg.name { - rest_optional = true; - Some(quote! { .not_required() }) - } else { - None - }); + fn php_function_impl(&self, optional: Option<&Ident>) -> Result { + let name = &self.name; + let name_str = self.name.to_string(); + let internal_ident = self.internal_ident(); + let (required, not_required) = self.args.split_args(optional); + + // `handler` impl + let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect(); + let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect(); + let arg_declerations = self + .args + .typed + .iter() + .map(TypedArg::arg_decleration) + .collect::>>()?; + let arg_accessors = self.args.typed.iter().map(TypedArg::accessor); - if rest_optional && !arg.nullable && arg.default.is_none() { - bail!( - "Parameter `{}` must be a variant of `Option` or have a default value as it is optional.", - arg.name + // `entry` impl + let required_args = required + .iter() + .map(TypedArg::arg_builder) + .collect::>>()?; + let not_required_args = not_required + .iter() + .map(TypedArg::arg_builder) + .collect::>>()?; + let output = self.output.as_ref().map(|output| { + quote! { + .returns( + <#output as ::ext_php_rs::convert::IntoZval>::TYPE, + false, + <#output as ::ext_php_rs::convert::IntoZval>::NULLABLE, ) - } else { - Ok(quote! { - #prelude - .arg(&mut #name) - }) } - }) - .collect::>>()?; - let (parser, this) = match ty { - ParserType::Function | ParserType::StaticMethod => { - (quote! { let parser = ex.parser(); }, None) - } - ParserType::Method => ( - quote! { let (parser, this) = ex.parser_method::(); }, - Some(quote! { - let this = match this { - Some(this) => this, - None => { - ::ext_php_rs::exception::PhpException::default("Failed to retrieve reference to `$this`".into()) - .throw() - .unwrap(); - return; - }, - }; - }), - ), - }; - - Ok(quote! { - #parser - let parser = parser - #(#args)* - .parse(); - - if parser.is_err() { - #ret - } + }); - #this - }) + Ok(quote! { + #[doc(hidden)] + #[allow(non_camel_case_types)] + struct #internal_ident; + + impl ::ext_php_rs::internal::function::PhpFunction for #internal_ident { + const FUNCTION_ENTRY: fn() -> ::ext_php_rs::error::Result< + ::ext_php_rs::zend::FunctionEntry + > = { + ::ext_php_rs::zend_fastcall! { + extern fn handler( + ex: &mut ::ext_php_rs::zend::ExecuteData, + retval: &mut ::ext_php_rs::types::Zval, + ) { + use ::ext_php_rs::convert::IntoZval; + + #(#arg_declerations)* + let parse = ex.parser() + #(.arg(&mut #required_arg_names))* + .not_required() + #(.arg(&mut #not_required_arg_names))* + .parse(); + if parse.is_err() { + return; + } + + let result = #name(#({#arg_accessors}),*); + if let Err(e) = result.set_zval(retval, false) { + let e: ::ext_php_rs::exception::PhpException = e.into(); + e.throw().expect("Failed to throw PHP exception."); + } + } + } + fn entry() -> ::ext_php_rs::error::Result< + ::ext_php_rs::zend::FunctionEntry + > { + ::ext_php_rs::builders::FunctionBuilder::new(#name_str, handler) + #(.arg(#required_args))* + .not_required() + #(.arg(#not_required_args))* + #output + .build() + } + entry + }; + } + }) + } } -fn build_arg_accessors(args: &[Arg]) -> Vec { - args.iter() - .map(|arg| arg.get_accessor("e! { return; })) - .collect() +#[derive(Debug)] +pub struct ReceiverArg { + pub mutable: bool, + pub span: Span, } -pub fn get_return_type(output_type: &ReturnType) -> Result> { - Ok(match output_type { - ReturnType::Default => None, - ReturnType::Type(_, ty) => { - Arg::from_type("".to_string(), ty, None, true).map(|arg| (arg.ty, arg.nullable)) - } - }) +#[derive(Debug)] +pub struct TypedArg<'a> { + pub name: &'a Ident, + pub ty: &'a Type, + pub nullable: bool, + pub default: Option<&'a Lit>, } -impl Arg { - pub fn new( - name: String, - ty: String, - nullable: bool, - default: Option, - as_ref: bool, - ) -> Self { - Self { - name, - ty, - nullable, - default, - as_ref, - } - } - - pub fn from_type( - name: String, - ty: &syn::Type, - default: Option<&Lit>, - is_return: bool, - ) -> Option { - let default = default.map(|lit| lit.to_token_stream().to_string()); - match ty { - Type::Path(TypePath { path, .. }) => { - let mut path = path.clone(); - let mut pass_by_ref = false; - path.drop_lifetimes(); +#[derive(Debug)] +pub struct Args<'a> { + pub receiver: Option, + pub typed: Vec>, +} - let seg = path.segments.last()?; - let result = Some(seg) - .filter(|seg| seg.ident == "Result") - .and_then(|seg| { - if let PathArguments::AngleBracketed(args) = &seg.arguments { - args.args - .iter() - .find(|arg| matches!(arg, GenericArgument::Type(_))) - .map(|ty| ty.to_token_stream().to_string()) - } else { - None - } +impl<'a> Args<'a> { + pub fn parse_from_fnargs( + args: impl Iterator, + defaults: &'a HashMap, + ) -> Result { + let mut result = Self { + receiver: None, + typed: vec![], + }; + for arg in args { + match arg { + FnArg::Receiver(receiver) => { + if receiver.reference.is_none() { + bail!(receiver => "PHP objects are heap-allocated and cannot be passed by value. Try using `&self` or `&mut self`."); + } else if result.receiver.is_some() { + bail!(receiver => "Too many receivers specified.") + } + result.receiver.replace(ReceiverArg { + mutable: receiver.mutability.is_some(), + span: receiver.span(), }); - - // For for types that are `Option<&mut T>` to turn them into `Option<&T>`, - // marking the Arg as as "passed by reference". - let option = Some(seg) - .filter(|seg| seg.ident == "Option") - .and_then(|seg| { - if let PathArguments::AngleBracketed(args) = &seg.arguments { - args.args - .iter() - .find(|arg| matches!(arg, GenericArgument::Type(_))) - .map(|ga| { - let new_ga = match ga { - GenericArgument::Type(ty) => { - let _rtype = match ty { - Type::Reference(r) => { - let mut new_ref = r.clone(); - new_ref.mutability = None; - pass_by_ref = true; - Type::Reference(new_ref) - } - _ => ty.clone(), - }; - GenericArgument::Type(_rtype) - } - _ => ga.clone(), - }; - new_ga.to_token_stream().to_string() - }) - } else { - None - } + } + FnArg::Typed(PatType { pat, ty, .. }) => { + let ident = match &**pat { + syn::Pat::Ident(syn::PatIdent { ident, .. }) => ident, + _ => bail!(pat => "Unsupported argument."), + }; + let nullable = type_is_nullable(&**ty)?; + result.typed.push(TypedArg { + name: ident, + ty: &**ty, + nullable, + default: defaults.get(ident), }); - - let stringified = match result { - Some(result) if is_return => result, - _ => match option { - Some(result) => result, - None => path.to_token_stream().to_string(), - }, - }; - - Some(Arg::new( - name, - stringified, - seg.ident == "Option" || default.is_some(), - default, - pass_by_ref, - )) - } - Type::Reference(ref_) => { - // Returning references is invalid, so let's just create our arg - Some(Arg::new( - name, - ref_.to_token_stream().to_string(), - false, - default, - ref_.mutability.is_some(), - )) + } } - _ => None, } + Ok(result) } - #[inline] - pub fn get_type_ident(&self) -> TokenStream { - let ty: Type = syn::parse_str(&self.ty).unwrap(); - quote! { - <#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE + /// Splits the typed arguments into two slices: + /// + /// 1. Required arguments. + /// 2. Non-required arguments. + /// + /// # Parameters + /// + /// * `optional` - The first optional argument. If [`None`], the optional + /// arguments will be from the first nullable argument after the last + /// non-nullable argument to the end of the arguments. + pub fn split_args(&self, optional: Option<&Ident>) -> (&[TypedArg<'a>], &[TypedArg<'a>]) { + let mut mid = None; + for (i, arg) in self.typed.iter().enumerate() { + if let Some(optional) = optional { + if optional == arg.name { + mid.replace(i); + } + } else if mid.is_none() && arg.nullable { + mid.replace(i); + } else if !arg.nullable { + mid.take(); + } + } + match mid { + Some(mid) => (&self.typed[..mid], &self.typed[mid..]), + None => (&self.typed[..], &self.typed[0..0]), } } +} - #[inline] - pub fn get_name_ident(&self) -> Ident { - Ident::new(&self.name, Span::call_site()) +impl<'a> TypedArg<'a> { + /// Returns a 'clean type' with the lifetimes removed. This allows the type + /// to be used outside of the original function context. + fn clean_ty(&self) -> Type { + let mut ty = self.ty.clone(); + ty.drop_lifetimes(); + ty } - /// Returns a [`TokenStream`] containing the line required to retrieve the - /// value from the argument. - pub fn get_accessor(&self, ret: &TokenStream) -> TokenStream { - let name = &self.name; - let name_ident = self.get_name_ident(); + /// Returns a token stream containing an argument decleration, where the + /// name of the variable holding the arg is the name of the argument. + fn arg_decleration(&self) -> Result { + let name = self.name; + let val = self.arg_builder()?; + Ok(quote! { + let mut #name = #val; + }) + } - if let Some(default) = self.default.as_ref() { - // `bool`s are not literals - need to use Ident. - let val = syn::parse_str::(default) - .map(|lit| lit.to_token_stream()) - .or_else(|_| Ident::from_string(default).map(|ident| ident.to_token_stream())) - .unwrap_or(quote! { Default::default() }); + /// Returns a token stream containing the `Arg` definition to be passed to + /// `ext-php-rs`. + fn arg_builder(&self) -> Result { + let name = self.name.to_string(); + let ty = self.clean_ty(); + let null = if self.nullable { + Some(quote! { .allow_null() }) + } else { + None + }; + let default = self.default.as_ref().map(|val| { + let val = val.to_token_stream().to_string(); + quote! { + .default(#val) + } + }); + Ok(quote! { + ::ext_php_rs::args::Arg::new(#name, <#ty as ::ext_php_rs::convert::FromZval>::TYPE) + #null + #default + }) + } - quote! { #name_ident.val().unwrap_or(#val.into()) } + /// Get the accessor used to access the value of the argument. + fn accessor(&self) -> TokenStream { + let name = self.name; + if let Some(default) = self.default { + quote! { + #name.val().unwrap_or(#default) + } } else if self.nullable { - quote! { #name_ident.val() } + // Originally I thought we could just use the below case for `null` options, as + // `val()` will return `Option>`, however, this isn't the case when + // the argument isn't given, as the underlying zval is null. + quote! { + #name.val() + } } else { quote! { - match #name_ident.val() { + match #name.val() { Some(val) => val, None => { ::ext_php_rs::exception::PhpException::default( - concat!("Invalid value given for argument `", #name, "`.").into() + concat!("Invalid value given for argument `", stringify!(#name), "`.").into() ) .throw() - .expect(concat!("Failed to throw exception: Invalid value given for argument `", #name, "`.")); - #ret + .expect("Failed to throw PHP exception."); + return; } } } } } - - /// Returns a [`TokenStream`] containing the line required to instantiate - /// the argument. - pub fn get_arg_definition(&self) -> TokenStream { - let name = &self.name; - let ty = self.get_type_ident(); - - let null = self.nullable.then(|| quote! { .allow_null() }); - let passed_by_ref = self.as_ref.then(|| quote! { .as_ref() }); - let default = self.default.as_ref().map(|val| { - quote! { - .default(#val) - } - }); - - quote! { - ::ext_php_rs::args::Arg::new(#name, #ty) #null #passed_by_ref #default - } - } } -impl Function { - #[inline] - pub fn get_name_ident(&self) -> Ident { - Ident::new(&self.ident, Span::call_site()) - } - - pub fn get_builder(&self) -> TokenStream { - let name = &self.name; - let name_ident = self.get_name_ident(); - let args = self - .args +/// Returns true of the given type is nullable in PHP. +// TODO(david): Eventually move to compile-time constants for this (similar to +// FromZval::NULLABLE). +pub fn type_is_nullable(ty: &Type) -> Result { + Ok(match ty { + syn::Type::Path(path) => path + .path + .segments .iter() - .map(|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 - } - }); - quote! { #prelude.arg(#def) } - }) - .collect::>(); - let output = self.output.as_ref().map(|(ty, nullable)| { - let ty: Type = syn::parse_str(ty).expect("failed to parse ty"); - - // TODO allow reference returns? - quote! { - .returns(<#ty as ::ext_php_rs::convert::IntoZval>::TYPE, false, #nullable) - } - }); - - quote! { - ::ext_php_rs::builders::FunctionBuilder::new(#name, #name_ident) - #(#args)* - #output - .build() - } - } + .last() + .map(|seg| seg.ident == "Option") + .unwrap_or(false), + syn::Type::Reference(_) => false, /* Reference cannot be nullable unless */ + // wrapped in `Option` (in that case it'd be a Path). + _ => bail!(ty => "Unsupported argument type."), + }) } diff --git a/src/binary.rs b/src/binary.rs index b652adff74..decb0e9703 100644 --- a/src/binary.rs +++ b/src/binary.rs @@ -70,6 +70,7 @@ impl TryFrom for Binary { impl IntoZval for Binary { const TYPE: DataType = DataType::String; + const NULLABLE: bool = false; fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { zv.set_binary(self.0); diff --git a/src/convert.rs b/src/convert.rs index e1e2169912..a48d2d86b5 100644 --- a/src/convert.rs +++ b/src/convert.rs @@ -119,6 +119,9 @@ pub trait IntoZval: Sized { /// The corresponding type of the implemented value in PHP. const TYPE: DataType; + /// Whether converting into a [`Zval`] may result in null. + const NULLABLE: bool; + /// Converts a Rust primitive type into a Zval. Returns a result containing /// the Zval if successful. /// @@ -145,6 +148,7 @@ pub trait IntoZval: Sized { impl IntoZval for () { const TYPE: DataType = DataType::Void; + const NULLABLE: bool = true; #[inline] fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { @@ -158,6 +162,7 @@ where T: IntoZval, { const TYPE: DataType = T::TYPE; + const NULLABLE: bool = true; #[inline] fn set_zval(self, zv: &mut Zval, persistent: bool) -> Result<()> { @@ -177,6 +182,7 @@ where E: Into, { const TYPE: DataType = T::TYPE; + const NULLABLE: bool = T::NULLABLE; fn set_zval(self, zv: &mut Zval, persistent: bool) -> Result<()> { match self { diff --git a/src/macros.rs b/src/macros.rs index 6387652221..07d60717e5 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -329,6 +329,7 @@ macro_rules! into_zval { impl $crate::convert::IntoZval for $type { const TYPE: $crate::flags::DataType = $crate::flags::DataType::$dt; + const NULLABLE: bool = false; fn set_zval(self, zv: &mut $crate::types::Zval, _: bool) -> $crate::error::Result<()> { zv.$fn(self); diff --git a/src/types/array.rs b/src/types/array.rs index c40b8f3993..da9c64bf6b 100644 --- a/src/types/array.rs +++ b/src/types/array.rs @@ -752,6 +752,7 @@ impl Clone for ZBox { impl IntoZval for ZBox { const TYPE: DataType = DataType::Array; + const NULLABLE: bool = false; fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { zv.set_hashtable(self); @@ -817,6 +818,7 @@ where V: IntoZval, { const TYPE: DataType = DataType::Array; + const NULLABLE: bool = false; fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { let arr = self.try_into()?; @@ -881,6 +883,7 @@ where T: IntoZval, { const TYPE: DataType = DataType::Array; + const NULLABLE: bool = false; fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { let arr = self.try_into()?; diff --git a/src/types/class_object.rs b/src/types/class_object.rs index e9b116529b..e98e1de1ec 100644 --- a/src/types/class_object.rs +++ b/src/types/class_object.rs @@ -270,6 +270,7 @@ impl Clone for ZBox> { impl IntoZval for ZBox> { const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME)); + const NULLABLE: bool = false; fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { let obj = self.into_raw(); @@ -280,6 +281,7 @@ impl IntoZval for ZBox> { impl IntoZval for &mut ZendClassObject { const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME)); + const NULLABLE: bool = false; #[inline] fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { diff --git a/src/types/long.rs b/src/types/long.rs index 13cbb9b62c..cc056b26ce 100644 --- a/src/types/long.rs +++ b/src/types/long.rs @@ -41,6 +41,7 @@ macro_rules! try_into_zval_int { impl IntoZval for $type { const TYPE: DataType = DataType::Long; + const NULLABLE: bool = false; fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { let val: ZendLong = self.try_into().map_err(|_| Error::IntegerOverflow)?; diff --git a/src/types/object.rs b/src/types/object.rs index ca8e526ebe..b87e7942fd 100644 --- a/src/types/object.rs +++ b/src/types/object.rs @@ -344,6 +344,7 @@ impl<'a> FromZvalMut<'a> for &'a mut ZendObject { impl IntoZval for ZBox { const TYPE: DataType = DataType::Object(None); + const NULLABLE: bool = false; #[inline] fn set_zval(mut self, zv: &mut Zval, _: bool) -> Result<()> { @@ -358,6 +359,7 @@ impl IntoZval for ZBox { impl<'a> IntoZval for &'a mut ZendObject { const TYPE: DataType = DataType::Object(None); + const NULLABLE: bool = false; #[inline] fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { diff --git a/src/types/string.rs b/src/types/string.rs index efa5a68974..84d93a164b 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -434,6 +434,7 @@ macro_rules! try_into_zval_str { impl IntoZval for $type { const TYPE: DataType = DataType::String; + const NULLABLE: bool = false; fn set_zval(self, zv: &mut Zval, persistent: bool) -> Result<()> { zv.set_string(&self, persistent) diff --git a/src/types/zval.rs b/src/types/zval.rs index 94b62aa985..b95831abb1 100644 --- a/src/types/zval.rs +++ b/src/types/zval.rs @@ -623,6 +623,7 @@ impl Default for Zval { impl IntoZval for Zval { const TYPE: DataType = DataType::Mixed; + const NULLABLE: bool = true; fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { *zv = self; From babbaff456b9cfb26a9aefb31421e6877d2db971 Mon Sep 17 00:00:00 2001 From: David Cole Date: Thu, 10 Nov 2022 16:19:47 +1300 Subject: [PATCH 03/16] many months ago --- Cargo.toml | 4 + crates/macros/src/class.rs | 466 ++++++++++--------------- crates/macros/src/constant.rs | 23 +- crates/macros/src/function.rs | 289 ++++++++++++---- crates/macros/src/impl_.rs | 476 +++++++++++++++----------- crates/macros/src/lib.rs | 102 +++--- crates/macros/src/method.rs | 465 +------------------------ crates/macros/src/module.rs | 376 ++------------------ crates/macros/src/old_class.rs | 378 ++++++++++++++++++++ crates/macros/src/old_impl.rs | 371 ++++++++++++++++++++ crates/macros/src/old_method.rs | 456 ++++++++++++++++++++++++ crates/macros/src/startup_function.rs | 326 ++++++++---------- examples/hello_world.rs | 154 +++++++++ php | 4 + src/builders/class.rs | 23 +- src/builders/module.rs | 15 +- src/class.rs | 16 +- src/internal/class.rs | 26 +- src/macros.rs | 1 + 19 files changed, 2345 insertions(+), 1626 deletions(-) create mode 100644 crates/macros/src/old_class.rs create mode 100644 crates/macros/src/old_impl.rs create mode 100644 crates/macros/src/old_method.rs create mode 100644 examples/hello_world.rs create mode 100755 php diff --git a/Cargo.toml b/Cargo.toml index f2af9623a4..e662617f6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,3 +45,7 @@ members = ["crates/macros", "crates/cli"] [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docs"] + +[[example]] +name = "hello_world" +crate-type = ["cdylib"] diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index c10a015429..b2225bfc49 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -1,329 +1,215 @@ -use std::collections::HashMap; +use crate::prelude::*; +use darling::FromMeta; +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use syn::AttributeArgs; -use crate::STATE; -use anyhow::{anyhow, bail, Context, Result}; -use darling::{FromMeta, ToTokens}; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use syn::parse::ParseStream; -use syn::{Attribute, AttributeArgs, Expr, Fields, FieldsNamed, ItemStruct, LitStr, Token}; - -#[derive(Debug, Default)] -pub struct Class { - pub class_name: String, - pub struct_path: String, - pub parent: Option, - pub interfaces: Vec, - pub docs: Vec, - pub methods: Vec, - pub constructor: Option, - pub constants: Vec, - pub properties: HashMap, - /// A function name called when creating the class entry. Given an instance - /// of `ClassBuilder` and must return it. - pub modifier: Option, - pub flags: Option, -} - -#[derive(Debug)] -pub enum ParsedAttribute { - Extends(Expr), - Implements(Expr), - Property(PropertyAttr), - Comment(String), -} - -#[derive(Default, Debug, FromMeta)] +#[derive(Debug, Default, FromMeta)] #[darling(default)] -pub struct AttrArgs { +pub struct StructArgs { + /// The name of the PHP class. Defaults to the same name as the struct. name: Option, - modifier: Option, - flags: Option, + /// A modifier function which should accept one argument, a `ClassBuilder`, + /// and return the same object. Allows the user to modify the class before + /// it is built. + modifier: Option, + /// An expression of `ClassFlags` to be applied to the class. + flags: Option, } -pub fn parser(args: AttributeArgs, mut input: ItemStruct) -> Result { - let args = AttrArgs::from_list(&args) - .map_err(|e| anyhow!("Unable to parse attribute arguments: {:?}", e))?; - - let mut parent = None; - let mut interfaces = vec![]; - let mut properties = HashMap::new(); - let mut comments = vec![]; - - input.attrs = { - let mut unused = vec![]; - for attr in input.attrs.into_iter() { - match parse_attribute(&attr)? { - Some(parsed) => match parsed { - ParsedAttribute::Extends(class) => { - parent = Some(class.to_token_stream().to_string()); - } - ParsedAttribute::Implements(class) => { - interfaces.push(class.to_token_stream().to_string()); - } - ParsedAttribute::Comment(comment) => { - comments.push(comment); - } - attr => bail!("Attribute `{:?}` is not valid for structs.", attr), - }, - None => unused.push(attr), - } - } - unused - }; - - if let Fields::Named(FieldsNamed { - brace_token: _, - named, - }) = &mut input.fields - { - for field in named.iter_mut() { - let mut docs = vec![]; - let mut attrs = vec![]; - attrs.append(&mut field.attrs); - - for attr in attrs.into_iter() { - let mut result_prop = None; - match parse_attribute(&attr)? { - Some(parsed) => match parsed { - ParsedAttribute::Property(prop) => { - let field_name = field - .ident - .as_ref() - .ok_or_else(|| anyhow!("Only named fields can be properties."))? - .to_string(); - let prop_name = prop.rename.unwrap_or_else(|| field_name.clone()); - result_prop = Some(( - prop_name, - Property::field( - field_name, - vec![], - prop.flags.map(|flags| flags.to_token_stream().to_string()), - ), - )); - } - ParsedAttribute::Comment(doc) => docs.push(doc), - _ => bail!("Attribute {:?} is not valid for struct fields.", attr), - }, - None => field.attrs.push(attr), - } +/// Sub-attributes which are parsed by this macro. Must be placed underneath the +/// main `#[php_class]` attribute. +#[derive(Debug, Default)] +struct ClassAttrs { + extends: Option, + implements: Vec, +} - if let Some(mut prop) = result_prop { - prop.1.docs.append(&mut docs); - properties.insert(prop.0, prop.1); +impl ClassAttrs { + fn parse(&mut self, attrs: &mut Vec) -> Result<()> { + let mut unparsed = vec![]; + unparsed.append(attrs); + for attr in unparsed { + if attr.path.is_ident("extends") { + if self.extends.is_some() { + bail!(attr => "Only one `#[extends]` attribute is valid per struct."); } + let extends: syn::Expr = match attr.parse_args() { + Ok(extends) => extends, + Err(_) => bail!(attr => "Invalid arguments passed to extends attribute."), + }; + self.extends = Some(extends); + } else if attr.path.is_ident("implements") { + let implements: syn::Expr = match attr.parse_args() { + Ok(extends) => extends, + Err(_) => bail!(attr => "Invalid arguments passed to implements attribute."), + }; + self.implements.push(implements); + } else { + attrs.push(attr); } } + Ok(()) } +} - let ItemStruct { ident, .. } = &input; - let class_name = args.name.unwrap_or_else(|| ident.to_string()); - let struct_path = ident.to_string(); - let flags = args.flags.map(|flags| flags.to_token_stream().to_string()); - let class = Class { - class_name, - struct_path, - parent, - interfaces, - docs: comments, - properties, - modifier: args.modifier, - flags, - ..Default::default() +pub fn parser(args: AttributeArgs, mut input: syn::ItemStruct) -> Result { + let ident = &input.ident; + let args = match StructArgs::from_list(&args) { + Ok(args) => args, + Err(e) => bail!("Failed to parse struct arguments: {:?}", e), }; + let mut class_attrs = ClassAttrs::default(); + class_attrs.parse(&mut input.attrs)?; - let mut state = STATE.lock(); - - if state.built_module { - bail!("The `#[php_module]` macro must be called last to ensure functions and classes are registered."); - } - - if state.startup_function.is_some() { - bail!("The `#[php_startup]` macro must be called after all the classes have been defined."); - } - - state.classes.insert(ident.to_string(), class); + let fields = match &mut input.fields { + syn::Fields::Named(fields) => parse_fields(fields.named.iter_mut())?, + _ => vec![], + }; + let (metadata, metadata_ident) = class_metadata(ident); + let class_impl = generate_registered_class_impl( + ident, + &metadata_ident, + args.name.as_deref(), + args.modifier.as_ref(), + class_attrs.extends.as_ref(), + &class_attrs.implements, + &fields, + args.flags.as_ref(), + ); Ok(quote! { #input - + #metadata + #class_impl ::ext_php_rs::class_derives!(#ident); }) } -#[derive(Debug)] -pub struct Property { - pub ty: PropertyType, - pub docs: Vec, - #[allow(dead_code)] - pub flags: Option, -} - -#[derive(Debug)] -pub enum PropertyType { - Field { - field_name: String, - }, - Method { - getter: Option, - setter: Option, - }, -} - -impl Property { - pub fn add_getter(&mut self, new_getter: String) -> Result<()> { - match &mut self.ty { - PropertyType::Field { .. } => bail!("Cannot add getter to field property."), - PropertyType::Method { getter, setter: _ } => match getter { - Some(getter) => bail!( - "Attempted to add getter `{}` to property that already has a getter `{}`.", - new_getter, - getter - ), - None => { - getter.replace(new_getter); - Ok(()) - } - }, - } - } - - pub fn add_setter(&mut self, new_setter: String) -> Result<()> { - match &mut self.ty { - PropertyType::Field { .. } => bail!("Cannot add setter to field property."), - PropertyType::Method { getter: _, setter } => match setter { - Some(getter) => bail!( - "Attempted to add setter `{}` to property that already has a setter `{}`.", - new_setter, - getter - ), - None => { - setter.replace(new_setter); - Ok(()) - } - }, - } - } - - pub fn field(field_name: String, docs: Vec, flags: Option) -> Self { - Self { - ty: PropertyType::Field { field_name }, - docs, - flags, - } +fn parse_fields<'a>( + fields: impl Iterator, +) -> Result> { + #[derive(Debug, Default, FromMeta)] + #[darling(default)] + struct FieldAttr { + rename: Option, } - pub fn method(docs: Vec, flags: Option) -> Self { - Self { - ty: PropertyType::Method { - getter: None, - setter: None, - }, - docs, - flags, - } - } - - pub fn as_prop_tuple(&self, name: &str) -> TokenStream { - match &self.ty { - PropertyType::Field { field_name } => { - let field_name = Ident::new(field_name, Span::call_site()); - quote! { - (#name, ::ext_php_rs::props::Property::field(|obj: &mut Self| &mut obj.#field_name)), - } - } - PropertyType::Method { getter, setter } => { - let getter = if let Some(getter) = getter { - let ident = Ident::new(getter, Span::call_site()); - quote! { Some(Self::#ident) } - } else { - quote! { None } - }; - let setter = if let Some(setter) = setter { - let ident = Ident::new(setter, Span::call_site()); - quote! { Some(Self::#ident) } - } else { - quote! { None } + let mut result = vec![]; + for field in fields { + let mut unparsed = vec![]; + unparsed.append(&mut field.attrs); + for attr in unparsed { + if attr.path.is_ident("prop") { + let meta = match attr.parse_meta() { + Ok(meta) => meta, + Err(_) => bail!(attr => "Failed to parse attribute arguments"), }; - quote! { - (#name, ::ext_php_rs::props::Property::method(#getter, #setter)), + let ident = field + .ident + .as_ref() + .expect("Named field struct should have ident."); + let field_name = match meta { + syn::Meta::List(_) => FieldAttr::from_meta(&meta).unwrap(), + _ => FieldAttr::default(), } + .rename + .unwrap_or_else(|| ident.to_string()); + result.push((field_name, ident)) + } else { + field.attrs.push(attr); } } } + Ok(result) } -#[derive(Debug, Default)] -pub struct PropertyAttr { - pub rename: Option, - pub flags: Option, +/// Returns a class metadata definition alongside the ident to access the +/// metadata. +fn class_metadata(ident: &syn::Ident) -> (TokenStream, syn::Ident) { + let meta_ident = format_ident!( + "__INTERNAL_{}_METADATA", + ident.to_string().to_ascii_uppercase() + ); + ( + quote! { + static #meta_ident: ::ext_php_rs::class::ClassMetadata<#ident> = ::ext_php_rs::class::ClassMetadata::new(); + }, + meta_ident, + ) } -impl syn::parse::Parse for PropertyAttr { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let mut this = Self::default(); - while !input.is_empty() { - let field = input.parse::()?.to_string(); - input.parse::()?; - - match field.as_str() { - "rename" => { - this.rename.replace(input.parse::()?.value()); - } - "flags" => { - this.flags.replace(input.parse::()?); - } - _ => return Err(input.error("invalid attribute field")), - } - - let _ = input.parse::(); +/// Generates an implementation of `RegisteredClass` for struct `ident`. +fn generate_registered_class_impl( + ident: &syn::Ident, + metadata_ident: &syn::Ident, + class_name: Option<&str>, + modifier: Option<&syn::Ident>, + extends: Option<&syn::Expr>, + implements: &[syn::Expr], + fields: &[(String, &syn::Ident)], + flags: Option<&syn::Expr>, +) -> TokenStream { + let ident_str = ident.to_string(); + let class_name = match class_name { + Some(class_name) => class_name, + None => &ident_str, + }; + let modifier = modifier.option_tokens(); + let extends = extends.option_tokens(); + let fields = fields.iter().map(|(name, ident)| { + quote! { + (#name, ::ext_php_rs::props::Property::field(|this: &mut Self| &mut this.#ident)) } + }); + let flags = match flags { + Some(flags) => flags.to_token_stream(), + None => quote! { ::ext_php_rs::flags::ClassFlags::empty() }.to_token_stream() + }; + quote! { + impl ::ext_php_rs::class::RegisteredClass for #ident { + const CLASS_NAME: &'static str = #class_name; + const BUILDER_MODIFIER: ::std::option::Option< + fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder + > = #modifier; + const EXTENDS: ::std::option::Option< + fn() -> &'static ::ext_php_rs::zend::ClassEntry + > = #extends; + const IMPLEMENTS: &'static [fn() -> &'static ::ext_php_rs::zend::ClassEntry] = &[ + #(#implements,)* + ]; + const FLAGS: ::ext_php_rs::flags::ClassFlags = #flags; + + #[inline] + fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { + &#metadata_ident + } - Ok(this) - } -} - -pub fn parse_attribute(attr: &Attribute) -> Result> { - let name = attr.path.to_token_stream().to_string(); - - Ok(match name.as_ref() { - "extends" => { - let meta: Expr = attr - .parse_args() - .map_err(|_| anyhow!("Unable to parse `#[{}]` attribute.", name))?; - Some(ParsedAttribute::Extends(meta)) - } - "implements" => { - let meta: Expr = attr - .parse_args() - .map_err(|_| anyhow!("Unable to parse `#[{}]` attribute.", name))?; - Some(ParsedAttribute::Implements(meta)) - } - "doc" => { - struct DocComment(pub String); + fn get_properties<'a>() -> ::std::collections::HashMap< + &'static str, ::ext_php_rs::props::Property<'a, Self> + > { + use ::std::iter::FromIterator; + ::std::collections::HashMap::from_iter([ + #(#fields,)* + ]) + } - impl syn::parse::Parse for DocComment { - fn parse(input: ParseStream) -> syn::Result { - input.parse::()?; - let comment: LitStr = input.parse()?; - Ok(Self(comment.value())) - } + #[inline] + fn method_builders() -> ::std::vec::Vec< + (::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags) + > { + ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_methods() } - let comment: DocComment = - syn::parse2(attr.tokens.clone()).with_context(|| "Failed to parse doc comment")?; - Some(ParsedAttribute::Comment(comment.0)) - } - "prop" | "property" => { - let attr = if attr.tokens.is_empty() { - PropertyAttr::default() - } else { - attr.parse_args() - .map_err(|e| anyhow!("Unable to parse `#[{}]` attribute: {}", name, e))? - }; + #[inline] + fn constructor() -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta> { + ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_constructor() + } - Some(ParsedAttribute::Property(attr)) + #[inline] + fn constants() -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn)] { + ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_constants() + } } - _ => None, - }) + } } diff --git a/crates/macros/src/constant.rs b/crates/macros/src/constant.rs index 3a9516c26b..1ae9644ff0 100644 --- a/crates/macros/src/constant.rs +++ b/crates/macros/src/constant.rs @@ -1,12 +1,9 @@ -use crate::helpers::get_docs; -use anyhow::{bail, Result}; +use anyhow::{Result}; use darling::ToTokens; use proc_macro2::TokenStream; use quote::quote; use syn::{Expr, ItemConst}; -use crate::STATE; - #[derive(Debug)] pub struct Constant { pub name: String, @@ -16,17 +13,17 @@ pub struct Constant { } pub fn parser(input: ItemConst) -> Result { - let mut state = STATE.lock(); + // let mut state = STATE.lock(); - if state.startup_function.is_some() { - bail!("Constants must be declared before you declare your startup function and module function."); - } + // if state.startup_function.is_some() { + // bail!("Constants must be declared before you declare your startup + // function and module function."); } - state.constants.push(Constant { - name: input.ident.to_string(), - docs: get_docs(&input.attrs), - value: input.expr.to_token_stream().to_string(), - }); + // state.constants.push(Constant { + // name: input.ident.to_string(), + // docs: get_docs(&input.attrs), + // value: input.expr.to_token_stream().to_string(), + // }); Ok(quote! { #[allow(dead_code)] diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index 24d8f6a4dd..167225a407 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -1,11 +1,12 @@ -use std::{borrow::Cow, collections::HashMap}; +use std::collections::HashMap; use darling::{FromMeta, ToTokens}; use proc_macro2::{Span, TokenStream}; -use quote::quote; +use quote::{format_ident, quote}; use syn::{spanned::Spanned, AttributeArgs, FnArg, Ident, ItemFn, Lit, PatType, Type}; -use crate::{bail, syn_ext::DropLifetimes, Result}; +use crate::prelude::*; +use crate::syn_ext::DropLifetimes; #[derive(Debug, Default, FromMeta)] #[darling(default)] @@ -37,12 +38,12 @@ pub fn parser(opts: AttributeArgs, input: ItemFn) -> Result { Ok(opts) => opts, Err(e) => bail!("Failed to parse attribute options: {:?}", e), }; - let args = Args::parse_from_fnargs(input.sig.inputs.iter(), &opts.defaults)?; + let args = Args::parse_from_fnargs(input.sig.inputs.iter(), opts.defaults)?; if let Some(ReceiverArg { span, .. }) = args.receiver { bail!(span => "Receiver arguments are invalid on PHP functions. See `#[php_impl]`."); } - let func = Function::parse(opts.name.as_deref(), &input, args)?; - let function_impl = func.php_function_impl(opts.optional.as_ref())?; + let func = Function::parse(&input.sig, opts.name, args, opts.optional)?; + let function_impl = func.php_function_impl()?; Ok(quote! { #input @@ -50,36 +51,77 @@ pub fn parser(opts: AttributeArgs, input: ItemFn) -> Result { }) } -struct Function<'a> { - name: Cow<'a, Ident>, - args: Args<'a>, - output: Option<&'a Type>, +#[derive(Debug)] +pub struct Function<'a> { + /// Identifier of the Rust function associated with the function. + pub ident: &'a Ident, + /// Name of the function in PHP. + pub name: String, + /// Function arguments. + pub args: Args<'a>, + /// Function outputs. + pub output: Option<&'a Type>, + /// The first optional argument of the function. + pub optional: Option, +} + +#[derive(Debug)] +pub enum CallType<'a> { + Function, + Method { + class: &'a syn::Path, + receiver: MethodReceiver, + }, +} + +/// Type of receiver on the method. +#[derive(Debug)] +pub enum MethodReceiver { + /// Static method - has no receiver. + Static, + /// Class method, takes `&self` or `&mut self`. + Class, + /// Class method, takes `&mut ZendClassObject`. + ZendClassObject, } impl<'a> Function<'a> { - fn parse(name: Option<&str>, func: &'a ItemFn, args: Args<'a>) -> Result { + /// Parse a function. + /// + /// # Parameters + /// + /// * `sig` - Function signature. + /// * `name` - Function name in PHP land. + /// * `args` - Function arguments. + /// * `optional` - The ident of the first optional argument. + pub fn parse( + sig: &'a syn::Signature, + name: Option, + args: Args<'a>, + optional: Option, + ) -> Result { Ok(Self { - name: match name { - Some(name) => Cow::Owned(Ident::new(name, Span::call_site())), - None => Cow::Borrowed(&func.sig.ident), - }, + ident: &sig.ident, + name: name.unwrap_or_else(|| sig.ident.to_string()), args, - output: match &func.sig.output { + output: match &sig.output { syn::ReturnType::Default => None, syn::ReturnType::Type(_, ty) => Some(&**ty), }, + optional, }) } - fn internal_ident(&self) -> Ident { - Ident::new(&format!("_internal_{}", &self.name), Span::call_site()) + /// Generates an internal identifier for the function. + pub fn internal_ident(&self) -> Ident { + format_ident!("_internal_{}", &self.ident) } - fn php_function_impl(&self, optional: Option<&Ident>) -> Result { + /// Generates the function builder for the function. + pub fn function_builder(&self, call_type: CallType) -> Result { + let ident = self.ident; let name = &self.name; - let name_str = self.name.to_string(); - let internal_ident = self.internal_ident(); - let (required, not_required) = self.args.split_args(optional); + let (required, not_required) = self.args.split_args(self.optional.as_ref()); // `handler` impl let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect(); @@ -90,7 +132,9 @@ impl<'a> Function<'a> { .iter() .map(TypedArg::arg_decleration) .collect::>>()?; - let arg_accessors = self.args.typed.iter().map(TypedArg::accessor); + let arg_accessors = self.args.typed.iter().map(|arg| { + arg.accessor(|e| quote! { #e.throw().expect("Failed to throw PHP exception.") }) + }); // `entry` impl let required_args = required @@ -111,6 +155,96 @@ impl<'a> Function<'a> { } }); + let result = match call_type { + CallType::Function => quote! { + let parse = ex.parser() + #(.arg(&mut #required_arg_names))* + .not_required() + #(.arg(&mut #not_required_arg_names))* + .parse(); + if parse.is_err() { + return; + } + + #ident(#({#arg_accessors}),*) + }, + CallType::Method { class, receiver } => { + let this = match receiver { + MethodReceiver::Static => quote! { + let parse = ex.parser(); + }, + MethodReceiver::ZendClassObject | MethodReceiver::Class => quote! { + let (parse, this) = ex.parser_method::<#class>(); + let this = match this { + Some(this) => this, + None => { + ::ext_php_rs::exception::PhpException::default("Failed to retrieve reference to `$this`".into()) + .throw() + .unwrap(); + return; + } + }; + }, + }; + let call = match receiver { + MethodReceiver::Static => { + quote! { #class::#ident(#({#arg_accessors}),*) } + } + MethodReceiver::Class => quote! { this.#ident(#({#arg_accessors}),*) }, + MethodReceiver::ZendClassObject => { + quote! { #class::#ident(this, #({#arg_accessors}),*) } + } + }; + quote! { + #this + let parse_result = parse + #(.arg(&mut #required_arg_names))* + .not_required() + #(.arg(&mut #not_required_arg_names))* + .parse(); + if parse_result.is_err() { + return; + } + + #call + } + } + }; + + Ok(quote! { + ::ext_php_rs::builders::FunctionBuilder::new(#name, { + ::ext_php_rs::zend_fastcall! { + extern fn handler( + ex: &mut ::ext_php_rs::zend::ExecuteData, + retval: &mut ::ext_php_rs::types::Zval, + ) { + use ::ext_php_rs::convert::IntoZval; + + #(#arg_declerations)* + let result = { + #result + }; + + if let Err(e) = result.set_zval(retval, false) { + let e: ::ext_php_rs::exception::PhpException = e.into(); + e.throw().expect("Failed to throw PHP exception."); + } + } + } + handler + }) + #(.arg(#required_args))* + .not_required() + #(.arg(#not_required_args))* + #output + }) + } + + /// Generates a struct and impl for the `PhpFunction` trait. + pub fn php_function_impl(&self) -> Result { + let internal_ident = self.internal_ident(); + let builder = self.function_builder(CallType::Function)?; + Ok(quote! { #[doc(hidden)] #[allow(non_camel_case_types)] @@ -120,42 +254,71 @@ impl<'a> Function<'a> { const FUNCTION_ENTRY: fn() -> ::ext_php_rs::error::Result< ::ext_php_rs::zend::FunctionEntry > = { - ::ext_php_rs::zend_fastcall! { - extern fn handler( - ex: &mut ::ext_php_rs::zend::ExecuteData, - retval: &mut ::ext_php_rs::types::Zval, - ) { - use ::ext_php_rs::convert::IntoZval; - - #(#arg_declerations)* - let parse = ex.parser() - #(.arg(&mut #required_arg_names))* - .not_required() - #(.arg(&mut #not_required_arg_names))* - .parse(); - if parse.is_err() { - return; - } - - let result = #name(#({#arg_accessors}),*); - if let Err(e) = result.set_zval(retval, false) { - let e: ::ext_php_rs::exception::PhpException = e.into(); - e.throw().expect("Failed to throw PHP exception."); - } - } - } fn entry() -> ::ext_php_rs::error::Result< ::ext_php_rs::zend::FunctionEntry > { - ::ext_php_rs::builders::FunctionBuilder::new(#name_str, handler) + #builder.build() + } + entry + }; + } + }) + } + + /// Returns a constructor metadata object for this function. This doesn't + /// check if the function is a constructor, however. + pub fn constructor_meta(&self, class: &syn::Path) -> Result { + let ident = self.ident; + let (required, not_required) = self.args.split_args(self.optional.as_ref()); + let required_args = required + .iter() + .map(TypedArg::arg_builder) + .collect::>>()?; + let not_required_args = not_required + .iter() + .map(TypedArg::arg_builder) + .collect::>>()?; + + let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect(); + let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect(); + let arg_declerations = self + .args + .typed + .iter() + .map(TypedArg::arg_decleration) + .collect::>>()?; + let arg_accessors = self.args.typed.iter().map(|arg| { + arg.accessor( + |e| quote! { return ::ext_php_rs::class::ConstructorResult::Exception(#e); }, + ) + }); + + Ok(quote! { + ::ext_php_rs::class::ConstructorMeta { + constructor: { + fn inner(ex: &mut ::ext_php_rs::zend::ExecuteData) -> ::ext_php_rs::class::ConstructorResult<#class> { + #(#arg_declerations)* + let parse = ex.parser() + #(.arg(&mut #required_arg_names))* + .not_required() + #(.arg(&mut #not_required_arg_names))* + .parse(); + if parse.is_err() { + return ::ext_php_rs::class::ConstructorResult::ArgError; + } + #class::#ident(#({#arg_accessors}),*).into() + } + inner + }, + build_fn: { + fn inner(func: ::ext_php_rs::builders::FunctionBuilder) -> ::ext_php_rs::builders::FunctionBuilder { + func #(.arg(#required_args))* .not_required() #(.arg(#not_required_args))* - #output - .build() } - entry - }; + inner + } } }) } @@ -172,7 +335,7 @@ pub struct TypedArg<'a> { pub name: &'a Ident, pub ty: &'a Type, pub nullable: bool, - pub default: Option<&'a Lit>, + pub default: Option, } #[derive(Debug)] @@ -184,7 +347,7 @@ pub struct Args<'a> { impl<'a> Args<'a> { pub fn parse_from_fnargs( args: impl Iterator, - defaults: &'a HashMap, + mut defaults: HashMap, ) -> Result { let mut result = Self { receiver: None, @@ -213,7 +376,7 @@ impl<'a> Args<'a> { name: ident, ty: &**ty, nullable, - default: defaults.get(ident), + default: defaults.remove(ident), }); } } @@ -294,9 +457,9 @@ impl<'a> TypedArg<'a> { } /// Get the accessor used to access the value of the argument. - fn accessor(&self) -> TokenStream { + fn accessor(&self, bail_fn: impl Fn(TokenStream) -> TokenStream) -> TokenStream { let name = self.name; - if let Some(default) = self.default { + if let Some(default) = &self.default { quote! { #name.val().unwrap_or(#default) } @@ -308,16 +471,16 @@ impl<'a> TypedArg<'a> { #name.val() } } else { + let bail = bail_fn(quote! { + ::ext_php_rs::exception::PhpException::default( + concat!("Invalid value given for argument `", stringify!(#name), "`.").into() + ) + }); quote! { match #name.val() { Some(val) => val, None => { - ::ext_php_rs::exception::PhpException::default( - concat!("Invalid value given for argument `", stringify!(#name), "`.").into() - ) - .throw() - .expect("Failed to throw PHP exception."); - return; + #bail; } } } diff --git a/crates/macros/src/impl_.rs b/crates/macros/src/impl_.rs index 49fb9dc3ea..2bad0edb63 100644 --- a/crates/macros/src/impl_.rs +++ b/crates/macros/src/impl_.rs @@ -1,31 +1,53 @@ -use anyhow::{anyhow, bail, Result}; -use darling::{FromMeta, ToTokens}; -use proc_macro2::TokenStream; -use quote::quote; use std::collections::HashMap; -use syn::{Attribute, AttributeArgs, ItemImpl, Lit, Meta, NestedMeta}; -use crate::helpers::get_docs; use crate::{ - class::{Property, PropertyAttr}, - constant::Constant, - method, + function::{Args, CallType, Function, MethodReceiver}, + prelude::*, }; +use darling::FromMeta; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{AttributeArgs, Ident, Lit}; -#[derive(Debug, Clone)] -pub enum Visibility { - Public, - Protected, - Private, +pub fn parser(args: AttributeArgs, mut input: syn::ItemImpl) -> Result { + let args = match ImplArgs::from_list(&args) { + Ok(args) => args, + Err(e) => bail!("Failed to parse impl attribute arguments: {:?}", e), + }; + let path = match &*input.self_ty { + syn::Type::Path(ty) => &ty.path, + _ => bail!(input.self_ty => "The `#[php_impl]` attribute is only valid on structs."), + }; + + let mut parsed = ParsedImpl::new(path, args.rename_methods); + parsed.parse(input.items.iter_mut())?; + + let php_class_impl = parsed.generate_php_class_impl()?; + Ok(quote::quote! { + #input + #php_class_impl + }) } -#[derive(Debug, Copy, Clone, FromMeta, Default)] +/// Attribute arguments for `impl` blocks. +#[derive(Debug, Default, FromMeta)] +#[darling(default)] +pub struct ImplArgs { + /// How the methods are renamed. + rename_methods: RenameRule, +} + +/// Different types of rename rules for methods. +#[derive(Debug, Copy, Clone, FromMeta)] pub enum RenameRule { + /// Methods won't be renamed. #[darling(rename = "none")] None, + /// Methods will be conveted to camelCase. #[darling(rename = "camelCase")] #[default] Camel, + /// Methods will be converted to snake_case. #[darling(rename = "snake_case")] Snake, } @@ -35,11 +57,10 @@ impl RenameRule { /// /// Magic methods are handled specially to make sure they're always cased /// correctly. - pub fn rename(&self, name: impl AsRef) -> String { - let name = name.as_ref(); + pub fn rename(&self, name: String) -> String { match self { - RenameRule::None => name.to_string(), - rule => match name { + RenameRule::None => name, + rule => match name.as_str() { "__construct" => "__construct".to_string(), "__destruct" => "__destruct".to_string(), "__call" => "__call".to_string(), @@ -67,231 +88,272 @@ impl RenameRule { } } +/// Arguments applied to methods. #[derive(Debug)] -pub enum ParsedAttribute { - Default(HashMap), - Optional(String), - Visibility(Visibility), - Rename(String), - Property { - prop_name: Option, - ty: PropAttrTy, - }, - Constructor, - This, - Abstract, +struct MethodArgs { + /// Method name. Only applies to PHP (not the Rust method name). + name: String, + /// The first optional argument of the function signature. + optional: Option, + /// Default values for optional arguments. + defaults: HashMap, + /// Visibility of the method (public, protected, private). + vis: MethodVis, + /// Method type. + ty: MethodTy, } -#[derive(Default, Debug, FromMeta)] -#[darling(default)] -pub struct AttrArgs { - rename_methods: Option, +/// Method visibilities. +#[derive(Debug)] +enum MethodVis { + Public, + Private, + Protected, } +/// Method types. #[derive(Debug)] -pub enum PropAttrTy { +enum MethodTy { + /// Regular PHP method. + Normal, + /// Constructor method. + Constructor, + /// Property getter method. Getter, + /// Property setter method. Setter, + /// Abstract method. + Abstract, } -pub fn parser(args: AttributeArgs, input: ItemImpl) -> Result { - let args = AttrArgs::from_list(&args) - .map_err(|e| anyhow!("Unable to parse attribute arguments: {:?}", e))?; - - let ItemImpl { self_ty, items, .. } = input; - let class_name = self_ty.to_token_stream().to_string(); +impl MethodArgs { + fn new(name: String) -> Self { + let ty = if name == "__construct" { + MethodTy::Constructor + } else { + MethodTy::Normal + }; + Self { + name, + optional: Default::default(), + defaults: Default::default(), + vis: MethodVis::Public, + ty, + } + } +} - if input.trait_.is_some() { - bail!("This macro cannot be used on trait implementations."); +impl MethodArgs { + fn parse(&mut self, attrs: &mut Vec) -> Result<()> { + let mut unparsed = vec![]; + unparsed.append(attrs); + for attr in unparsed { + if attr.path.is_ident("optional") { + if self.optional.is_some() { + bail!(attr => "Only one `#[optional]` attribute is valid per method."); + } + let optional = attr.parse_args().map_err( + |e| err!(attr => "Invalid arguments passed to `#[optional]` attribute. {}", e), + )?; + self.optional = Some(optional); + } else if attr.path.is_ident("defaults") { + let meta = attr + .parse_meta() + .map_err(|e| err!(attr => "Failed to parse metadata from attribute. {}", e))?; + let defaults = HashMap::from_meta(&meta).map_err( + |e| err!(attr => "Invalid arguments passed to `#[defaults]` attribute. {}", e), + )?; + self.defaults = defaults; + } else if attr.path.is_ident("public") { + self.vis = MethodVis::Public; + } else if attr.path.is_ident("protected") { + self.vis = MethodVis::Protected; + } else if attr.path.is_ident("private") { + self.vis = MethodVis::Private; + } else if attr.path.is_ident("rename") { + let lit: syn::Lit = attr.parse_args().map_err(|e| err!(attr => "Invalid arguments passed to the `#[rename]` attribute. {}", e))?; + match lit { + Lit::Str(name) => self.name = name.value(), + _ => bail!(attr => "Only strings are valid method names."), + }; + } else if attr.path.is_ident("getter") { + self.ty = MethodTy::Getter; + } else if attr.path.is_ident("setter") { + self.ty = MethodTy::Setter; + } else if attr.path.is_ident("constructor") { + self.ty = MethodTy::Constructor; + } else if attr.path.is_ident("abstract_method") { + self.ty = MethodTy::Abstract; + } else { + attrs.push(attr); + } + } + Ok(()) } +} - let mut state = crate::STATE.lock(); +#[derive(Debug)] +struct ParsedImpl<'a> { + path: &'a syn::Path, + rename: RenameRule, + functions: Vec, + constructor: Option>, + constants: Vec>, +} +#[derive(Debug)] +struct FnBuilder { + /// Tokens which represent the FunctionBuilder for this function. + pub builder: TokenStream, + /// The visibility of this method. + pub vis: MethodVis, + /// Whether this method is abstract. + pub r#abstract: bool, +} + +#[derive(Debug)] +struct Constant<'a> { + /// Name of the constant in PHP land. + name: String, + /// Identifier of the constant in Rust land. + ident: &'a syn::Ident, +} - if state.startup_function.is_some() { - bail!( - "Impls must be declared before you declare your startup function and module function." - ); +impl<'a> ParsedImpl<'a> { + /// Create a new, empty parsed impl block. + /// + /// # Parameters + /// + /// * `path` - Path of the type the `impl` block is for. + /// * `rename` - Rename rule for methods. + fn new(path: &'a syn::Path, rename: RenameRule) -> Self { + Self { + path, + rename, + functions: Default::default(), + constructor: Default::default(), + constants: Default::default(), + } } - let class = state.classes.get_mut(&class_name).ok_or_else(|| { - anyhow!( - "You must use `#[php_class]` on the struct before using this attribute on the impl." - ) - })?; + /// Parses an impl block from `items`, populating `self`. + fn parse(&mut self, items: impl Iterator) -> Result<()> { + for items in items { + match items { + syn::ImplItem::Const(c) => { + let mut name = None; + let mut unparsed = vec![]; + unparsed.append(&mut c.attrs); + for attr in unparsed { + if attr.path.is_ident("rename") { + let lit: syn::Lit = attr.parse_args().map_err(|e| err!(attr => "Invalid arguments passed to the `#[rename]` attribute. {}", e))?; + match lit { + Lit::Str(str) => name = Some(str.value()), + _ => bail!(attr => "Only strings are valid constant names."), + }; + } else { + c.attrs.push(attr); + } + } - let tokens = items - .into_iter() - .map(|item| { - Ok(match item { - syn::ImplItem::Const(constant) => { - class.constants.push(Constant { - name: constant.ident.to_string(), - // visibility: Visibility::Public, - docs: get_docs(&constant.attrs), - value: constant.expr.to_token_stream().to_string(), + self.constants.push(Constant { + name: name.unwrap_or_else(|| c.ident.to_string()), + ident: &c.ident, }); - - quote! { - #[allow(dead_code)] - #constant - } } syn::ImplItem::Method(method) => { - let parsed_method = - method::parser(&self_ty, method, args.rename_methods.unwrap_or_default())?; + let name = self.rename.rename(method.sig.ident.to_string()); + let mut opts = MethodArgs::new(name); + opts.parse(&mut method.attrs)?; - // TODO(david): How do we handle comments for getter/setter? Take the comments - // from the methods?? - if let Some((prop, ty)) = parsed_method.property { - let prop = class - .properties - .entry(prop) - .or_insert_with(|| Property::method(vec![], None)); - let ident = parsed_method.method.orig_ident.clone(); + let args = Args::parse_from_fnargs(method.sig.inputs.iter(), opts.defaults)?; + let func = Function::parse(&method.sig, Some(opts.name), args, opts.optional)?; - match ty { - PropAttrTy::Getter => prop.add_getter(ident)?, - PropAttrTy::Setter => prop.add_setter(ident)?, - } - } - if parsed_method.constructor { - if class.constructor.is_some() { - bail!("You cannot have two constructors on the same class."); + if matches!(opts.ty, MethodTy::Constructor) { + if self.constructor.replace(func).is_some() { + bail!(method => "Only one constructor can be provided per class."); } - class.constructor = Some(parsed_method.method); } else { - class.methods.push(parsed_method.method); + let builder = func.function_builder(CallType::Method { + class: self.path, + receiver: if func.args.receiver.is_some() { + MethodReceiver::Class + } else { + MethodReceiver::Static + }, + })?; + self.functions.push(FnBuilder { + builder, + vis: opts.vis, + r#abstract: matches!(opts.ty, MethodTy::Abstract), + }); } - parsed_method.tokens } - item => item.to_token_stream(), - }) - }) - .collect::>>()?; - - let output = quote! { - impl #self_ty { - #(#tokens)* + _ => {} + } } - }; + Ok(()) + } - Ok(output) -} + /// Generates an `impl PhpClassImpl for PhpClassImplCollector` + /// block. + fn generate_php_class_impl(&self) -> Result { + let path = &self.path; + let functions = &self.functions; + let constructor = match &self.constructor { + Some(func) => Some(func.constructor_meta(self.path)?), + None => None, + } + .option_tokens(); + let constants = self.constants.iter().map(|c| { + let name = &c.name; + let ident = c.ident; + quote! { + (#name, &#path::#ident) + } + }); -pub fn parse_attribute(attr: &Attribute) -> Result> { - let name = attr.path.to_token_stream().to_string(); - let meta = attr - .parse_meta() - .map_err(|_| anyhow!("Unable to parse attribute."))?; + Ok(quote! { + impl ::ext_php_rs::internal::class::PhpClassImpl<#path> + for ::ext_php_rs::internal::class::PhpClassImplCollector<#path> + { + fn get_methods(self) -> ::std::vec::Vec< + (::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags) + > { + vec![#(#functions),*] + } - Ok(Some(match name.as_ref() { - "defaults" => { - let defaults = HashMap::from_meta(&meta) - .map_err(|_| anyhow!("Unable to parse `#[default]` macro."))?; - ParsedAttribute::Default(defaults) - } - "optional" => { - let name = if let Meta::List(list) = meta { - if let Some(NestedMeta::Meta(meta)) = list.nested.first() { - Some(meta.to_token_stream().to_string()) - } else { - None + fn get_method_props<'a>(self) -> ::std::collections::HashMap<&'static str, ::ext_php_rs::props::Property<'a, #path>> { + todo!() } - } else { - None - } - .ok_or_else(|| anyhow!("Invalid argument given for `#[optional]` macro."))?; - ParsedAttribute::Optional(name) - } - "public" => ParsedAttribute::Visibility(Visibility::Public), - "protected" => ParsedAttribute::Visibility(Visibility::Protected), - "private" => ParsedAttribute::Visibility(Visibility::Private), - "abstract_method" => ParsedAttribute::Abstract, - "rename" => { - let ident = if let Meta::List(list) = meta { - if let Some(NestedMeta::Lit(lit)) = list.nested.first() { - String::from_value(lit).ok() - } else { - None + fn get_constructor(self) -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta<#path>> { + #constructor } - } else { - None - } - .ok_or_else(|| anyhow!("Invalid argument given for `#[rename] macro."))?; - ParsedAttribute::Rename(ident) - } - "getter" => { - let prop_name = if attr.tokens.is_empty() { - None - } else { - let parsed: PropertyAttr = attr - .parse_args() - .map_err(|e| anyhow!("Unable to parse `#[getter]` attribute: {}", e))?; - parsed.rename - }; - ParsedAttribute::Property { - prop_name, - ty: PropAttrTy::Getter, - } - } - "setter" => { - let prop_name = if attr.tokens.is_empty() { - None - } else { - let parsed: PropertyAttr = attr - .parse_args() - .map_err(|e| anyhow!("Unable to parse `#[setter]` attribute: {}", e))?; - parsed.rename - }; - ParsedAttribute::Property { - prop_name, - ty: PropAttrTy::Setter, + fn get_constants(self) -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn)] { + &[#(#constants),*] + } } - } - "constructor" => ParsedAttribute::Constructor, - "this" => ParsedAttribute::This, - _ => return Ok(None), - })) + }) + } } -#[cfg(test)] -mod tests { - use super::RenameRule; - - #[test] - fn test_rename_magic() { - for &(magic, expected) in &[ - ("__construct", "__construct"), - ("__destruct", "__destruct"), - ("__call", "__call"), - ("__call_static", "__callStatic"), - ("__get", "__get"), - ("__set", "__set"), - ("__isset", "__isset"), - ("__unset", "__unset"), - ("__sleep", "__sleep"), - ("__wakeup", "__wakeup"), - ("__serialize", "__serialize"), - ("__unserialize", "__unserialize"), - ("__to_string", "__toString"), - ("__invoke", "__invoke"), - ("__set_state", "__set_state"), - ("__clone", "__clone"), - ("__debug_info", "__debugInfo"), - ] { - assert_eq!(magic, RenameRule::None.rename(magic)); - assert_eq!(expected, RenameRule::Camel.rename(magic)); - assert_eq!(expected, RenameRule::Snake.rename(magic)); +impl quote::ToTokens for FnBuilder { + fn to_tokens(&self, tokens: &mut TokenStream) { + let builder = &self.builder; + // TODO(cole_d): allow more flags via attributes + let mut flags = vec![]; + flags.push(match self.vis { + MethodVis::Public => quote! { ::ext_php_rs::flags::MethodFlags::Public }, + MethodVis::Protected => quote! { ::ext_php_rs::flags::MethodFlags::Protected }, + MethodVis::Private => quote! { ::ext_php_rs::flags::MethodFlags::Private }, + }); + if self.r#abstract { + flags.push(quote! { ::ext_php_rs::flags::MethodFlags::Abstract }); } - } - - #[test] - fn test_rename_php_methods() { - let &(original, camel, snake) = &("get_name", "getName", "get_name"); - assert_eq!(original, RenameRule::None.rename(original)); - assert_eq!(camel, RenameRule::Camel.rename(original)); - assert_eq!(snake, RenameRule::Snake.rename(original)); + quote! { + (#builder, #(#flags)*) + } + .to_tokens(tokens); } } diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 2e6deaf8ab..d767e06462 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -3,7 +3,6 @@ mod constant; mod extern_; mod fastcall; mod function; -mod helpers; mod impl_; mod method; mod module; @@ -11,12 +10,6 @@ mod startup_function; mod syn_ext; mod zval; -use std::{ - collections::HashMap, - sync::{Mutex, MutexGuard}, -}; - -use constant::Constant; use proc_macro::TokenStream; use proc_macro2::Span; use syn::{ @@ -26,31 +19,6 @@ use syn::{ extern crate proc_macro; -#[derive(Default, Debug)] -struct State { - functions: Vec, - classes: HashMap, - constants: Vec, - startup_function: Option, - built_module: bool, -} - -lazy_static::lazy_static! { - pub(crate) static ref STATE: StateMutex = StateMutex::new(); -} - -struct StateMutex(Mutex); - -impl StateMutex { - pub fn new() -> Self { - Self(Mutex::new(Default::default())) - } - - pub fn lock(&self) -> MutexGuard { - self.0.lock().unwrap_or_else(|e| e.into_inner()) - } -} - #[proc_macro_attribute] pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream { let args = parse_macro_input!(args as AttributeArgs); @@ -58,7 +26,7 @@ pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream { match class::parser(args, input) { Ok(parsed) => parsed, - Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), + Err(e) => e.to_compile_error(), } .into() } @@ -69,8 +37,8 @@ pub fn php_function(args: TokenStream, input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemFn); match function::parser(args, input) { - Ok((parsed, _)) => parsed, - Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), + Ok(parsed) => parsed, + Err(e) => e.to_compile_error(), } .into() } @@ -79,19 +47,14 @@ pub fn php_function(args: TokenStream, input: TokenStream) -> TokenStream { pub fn php_module(_: TokenStream, input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemFn); - match module::parser(input) { - Ok(parsed) => parsed, - Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), - } - .into() + module::parser(input).into() } #[proc_macro_attribute] -pub fn php_startup(args: TokenStream, input: TokenStream) -> TokenStream { - let args = parse_macro_input!(args as AttributeArgs); +pub fn php_startup(_: TokenStream, input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemFn); - match startup_function::parser(Some(args), input) { + match startup_function::parser(input) { Ok(parsed) => parsed, Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), } @@ -105,7 +68,7 @@ pub fn php_impl(args: TokenStream, input: TokenStream) -> TokenStream { match impl_::parser(args, input) { Ok(parsed) => parsed, - Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), + Err(e) => e.to_compile_error(), } .into() } @@ -153,3 +116,54 @@ pub fn zend_fastcall(input: TokenStream) -> TokenStream { } .into() } + +#[proc_macro] +pub fn wrap_function(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as syn::Path); + + match function::wrap(input) { + Ok(parsed) => parsed, + Err(e) => e.to_compile_error(), + } + .into() +} + +macro_rules! err { + ($span:expr => $($msg:tt)*) => { + ::syn::Error::new(::syn::spanned::Spanned::span(&$span), format!($($msg)*)) + }; + ($($msg:tt)*) => { + ::syn::Error::new(::proc_macro2::Span::call_site(), format!($($msg)*)) + }; +} + +/// Bails out of a function with a syn error. +macro_rules! bail { + ($span:expr => $($msg:tt)*) => { + return Err($crate::err!($span => $($msg)*)) + }; + ($($msg:tt)*) => { + return Err($crate::err!($($msg)*)) + }; +} + +pub(crate) use bail; +pub(crate) use err; + +pub(crate) mod prelude { + pub(crate) trait OptionTokens { + fn option_tokens(&self) -> proc_macro2::TokenStream; + } + + impl OptionTokens for Option { + fn option_tokens(&self) -> proc_macro2::TokenStream { + match self { + Some(token) => quote::quote! { ::std::option::Option::Some(#token) }, + None => quote::quote! { ::std::option::Option::None }, + } + } + } + + pub(crate) use crate::{bail, err}; + pub(crate) type Result = std::result::Result; +} diff --git a/crates/macros/src/method.rs b/crates/macros/src/method.rs index 7eb8a34f1a..aef8e0587a 100644 --- a/crates/macros/src/method.rs +++ b/crates/macros/src/method.rs @@ -1,464 +1 @@ -use anyhow::{anyhow, bail, Result}; -use quote::ToTokens; -use std::collections::HashMap; -use syn::ReturnType; - -use crate::helpers::get_docs; -use crate::{ - function::{self, ParserType}, - impl_::{parse_attribute, ParsedAttribute, PropAttrTy, RenameRule, Visibility}, -}; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use syn::{punctuated::Punctuated, FnArg, ImplItemMethod, Lit, Pat, Token, Type}; - -#[derive(Debug, Clone)] -pub enum Arg { - Receiver(MethodType), - Typed(function::Arg), -} - -#[derive(Debug)] -pub struct AttrArgs { - pub defaults: HashMap, - pub optional: Option, - pub visibility: Visibility, -} - -#[derive(Debug, Clone)] -pub struct Method { - /// Method name - pub name: String, - /// extern "C" function ident - pub ident: String, - /// Rust internal function ident - pub orig_ident: String, - pub docs: Vec, - pub args: Vec, - pub optional: Option, - pub output: Option<(String, bool)>, - pub _static: bool, - pub _abstract: bool, - pub visibility: Visibility, -} - -pub struct ParsedMethod { - pub tokens: TokenStream, - pub method: Method, - pub property: Option<(String, PropAttrTy)>, - pub constructor: bool, -} - -#[derive(Debug, Clone, Copy)] -pub enum MethodType { - Receiver { mutable: bool }, - ReceiverClassObject, - Static, -} - -impl ParsedMethod { - pub fn new( - tokens: TokenStream, - method: Method, - property: Option<(String, PropAttrTy)>, - constructor: bool, - ) -> Self { - Self { - tokens, - method, - property, - constructor, - } - } -} - -pub fn parser( - struct_ty: &Type, - mut input: ImplItemMethod, - rename_rule: RenameRule, -) -> Result { - let mut defaults = HashMap::new(); - let mut optional = None; - let mut visibility = Visibility::Public; - let mut as_prop = None; - let mut identifier = None; - let mut is_abstract = false; - let mut is_constructor = false; - let docs = get_docs(&input.attrs); - - for attr in input.attrs.iter() { - if let Some(attr) = parse_attribute(attr)? { - match attr { - ParsedAttribute::Default(list) => defaults = list, - ParsedAttribute::Optional(name) => optional = Some(name), - ParsedAttribute::Visibility(vis) => visibility = vis, - ParsedAttribute::Abstract => is_abstract = true, - ParsedAttribute::Rename(ident) => identifier = Some(ident), - ParsedAttribute::Property { prop_name, ty } => { - if as_prop.is_some() { - bail!( - "Only one `#[getter]` and/or `#[setter]` attribute may be used per method." - ); - } - - let prop_name = prop_name.unwrap_or_else(|| { - input - .sig - .ident - .to_token_stream() - .to_string() - .trim_start_matches("get_") - .trim_start_matches("set_") - .to_string() - }); - as_prop = Some((prop_name, ty)) - } - ParsedAttribute::Constructor => is_constructor = true, - _ => bail!("Invalid attribute for method."), - } - } - } - - input.attrs.clear(); - - let ident = &input.sig.ident; - 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(struct_ty, &mut input.sig.inputs, &defaults)?; - let optional = function::find_optional_parameter( - args.iter().filter_map(|arg| match arg { - Arg::Typed(arg) => Some(arg), - _ => None, - }), - optional, - ); - let (arg_definitions, method_type) = build_arg_definitions(&args); - let arg_parser = build_arg_parser( - args.iter(), - &optional, - &bail, - match method_type { - MethodType::Static => ParserType::StaticMethod, - _ => ParserType::Method, - }, - )?; - let arg_accessors = build_arg_accessors(&args, &bail); - - let func = if is_constructor { - quote! { - #input - - #[doc(hidden)] - pub fn #internal_ident( - ex: &mut ::ext_php_rs::zend::ExecuteData - ) -> ::ext_php_rs::class::ConstructorResult { - use ::ext_php_rs::convert::IntoZval; - use ::ext_php_rs::class::ConstructorResult; - - #(#arg_definitions)* - #arg_parser - - Self::#ident(#(#arg_accessors,)*).into() - } - } - } else { - let this = match method_type { - MethodType::Receiver { .. } => quote! { this. }, - MethodType::ReceiverClassObject | MethodType::Static => quote! { Self:: }, - }; - - quote! { - #input - - ::ext_php_rs::zend_fastcall! { - #[doc(hidden)] - pub extern fn #internal_ident( - ex: &mut ::ext_php_rs::zend::ExecuteData, - retval: &mut ::ext_php_rs::types::Zval - ) { - use ::ext_php_rs::convert::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::exception::PhpException = e.into(); - e.throw().expect("Failed to throw exception"); - } - } - } - } - }; - - let method = Method { - name, - ident: internal_ident.to_string(), - orig_ident: ident.to_string(), - docs, - args, - optional, - output: get_return_type(struct_ty, &input.sig.output)?, - _static: matches!(method_type, MethodType::Static), - _abstract: is_abstract, - visibility, - }; - - Ok(ParsedMethod::new(func, method, as_prop, is_constructor)) -} - -pub fn get_return_type(self_ty: &Type, output_type: &ReturnType) -> Result> { - Ok(match output_type { - ReturnType::Default => None, - ReturnType::Type(_, ty) => { - let mut ty = ty.clone(); - replace_self(self_ty, &mut ty); - crate::function::Arg::from_type("".to_string(), &ty, None, true) - .map(|arg| (arg.ty, arg.nullable)) - } - }) -} - -/// Takes a type `ty` and replaces all instances of `Self` with the type -/// `self_ty`. -fn replace_self(self_ty: &Type, ty: &mut Type) { - match ty { - Type::Array(syn::TypeArray { elem, .. }) => replace_self(self_ty, elem), - Type::BareFn(syn::TypeBareFn { inputs, output, .. }) => { - for input in inputs { - replace_self(self_ty, &mut input.ty); - } - if let ReturnType::Type(_, ty) = output { - replace_self(self_ty, ty); - } - } - Type::Group(syn::TypeGroup { elem, .. }) => replace_self(self_ty, elem), - Type::Paren(syn::TypeParen { elem, .. }) => replace_self(self_ty, elem), - Type::Path(syn::TypePath { qself, path }) => { - if let Some(syn::QSelf { ty, .. }) = qself { - replace_self(self_ty, ty); - } - for seg in &mut path.segments { - if seg.ident == "Self" { - seg.ident = - Ident::new(&self_ty.to_token_stream().to_string(), Span::call_site()); - } - match &mut seg.arguments { - syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { - args, - .. - }) => { - for arg in args { - if let syn::GenericArgument::Type(ty) = arg { - replace_self(self_ty, ty); - } - } - } - syn::PathArguments::Parenthesized(syn::ParenthesizedGenericArguments { - inputs, - output, - .. - }) => { - for input in inputs { - replace_self(self_ty, input); - } - - if let ReturnType::Type(_, ty) = output { - replace_self(self_ty, ty); - } - } - _ => {} - } - } - } - Type::Ptr(syn::TypePtr { elem, .. }) => replace_self(self_ty, elem), - Type::Reference(syn::TypeReference { elem, .. }) => replace_self(self_ty, elem), - Type::Slice(syn::TypeSlice { elem, .. }) => replace_self(self_ty, elem), - Type::Tuple(syn::TypeTuple { elems, .. }) => { - for elem in elems { - replace_self(self_ty, elem); - } - } - _ => {} - } -} - -fn build_args( - struct_ty: &Type, - inputs: &mut Punctuated, - defaults: &HashMap, -) -> Result> { - inputs - .iter_mut() - .map(|arg| match arg { - FnArg::Receiver(receiver) => { - if receiver.reference.is_none() { - bail!("`self` parameter must be a reference."); - } - Ok(Arg::Receiver(MethodType::Receiver { - mutable: receiver.mutability.is_some(), - })) - } - FnArg::Typed(ty) => { - let mut this = false; - let attrs = std::mem::take(&mut ty.attrs); - for attr in attrs.into_iter() { - if let Some(attr) = parse_attribute(&attr)? { - match attr { - ParsedAttribute::This => this = true, - _ => bail!("Invalid attribute for argument."), - } - } - } - - if this { - Ok(Arg::Receiver(MethodType::ReceiverClassObject)) - } else { - let name = match &*ty.pat { - Pat::Ident(pat) => pat.ident.to_string(), - _ => bail!("Invalid parameter type."), - }; - let default = defaults.get(&name); - let mut ty = ty.ty.clone(); - replace_self(struct_ty, &mut ty); - - Ok(Arg::Typed( - crate::function::Arg::from_type(name.clone(), &ty, default, false) - .ok_or_else(|| anyhow!("Invalid parameter type for `{}`.", name))?, - )) - } - } - }) - .collect() -} - -fn build_arg_definitions(args: &[Arg]) -> (Vec, MethodType) { - let mut method_type = MethodType::Static; - - ( - args.iter() - .filter_map(|ty| match ty { - Arg::Receiver(t) => { - method_type = *t; - None - } - Arg::Typed(arg) => { - let ident = arg.get_name_ident(); - let definition = arg.get_arg_definition(); - Some(quote! { - let mut #ident = #definition; - }) - } - }) - .collect(), - method_type, - ) -} - -fn build_arg_parser<'a>( - args: impl Iterator, - optional: &Option, - ret: &TokenStream, - ty: ParserType, -) -> Result { - function::build_arg_parser( - args.filter_map(|arg| match arg { - Arg::Typed(arg) => Some(arg), - _ => None, - }), - optional, - ret, - ty, - ) -} - -fn build_arg_accessors(args: &[Arg], ret: &TokenStream) -> Vec { - args.iter() - .filter_map(|arg| match arg { - Arg::Typed(arg) => Some(arg.get_accessor(ret)), - Arg::Receiver(MethodType::ReceiverClassObject) => Some(quote! { this }), - _ => None, - }) - .collect() -} - -impl Method { - #[inline] - pub fn get_name_ident(&self) -> Ident { - 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.get_arg_definitions(); - let output = self.output.as_ref().map(|(ty, nullable)| { - let ty: Type = syn::parse_str(ty).unwrap(); - - // TODO allow reference returns? - quote! { - .returns(<#ty as ::ext_php_rs::convert::IntoZval>::TYPE, false, #nullable) - } - }); - - quote! { - ::ext_php_rs::builders::FunctionBuilder::new(#name, #class_path :: #name_ident) - #(#args)* - #output - .build() - } - } - - pub fn get_flags(&self) -> TokenStream { - let mut flags = vec![match self.visibility { - Visibility::Public => quote! { Public }, - Visibility::Protected => quote! { Protected }, - Visibility::Private => quote! { Private }, - }]; - - if self._static { - flags.push(quote! { Static }); - } - - if self._abstract { - flags.push(quote! { Abstract }); - } - - flags - .iter() - .map(|flag| quote! { ::ext_php_rs::flags::MethodFlags::#flag }) - .collect::>() - .to_token_stream() - } -} +// pub fn parser() -> Result {} diff --git a/crates/macros/src/module.rs b/crates/macros/src/module.rs index 2c118c056c..97f230c741 100644 --- a/crates/macros/src/module.rs +++ b/crates/macros/src/module.rs @@ -1,375 +1,49 @@ -use std::sync::MutexGuard; - -use anyhow::{anyhow, bail, Result}; -use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro2::{TokenStream}; use quote::quote; -use syn::{ItemFn, Signature, Type}; - -use crate::{ - class::{Class, Property}, - function::{Arg, Function}, - startup_function, State, STATE, -}; +use syn::{ItemFn, Signature}; -pub fn parser(input: ItemFn) -> Result { +pub fn parser(input: ItemFn) -> TokenStream { let ItemFn { sig, block, .. } = input; let Signature { output, inputs, .. } = sig; let stmts = &block.stmts; - let mut state = STATE.lock(); - - if state.built_module { - bail!("You may only define a module with the `#[php_module]` attribute once."); - } - - state.built_module = true; - - // Generate startup function if one hasn't already been tagged with the macro. - let startup_fn = if (!state.classes.is_empty() || !state.constants.is_empty()) - && state.startup_function.is_none() - { - drop(state); - - let parsed = syn::parse2(quote! { - fn php_module_startup() {} - }) - .map_err(|_| anyhow!("Unable to generate PHP module startup function."))?; - let startup = startup_function::parser(None, parsed)?; - - state = STATE.lock(); - Some(startup) - } else { - None - }; + quote! { + static __EXT_PHP_RS_MODULE_STARTUP: ::parking_lot::Mutex< + ::std::option::Option<::ext_php_rs::builders::ModuleStartup> + > = ::parking_lot::const_mutex(::std::option::Option::None); - let functions = state - .functions - .iter() - .map(|func| func.get_builder()) - .collect::>(); - let startup = state.startup_function.as_ref().map(|ident| { - let ident = Ident::new(ident, Span::call_site()); - quote! { - .startup_function(#ident) + #[doc(hidden)] + extern "C" fn __ext_php_rs_startup(ty: i32, mod_num: i32) -> i32 { + __EXT_PHP_RS_MODULE_STARTUP + .lock() + .take() + .expect("Module startup function has already been called.") + .startup(ty, mod_num) + .map(|_| 0) + .unwrap_or(1) } - }); - let registered_classes_impls = state - .classes - .values() - .map(generate_registered_class_impl) - .collect::>>()?; - let describe_fn = generate_stubs(&state); - - let result = quote! { - #(#registered_classes_impls)* - - #startup_fn #[doc(hidden)] #[no_mangle] - pub extern "C" fn get_module() -> *mut ::ext_php_rs::zend::ModuleEntry { + extern "C" fn get_module() -> *mut ::ext_php_rs::zend::ModuleEntry { + #[inline] fn internal(#inputs) #output { #(#stmts)* } - let mut builder = ::ext_php_rs::builders::ModuleBuilder::new( + let builder = internal(::ext_php_rs::builders::ModuleBuilder::new( env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION") - ) - #startup - #(.function(#functions.unwrap()))* - ; - - // TODO allow result return types - let builder = internal(builder); + )) + .startup_function(__ext_php_rs_startup); match builder.build() { - Ok(module) => module.into_raw(), + Ok((entry, startup)) => { + __EXT_PHP_RS_MODULE_STARTUP.lock().replace(startup); + entry.into_raw() + }, Err(e) => panic!("Failed to build PHP module: {:?}", e), } } - - #describe_fn - }; - Ok(result) -} - -/// Generates an implementation for `RegisteredClass` on the given class. -pub fn generate_registered_class_impl(class: &Class) -> Result { - let self_ty = Ident::new(&class.struct_path, Span::call_site()); - let class_name = &class.class_name; - let meta = Ident::new(&format!("_{}_META", &class.struct_path), Span::call_site()); - let prop_tuples = class - .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::class::ConstructorMeta { - constructor: Self::#func, - build_fn: { - use ::ext_php_rs::builders::FunctionBuilder; - fn build_fn(func: FunctionBuilder) -> FunctionBuilder { - func - #(#args)* - } - build_fn - } - }) - } - } else { - quote! { None } - }; - - Ok(quote! { - static #meta: ::ext_php_rs::class::ClassMetadata<#self_ty> = ::ext_php_rs::class::ClassMetadata::new(); - - impl ::ext_php_rs::class::RegisteredClass for #self_ty { - const CLASS_NAME: &'static str = #class_name; - const CONSTRUCTOR: ::std::option::Option< - ::ext_php_rs::class::ConstructorMeta - > = #constructor; - - fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { - &#meta - } - - fn get_properties<'a>() -> ::std::collections::HashMap<&'static str, ::ext_php_rs::props::Property<'a, Self>> { - use ::std::iter::FromIterator; - - ::std::collections::HashMap::from_iter([ - #(#prop_tuples)* - ]) - } - } - }) -} - -pub trait Describe { - fn describe(&self) -> TokenStream; -} - -fn generate_stubs(state: &MutexGuard) -> TokenStream { - let module = state.describe(); - - quote! { - #[cfg(debug_assertions)] - #[no_mangle] - pub extern "C" fn ext_php_rs_describe_module() -> ::ext_php_rs::describe::Description { - use ::ext_php_rs::describe::*; - - Description::new(#module) - } - } -} - -impl Describe for Function { - fn describe(&self) -> TokenStream { - let name = &self.name; - let ret = if let Some((ty, null)) = &self.output { - let ty: Type = syn::parse_str(ty) - .expect("unreachable - failed to parse previously parsed function return type"); - quote! { - Some(Retval { - ty: <#ty as ::ext_php_rs::convert::IntoZval>::TYPE, - nullable: #null, - }) - } - } else { - quote! { None } - }; - let params = self.args.iter().map(Describe::describe); - let docs = self.docs.iter().map(|doc| { - quote! { - #doc.into() - } - }); - - quote! { - Function { - name: #name.into(), - docs: DocBlock(vec![#(#docs,)*].into()), - ret: abi::Option::#ret, - params: vec![#(#params,)*].into(), - } - } - } -} - -impl Describe for Arg { - fn describe(&self) -> TokenStream { - let Arg { name, nullable, .. } = self; - let ty: Type = syn::parse_str(&self.ty).expect("failed to parse previously parsed type"); - let default = if let Some(default) = &self.default { - quote! { Some(#default.into()) } - } else { - quote! { None } - }; - - quote! { - Parameter { - name: #name.into(), - ty: abi::Option::Some(<#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE), - nullable: #nullable, - default: abi::Option::#default, - } - } - } -} - -impl Describe for Class { - fn describe(&self) -> TokenStream { - let name = &self.class_name; - let extends = if let Some(parent) = &self.parent { - quote! { Some(#parent.into()) } - } else { - quote! { None } - }; - let interfaces = self.interfaces.iter().map(|iface| quote! { #iface.into() }); - let properties = self.properties.iter().map(|d| d.describe()); - let mut methods: Vec<_> = self.methods.iter().map(Describe::describe).collect(); - let docs = self.docs.iter().map(|c| { - quote! { - #c.into() - } - }); - let constants = self.constants.iter().map(Describe::describe); - - if let Some(ctor) = &self.constructor { - methods.insert(0, ctor.describe()); - } - - quote! { - Class { - name: #name.into(), - docs: DocBlock(vec![#(#docs,)*].into()), - extends: abi::Option::#extends, - implements: vec![#(#interfaces,)*].into(), - properties: vec![#(#properties,)*].into(), - methods: vec![#(#methods,)*].into(), - constants: vec![#(#constants,)*].into(), - } - } - } -} - -impl Describe for (&String, &Property) { - fn describe(&self) -> TokenStream { - let name = self.0; - let docs = self.1.docs.iter().map(|doc| { - quote! { - #doc.into() - } - }); - - // TODO(david): store metadata for ty, vis, static, null, default - quote! { - Property { - name: #name.into(), - docs: DocBlock(vec![#(#docs,)*].into()), - ty: abi::Option::None, - vis: Visibility::Public, - static_: false, - nullable: false, - default: abi::Option::None, - } - } - } -} - -impl Describe for crate::method::Method { - fn describe(&self) -> TokenStream { - let crate::method::Method { name, _static, .. } = &self; - let ty = if self.name == "__construct" { - quote! { MethodType::Constructor } - } else if self._static { - quote! { MethodType::Static } - } else { - quote! { MethodType::Member } - }; - let parameters = self.args.iter().filter_map(|arg| { - if let crate::method::Arg::Typed(arg) = &arg { - Some(arg.describe()) - } else { - None - } - }); - let ret = if let Some((ty, null)) = &self.output { - let ty: Type = syn::parse_str(ty).expect("failed to parse previously parsed type"); - quote! { - Some(Retval { - ty: <#ty as ::ext_php_rs::convert::IntoZval>::TYPE, - nullable: #null, - }) - } - } else { - quote! { None } - }; - let vis = self.visibility.describe(); - let docs = self.docs.iter().map(|doc| { - quote! { - #doc.into() - } - }); - - quote! { - Method { - name: #name.into(), - docs: DocBlock(vec![#(#docs,)*].into()), - ty: #ty, - params: vec![#(#parameters,)*].into(), - retval: abi::Option::#ret, - _static: #_static, - visibility: #vis, - } - } - } -} - -impl Describe for crate::impl_::Visibility { - fn describe(&self) -> TokenStream { - match self { - crate::impl_::Visibility::Public => quote! { Visibility::Public }, - crate::impl_::Visibility::Protected => quote! { Visibility::Protected }, - crate::impl_::Visibility::Private => quote! { Visibility::Private }, - } - } -} - -impl Describe for crate::constant::Constant { - fn describe(&self) -> TokenStream { - let name = &self.name; - let docs = self.docs.iter().map(|doc| { - quote! { - #doc.into() - } - }); - - quote! { - Constant { - name: #name.into(), - docs: DocBlock(vec![#(#docs,)*].into()), - value: abi::Option::None, - } - } - } -} - -impl Describe for State { - fn describe(&self) -> TokenStream { - let functs = self.functions.iter().map(Describe::describe); - let classes = self.classes.values().map(|class| class.describe()); - let constants = self.constants.iter().map(Describe::describe); - - quote! { - Module { - name: env!("CARGO_PKG_NAME").into(), - functions: vec![#(#functs,)*].into(), - classes: vec![#(#classes,)*].into(), - constants: vec![#(#constants,)*].into(), - } - } } } diff --git a/crates/macros/src/old_class.rs b/crates/macros/src/old_class.rs new file mode 100644 index 0000000000..978106f106 --- /dev/null +++ b/crates/macros/src/old_class.rs @@ -0,0 +1,378 @@ +use std::collections::HashMap; + +use anyhow::{anyhow, bail, Context, Result}; +use darling::{FromMeta, ToTokens}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::parse::ParseStream; +use syn::{Attribute, AttributeArgs, Expr, Fields, FieldsNamed, ItemStruct, LitStr, Token}; + +#[derive(Debug, Default)] +pub struct Class { + pub class_name: String, + pub struct_path: String, + pub parent: Option, + pub interfaces: Vec, + pub docs: Vec, + pub methods: Vec, + pub constructor: Option, + pub constants: Vec, + pub properties: HashMap, + /// A function name called when creating the class entry. Given an instance + /// of `ClassBuilder` and must return it. + pub modifier: Option, +} + +#[derive(Debug)] +pub enum ParsedAttribute { + Extends(Expr), + Implements(Expr), + Property(PropertyAttr), + Comment(String), +} + +#[derive(Default, Debug, FromMeta)] +#[darling(default)] +pub struct AttrArgs { + name: Option, + modifier: Option, +} + +pub fn parser(args: AttributeArgs, mut input: ItemStruct) -> Result { + let args = AttrArgs::from_list(&args) + .map_err(|e| anyhow!("Unable to parse attribute arguments: {:?}", e))?; + + let mut parent = None; + let mut interfaces = vec![]; + let mut properties = HashMap::new(); + let mut comments = vec![]; + + input.attrs = { + let mut unused = vec![]; + for attr in input.attrs.into_iter() { + match parse_attribute(&attr)? { + Some(parsed) => match parsed { + ParsedAttribute::Extends(class) => { + parent = Some(class.to_token_stream().to_string()); + } + ParsedAttribute::Implements(class) => { + interfaces.push(class.to_token_stream().to_string()); + } + ParsedAttribute::Comment(comment) => { + comments.push(comment); + } + attr => bail!("Attribute `{:?}` is not valid for structs.", attr), + }, + None => unused.push(attr), + } + } + unused + }; + + if let Fields::Named(FieldsNamed { + brace_token: _, + named, + }) = &mut input.fields + { + for field in named.iter_mut() { + let mut docs = vec![]; + let mut attrs = vec![]; + attrs.append(&mut field.attrs); + + for attr in attrs.into_iter() { + let mut result_prop = None; + match parse_attribute(&attr)? { + Some(parsed) => match parsed { + ParsedAttribute::Property(prop) => { + let field_name = field + .ident + .as_ref() + .ok_or_else(|| anyhow!("Only named fields can be properties."))? + .to_string(); + let prop_name = prop.rename.unwrap_or_else(|| field_name.clone()); + result_prop = Some(( + prop_name, + Property::field( + field_name, + vec![], + prop.flags.map(|flags| flags.to_token_stream().to_string()), + ), + )); + } + ParsedAttribute::Comment(doc) => docs.push(doc), + _ => bail!("Attribute {:?} is not valid for struct fields.", attr), + }, + None => field.attrs.push(attr), + } + + if let Some(mut prop) = result_prop { + prop.1.docs.append(&mut docs); + properties.insert(prop.0, prop.1); + } + } + } + } + + let ItemStruct { ident, .. } = &input; + let class_name = args.name.unwrap_or_else(|| ident.to_string()); + let struct_path = ident.to_string(); + let class = Class { + class_name, + struct_path, + parent, + interfaces, + docs: comments, + properties, + modifier: args.modifier, + ..Default::default() + }; + + // let mut state = STATE.lock(); + + // if state.built_module { + // bail!("The `#[php_module]` macro must be called last to ensure functions + // and classes are registered."); } + + // if state.startup_function.is_some() { + // bail!("The `#[php_startup]` macro must be called after all the classes + // have been defined."); } + + // state.classes.insert(ident.to_string(), class); + + Ok(quote! { + #input + + ::ext_php_rs::class_derives!(#ident); + }) +} + +impl Class { + pub fn generate_registered_class_impl(&self) -> Result { + let self_ty = Ident::new(&self.struct_path, Span::call_site()); + let class_name = &self.class_name; + let meta = Ident::new(&format!("_{}_META", &self.struct_path), Span::call_site()); + let prop_tuples = self + .properties + .iter() + .map(|(name, prop)| prop.as_prop_tuple(name)); + let constructor = if let Some(constructor) = &self.constructor { + let func = Ident::new(&constructor.ident, Span::call_site()); + let args = constructor.get_arg_definitions(); + quote! { + Some(::ext_php_rs::class::ConstructorMeta { + constructor: Self::#func, + build_fn: { + use ::ext_php_rs::builders::FunctionBuilder; + fn build_fn(func: FunctionBuilder) -> FunctionBuilder { + func + #(#args)* + } + build_fn + } + }) + } + } else { + quote! { None } + }; + + Ok(quote! { + static #meta: ::ext_php_rs::class::ClassMetadata<#self_ty> = ::ext_php_rs::class::ClassMetadata::new(); + + impl ::ext_php_rs::class::RegisteredClass for #self_ty { + const CLASS_NAME: &'static str = #class_name; + const CONSTRUCTOR: ::std::option::Option< + ::ext_php_rs::class::ConstructorMeta + > = #constructor; + + fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { + &#meta + } + + fn get_properties<'a>() -> ::std::collections::HashMap<&'static str, ::ext_php_rs::props::Property<'a, Self>> { + use ::std::iter::FromIterator; + + ::std::collections::HashMap::from_iter([ + #(#prop_tuples)* + ]) + } + } + }) + } +} + +#[derive(Debug)] +pub struct Property { + pub ty: PropertyType, + pub docs: Vec, + #[allow(dead_code)] + pub flags: Option, +} + +#[derive(Debug)] +pub enum PropertyType { + Field { + field_name: String, + }, + Method { + getter: Option, + setter: Option, + }, +} + +impl Property { + pub fn add_getter(&mut self, new_getter: String) -> Result<()> { + match &mut self.ty { + PropertyType::Field { .. } => bail!("Cannot add getter to field property."), + PropertyType::Method { getter, setter: _ } => match getter { + Some(getter) => bail!( + "Attempted to add getter `{}` to property that already has a getter `{}`.", + new_getter, + getter + ), + None => { + getter.replace(new_getter); + Ok(()) + } + }, + } + } + + pub fn add_setter(&mut self, new_setter: String) -> Result<()> { + match &mut self.ty { + PropertyType::Field { .. } => bail!("Cannot add setter to field property."), + PropertyType::Method { getter: _, setter } => match setter { + Some(getter) => bail!( + "Attempted to add setter `{}` to property that already has a setter `{}`.", + new_setter, + getter + ), + None => { + setter.replace(new_setter); + Ok(()) + } + }, + } + } + + pub fn field(field_name: String, docs: Vec, flags: Option) -> Self { + Self { + ty: PropertyType::Field { field_name }, + docs, + flags, + } + } + + pub fn method(docs: Vec, flags: Option) -> Self { + Self { + ty: PropertyType::Method { + getter: None, + setter: None, + }, + docs, + flags, + } + } + + pub fn as_prop_tuple(&self, name: &str) -> TokenStream { + match &self.ty { + PropertyType::Field { field_name } => { + let field_name = Ident::new(field_name, Span::call_site()); + quote! { + (#name, ::ext_php_rs::props::Property::field(|obj: &mut Self| &mut obj.#field_name)), + } + } + PropertyType::Method { getter, setter } => { + let getter = if let Some(getter) = getter { + let ident = Ident::new(getter, Span::call_site()); + quote! { Some(Self::#ident) } + } else { + quote! { None } + }; + let setter = if let Some(setter) = setter { + let ident = Ident::new(setter, Span::call_site()); + quote! { Some(Self::#ident) } + } else { + quote! { None } + }; + quote! { + (#name, ::ext_php_rs::props::Property::method(#getter, #setter)), + } + } + } + } +} + +#[derive(Debug, Default)] +pub struct PropertyAttr { + pub rename: Option, + pub flags: Option, +} + +impl syn::parse::Parse for PropertyAttr { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut this = Self::default(); + while !input.is_empty() { + let field = input.parse::()?.to_string(); + input.parse::()?; + + match field.as_str() { + "rename" => { + this.rename.replace(input.parse::()?.value()); + } + "flags" => { + this.flags.replace(input.parse::()?); + } + _ => return Err(input.error("invalid attribute field")), + } + + let _ = input.parse::(); + } + + Ok(this) + } +} + +pub fn parse_attribute(attr: &Attribute) -> Result> { + let name = attr.path.to_token_stream().to_string(); + + Ok(match name.as_ref() { + "extends" => { + let meta: Expr = attr + .parse_args() + .map_err(|_| anyhow!("Unable to parse `#[{}]` attribute.", name))?; + Some(ParsedAttribute::Extends(meta)) + } + "implements" => { + let meta: Expr = attr + .parse_args() + .map_err(|_| anyhow!("Unable to parse `#[{}]` attribute.", name))?; + Some(ParsedAttribute::Implements(meta)) + } + "doc" => { + struct DocComment(pub String); + + impl syn::parse::Parse for DocComment { + fn parse(input: ParseStream) -> syn::Result { + input.parse::()?; + let comment: LitStr = input.parse()?; + Ok(Self(comment.value())) + } + } + + let comment: DocComment = + syn::parse2(attr.tokens.clone()).with_context(|| "Failed to parse doc comment")?; + Some(ParsedAttribute::Comment(comment.0)) + } + "prop" | "property" => { + let attr = if attr.tokens.is_empty() { + PropertyAttr::default() + } else { + attr.parse_args() + .map_err(|e| anyhow!("Unable to parse `#[{}]` attribute: {}", name, e))? + }; + + Some(ParsedAttribute::Property(attr)) + } + _ => None, + }) +} diff --git a/crates/macros/src/old_impl.rs b/crates/macros/src/old_impl.rs new file mode 100644 index 0000000000..3f6674b416 --- /dev/null +++ b/crates/macros/src/old_impl.rs @@ -0,0 +1,371 @@ +use std::collections::HashMap; + +use anyhow::{anyhow, bail, Result}; +use darling::{FromMeta, ToTokens}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::spanned::Spanned; +use syn::{Attribute, AttributeArgs, ItemImpl, Lit, Meta, NestedMeta}; + +use crate::helpers::get_docs; +use crate::{ + class::{Property, PropertyAttr}, + constant::Constant, + method, +}; + +#[derive(Debug, Clone)] +pub enum Visibility { + Public, + Protected, + Private, +} + +#[derive(Debug, Copy, Clone, FromMeta)] +pub enum RenameRule { + #[darling(rename = "none")] + None, + #[darling(rename = "camelCase")] + Camel, + #[darling(rename = "snake_case")] + Snake, +} + +impl Default for RenameRule { + fn default() -> Self { + RenameRule::Camel + } +} + +impl RenameRule { + /// Change case of an identifier. + /// + /// Magic methods are handled specially to make sure they're always cased + /// correctly. + pub fn rename(&self, name: impl AsRef) -> String { + let name = name.as_ref(); + match self { + RenameRule::None => name.to_string(), + rule => match name { + "__construct" => "__construct".to_string(), + "__destruct" => "__destruct".to_string(), + "__call" => "__call".to_string(), + "__call_static" => "__callStatic".to_string(), + "__get" => "__get".to_string(), + "__set" => "__set".to_string(), + "__isset" => "__isset".to_string(), + "__unset" => "__unset".to_string(), + "__sleep" => "__sleep".to_string(), + "__wakeup" => "__wakeup".to_string(), + "__serialize" => "__serialize".to_string(), + "__unserialize" => "__unserialize".to_string(), + "__to_string" => "__toString".to_string(), + "__invoke" => "__invoke".to_string(), + "__set_state" => "__set_state".to_string(), + "__clone" => "__clone".to_string(), + "__debug_info" => "__debugInfo".to_string(), + field => match rule { + Self::Camel => ident_case::RenameRule::CamelCase.apply_to_field(field), + Self::Snake => ident_case::RenameRule::SnakeCase.apply_to_field(field), + Self::None => unreachable!(), + }, + }, + } + } +} + +#[derive(Debug)] +pub enum ParsedAttribute { + Default(HashMap), + Optional(String), + Visibility(Visibility), + Rename(String), + Property { + prop_name: Option, + ty: PropAttrTy, + }, + Constructor, + This, +} + +#[derive(Default, Debug, FromMeta)] +#[darling(default)] +pub struct AttrArgs { + rename_methods: Option, +} + +#[derive(Debug)] +pub enum PropAttrTy { + Getter, + Setter, +} + +pub fn parser(args: AttributeArgs, input: ItemImpl) -> Result { + let args = AttrArgs::from_list(&args) + .map_err(|e| anyhow!("Unable to parse attribute arguments: {:?}", e))?; + + let ItemImpl { self_ty, items, .. } = input; + let class_name = self_ty.to_token_stream().to_string(); + + if input.trait_.is_some() { + bail!("This macro cannot be used on trait implementations."); + } + + let mut constructor = None; + // let tokens = items + // .into_iter() + // .map(|item| { + // Ok(match item { + // syn::ImplItem::Const(constant) => { + // // class.constants.push(Constant { + // // name: constant.ident.to_string(), + // // // visibility: Visibility::Public, + // // docs: get_docs(&constant.attrs), + // // value: constant.expr.to_token_stream().to_string(), + // // }); + + // // quote! { + // // #[allow(dead_code)] + // // #constant + // // } + // todo!("class constants") + // } + // syn::ImplItem::Method(method) => { + // let parsed_method = + // method::parser(&self_ty, method, + // args.rename_methods.unwrap_or_default())?; + + // // TODO(david): How do we handle comments for getter/setter? + // Take the comments // // from the methods?? + // if let Some((prop, ty)) = parsed_method.property { + // // let prop = class + // // .properties + // // .entry(prop) + // // .or_insert_with(|| Property::method(vec![], + // None)); // let ident = + // parsed_method.method.orig_ident.clone(); + + // // match ty { + // // PropAttrTy::Getter => prop.add_getter(ident)?, + // // PropAttrTy::Setter => prop.add_setter(ident)?, + // // } + // todo!("class property methods") + // } + // if parsed_method.constructor { + // constructor = Some(parsed_method.method); + // } + // parsed_method.tokens + // } + // item => item.to_token_stream(), + // }) + // }) + // .collect::>>()?; + + let mut tokens = vec![]; + let mut methods = vec![]; + for item in items.into_iter() { + match item { + syn::ImplItem::Const(constant) => { + // class.constants.push(Constant { + // name: constant.ident.to_string(), + // // visibility: Visibility::Public, + // docs: get_docs(&constant.attrs), + // value: constant.expr.to_token_stream().to_string(), + // }); + + // quote! { + // #[allow(dead_code)] + // #constant + // } + todo!("class constants") + } + syn::ImplItem::Method(method) => { + let parsed_method = + method::parser(&self_ty, method, args.rename_methods.unwrap_or_default())?; + + // TODO(david): How do we handle comments for getter/setter? Take the comments + // // from the methods?? + if let Some((prop, ty)) = parsed_method.property { + // let prop = class + // .properties + // .entry(prop) + // .or_insert_with(|| Property::method(vec![], None)); + // let ident = parsed_method.method.orig_ident.clone(); + + // match ty { + // PropAttrTy::Getter => prop.add_getter(ident)?, + // PropAttrTy::Setter => prop.add_setter(ident)?, + // } + todo!("class property methods") + } + if parsed_method.constructor { + constructor = Some(parsed_method.method); + } else { + methods.push(parsed_method.method); + } + tokens.push(parsed_method.tokens); + } + item => tokens.push(item.to_token_stream()), + } + } + + let constructor = if let Some(constructor) = constructor { + let func = Ident::new(&constructor.ident, Span::call_site()); + let args = constructor.get_arg_definitions(); + quote! { + Some(::ext_php_rs::class::ConstructorMeta { + constructor: Self::#func, + build_fn: { + use ::ext_php_rs::builders::FunctionBuilder; + fn build_fn(func: FunctionBuilder) -> FunctionBuilder { + func + #(#args)* + } + build_fn + } + }) + } + } else { + quote! { None } + }; + let methods = methods.into_iter().map(|method| {}); + + Ok(quote! { + impl #self_ty { + #(#tokens)* + } + + impl ::ext_php_rs::internal::class::PhpClassMethods<#self_ty> for ::ext_php_rs::internal::class::PhpClassPropertyCollector<#self_ty> { + fn get_methods(self) -> ::std::vec::Vec<::ext_php_rs::builders::FunctionBuilder<'static>> { + } + + fn get_method_props<'a>(self) -> ::std::collections::HashMap<&'static str, ::ext_php_rs::props::Property<'a, #self_ty>> { + use ::std::iter::FromIterator; + + ::std::collections::HashMap::from_iter([]) + } + + fn get_constructor(self) -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta<#self_ty>> { + #constructor + } + } + }) +} + +pub fn parse_attribute(attr: &Attribute) -> Result> { + let name = attr.path.to_token_stream().to_string(); + let meta = attr + .parse_meta() + .map_err(|_| anyhow!("Unable to parse attribute."))?; + + Ok(Some(match name.as_ref() { + "defaults" => { + let defaults = HashMap::from_meta(&meta) + .map_err(|_| anyhow!("Unable to parse `#[default]` macro."))?; + ParsedAttribute::Default(defaults) + } + "optional" => { + let name = if let Meta::List(list) = meta { + if let Some(NestedMeta::Meta(meta)) = list.nested.first() { + Some(meta.to_token_stream().to_string()) + } else { + None + } + } else { + None + } + .ok_or_else(|| anyhow!("Invalid argument given for `#[optional]` macro."))?; + + ParsedAttribute::Optional(name) + } + "public" => ParsedAttribute::Visibility(Visibility::Public), + "protected" => ParsedAttribute::Visibility(Visibility::Protected), + "private" => ParsedAttribute::Visibility(Visibility::Private), + "rename" => { + let ident = if let Meta::List(list) = meta { + if let Some(NestedMeta::Lit(lit)) = list.nested.first() { + String::from_value(lit).ok() + } else { + None + } + } else { + None + } + .ok_or_else(|| anyhow!("Invalid argument given for `#[rename] macro."))?; + + ParsedAttribute::Rename(ident) + } + "getter" => { + let prop_name = if attr.tokens.is_empty() { + None + } else { + let parsed: PropertyAttr = attr + .parse_args() + .map_err(|e| anyhow!("Unable to parse `#[getter]` attribute: {}", e))?; + parsed.rename + }; + ParsedAttribute::Property { + prop_name, + ty: PropAttrTy::Getter, + } + } + "setter" => { + let prop_name = if attr.tokens.is_empty() { + None + } else { + let parsed: PropertyAttr = attr + .parse_args() + .map_err(|e| anyhow!("Unable to parse `#[setter]` attribute: {}", e))?; + parsed.rename + }; + ParsedAttribute::Property { + prop_name, + ty: PropAttrTy::Setter, + } + } + "constructor" => ParsedAttribute::Constructor, + "this" => ParsedAttribute::This, + _ => return Ok(None), + })) +} + +#[cfg(test)] +mod tests { + use super::RenameRule; + + #[test] + fn test_rename_magic() { + for &(magic, expected) in &[ + ("__construct", "__construct"), + ("__destruct", "__destruct"), + ("__call", "__call"), + ("__call_static", "__callStatic"), + ("__get", "__get"), + ("__set", "__set"), + ("__isset", "__isset"), + ("__unset", "__unset"), + ("__sleep", "__sleep"), + ("__wakeup", "__wakeup"), + ("__serialize", "__serialize"), + ("__unserialize", "__unserialize"), + ("__to_string", "__toString"), + ("__invoke", "__invoke"), + ("__set_state", "__set_state"), + ("__clone", "__clone"), + ("__debug_info", "__debugInfo"), + ] { + assert_eq!(magic, RenameRule::None.rename(magic)); + assert_eq!(expected, RenameRule::Camel.rename(magic)); + assert_eq!(expected, RenameRule::Snake.rename(magic)); + } + } + + #[test] + fn test_rename_php_methods() { + for &(original, camel, snake) in &[("get_name", "getName", "get_name")] { + assert_eq!(original, RenameRule::None.rename(original)); + assert_eq!(camel, RenameRule::Camel.rename(original)); + assert_eq!(snake, RenameRule::Snake.rename(original)); + } + } +} diff --git a/crates/macros/src/old_method.rs b/crates/macros/src/old_method.rs new file mode 100644 index 0000000000..d6bbaabc7c --- /dev/null +++ b/crates/macros/src/old_method.rs @@ -0,0 +1,456 @@ +use anyhow::{anyhow, bail, Result}; +use quote::ToTokens; +use std::collections::HashMap; +use syn::ReturnType; + +use crate::helpers::get_docs; +use crate::{ + function::{self, ParserType}, + impl_::{parse_attribute, ParsedAttribute, PropAttrTy, RenameRule, Visibility}, +}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::{punctuated::Punctuated, FnArg, ImplItemMethod, Lit, Pat, Token, Type}; + +#[derive(Debug, Clone)] +pub enum Arg { + Receiver(MethodType), + Typed(function::Arg), +} + +#[derive(Debug)] +pub struct AttrArgs { + pub defaults: HashMap, + pub optional: Option, + pub visibility: Visibility, +} + +#[derive(Debug, Clone)] +pub struct Method { + /// Method name + pub name: String, + /// extern "C" function ident + pub ident: String, + /// Rust internal function ident + pub orig_ident: String, + pub docs: Vec, + pub args: Vec, + pub optional: Option, + pub output: Option<(String, bool)>, + pub _static: bool, + pub visibility: Visibility, +} + +pub struct ParsedMethod { + pub tokens: TokenStream, + pub method: Method, + pub property: Option<(String, PropAttrTy)>, + pub constructor: bool, +} + +#[derive(Debug, Clone, Copy)] +pub enum MethodType { + Receiver { mutable: bool }, + ReceiverClassObject, + Static, +} + +impl ParsedMethod { + pub fn new( + tokens: TokenStream, + method: Method, + property: Option<(String, PropAttrTy)>, + constructor: bool, + ) -> Self { + Self { + tokens, + method, + property, + constructor, + } + } +} + +pub fn parser( + struct_ty: &Type, + mut input: ImplItemMethod, + rename_rule: RenameRule, +) -> Result { + let mut defaults = HashMap::new(); + let mut optional = None; + let mut visibility = Visibility::Public; + let mut as_prop = None; + let mut identifier = None; + let mut is_constructor = false; + let docs = get_docs(&input.attrs); + + for attr in input.attrs.iter() { + if let Some(attr) = parse_attribute(attr)? { + match attr { + ParsedAttribute::Default(list) => defaults = list, + ParsedAttribute::Optional(name) => optional = Some(name), + ParsedAttribute::Visibility(vis) => visibility = vis, + ParsedAttribute::Rename(ident) => identifier = Some(ident), + ParsedAttribute::Property { prop_name, ty } => { + if as_prop.is_some() { + bail!( + "Only one `#[getter]` and/or `#[setter]` attribute may be used per method." + ); + } + + let prop_name = prop_name.unwrap_or_else(|| { + input + .sig + .ident + .to_token_stream() + .to_string() + .trim_start_matches("get_") + .trim_start_matches("set_") + .to_string() + }); + as_prop = Some((prop_name, ty)) + } + ParsedAttribute::Constructor => is_constructor = true, + _ => bail!("Invalid attribute for method."), + } + } + } + + input.attrs.clear(); + + let ident = &input.sig.ident; + 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(struct_ty, &mut input.sig.inputs, &defaults)?; + let optional = function::find_optional_parameter( + args.iter().filter_map(|arg| match arg { + Arg::Typed(arg) => Some(arg), + _ => None, + }), + optional, + ); + let (arg_definitions, method_type) = build_arg_definitions(&args); + let arg_parser = build_arg_parser( + args.iter(), + &optional, + &bail, + match method_type { + MethodType::Static => ParserType::StaticMethod, + _ => ParserType::Method, + }, + )?; + let arg_accessors = build_arg_accessors(&args, &bail); + + let func = if is_constructor { + quote! { + #input + + #[doc(hidden)] + pub fn #internal_ident( + ex: &mut ::ext_php_rs::zend::ExecuteData + ) -> ::ext_php_rs::class::ConstructorResult { + use ::ext_php_rs::convert::IntoZval; + use ::ext_php_rs::class::ConstructorResult; + + #(#arg_definitions)* + #arg_parser + + Self::#ident(#(#arg_accessors,)*).into() + } + } + } else { + let this = match method_type { + MethodType::Receiver { .. } => quote! { this. }, + MethodType::ReceiverClassObject | MethodType::Static => quote! { Self:: }, + }; + + quote! { + #input + + ::ext_php_rs::zend_fastcall! { + #[doc(hidden)] + pub extern fn #internal_ident( + ex: &mut ::ext_php_rs::zend::ExecuteData, + retval: &mut ::ext_php_rs::types::Zval + ) { + use ::ext_php_rs::convert::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::exception::PhpException = e.into(); + e.throw().expect("Failed to throw exception"); + } + } + } + } + }; + + let method = Method { + name, + ident: internal_ident.to_string(), + orig_ident: ident.to_string(), + docs, + args, + optional, + output: get_return_type(struct_ty, &input.sig.output)?, + _static: matches!(method_type, MethodType::Static), + visibility, + }; + + Ok(ParsedMethod::new(func, method, as_prop, is_constructor)) +} + +pub fn get_return_type(self_ty: &Type, output_type: &ReturnType) -> Result> { + Ok(match output_type { + ReturnType::Default => None, + ReturnType::Type(_, ty) => { + let mut ty = ty.clone(); + replace_self(self_ty, &mut ty); + crate::function::Arg::from_type("".to_string(), &ty, None, true) + .map(|arg| (arg.ty, arg.nullable)) + } + }) +} + +/// Takes a type `ty` and replaces all instances of `Self` with the type +/// `self_ty`. +fn replace_self(self_ty: &Type, ty: &mut Type) { + match ty { + Type::Array(syn::TypeArray { elem, .. }) => replace_self(self_ty, elem), + Type::BareFn(syn::TypeBareFn { inputs, output, .. }) => { + for input in inputs { + replace_self(self_ty, &mut input.ty); + } + if let ReturnType::Type(_, ty) = output { + replace_self(self_ty, ty); + } + } + Type::Group(syn::TypeGroup { elem, .. }) => replace_self(self_ty, elem), + Type::Paren(syn::TypeParen { elem, .. }) => replace_self(self_ty, elem), + Type::Path(syn::TypePath { qself, path }) => { + if let Some(syn::QSelf { ty, .. }) = qself { + replace_self(self_ty, ty); + } + for seg in &mut path.segments { + if seg.ident == "Self" { + seg.ident = + Ident::new(&self_ty.to_token_stream().to_string(), Span::call_site()); + } + match &mut seg.arguments { + syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { + args, + .. + }) => { + for arg in args { + if let syn::GenericArgument::Type(ty) = arg { + replace_self(self_ty, ty); + } + } + } + syn::PathArguments::Parenthesized(syn::ParenthesizedGenericArguments { + inputs, + output, + .. + }) => { + for input in inputs { + replace_self(self_ty, input); + } + + if let ReturnType::Type(_, ty) = output { + replace_self(self_ty, ty); + } + } + _ => {} + } + } + } + Type::Ptr(syn::TypePtr { elem, .. }) => replace_self(self_ty, elem), + Type::Reference(syn::TypeReference { elem, .. }) => replace_self(self_ty, elem), + Type::Slice(syn::TypeSlice { elem, .. }) => replace_self(self_ty, elem), + Type::Tuple(syn::TypeTuple { elems, .. }) => { + for elem in elems { + replace_self(self_ty, elem); + } + } + _ => {} + } +} + +fn build_args( + struct_ty: &Type, + inputs: &mut Punctuated, + defaults: &HashMap, +) -> Result> { + inputs + .iter_mut() + .map(|arg| match arg { + FnArg::Receiver(receiver) => { + if receiver.reference.is_none() { + bail!("`self` parameter must be a reference."); + } + Ok(Arg::Receiver(MethodType::Receiver { + mutable: receiver.mutability.is_some(), + })) + } + FnArg::Typed(ty) => { + let mut this = false; + let attrs = std::mem::take(&mut ty.attrs); + for attr in attrs.into_iter() { + if let Some(attr) = parse_attribute(&attr)? { + match attr { + ParsedAttribute::This => this = true, + _ => bail!("Invalid attribute for argument."), + } + } + } + + if this { + Ok(Arg::Receiver(MethodType::ReceiverClassObject)) + } else { + let name = match &*ty.pat { + Pat::Ident(pat) => pat.ident.to_string(), + _ => bail!("Invalid parameter type."), + }; + let default = defaults.get(&name); + let mut ty = ty.ty.clone(); + replace_self(struct_ty, &mut ty); + + Ok(Arg::Typed( + crate::function::Arg::from_type(name.clone(), &ty, default, false) + .ok_or_else(|| anyhow!("Invalid parameter type for `{}`.", name))?, + )) + } + } + }) + .collect() +} + +fn build_arg_definitions(args: &[Arg]) -> (Vec, MethodType) { + let mut method_type = MethodType::Static; + + ( + args.iter() + .filter_map(|ty| match ty { + Arg::Receiver(t) => { + method_type = *t; + None + } + Arg::Typed(arg) => { + let ident = arg.get_name_ident(); + let definition = arg.get_arg_definition(); + Some(quote! { + let mut #ident = #definition; + }) + } + }) + .collect(), + method_type, + ) +} + +fn build_arg_parser<'a>( + args: impl Iterator, + optional: &Option, + ret: &TokenStream, + ty: ParserType, +) -> Result { + function::build_arg_parser( + args.filter_map(|arg| match arg { + Arg::Typed(arg) => Some(arg), + _ => None, + }), + optional, + ret, + ty, + ) +} + +fn build_arg_accessors(args: &[Arg], ret: &TokenStream) -> Vec { + args.iter() + .filter_map(|arg| match arg { + Arg::Typed(arg) => Some(arg.get_accessor(ret)), + Arg::Receiver(MethodType::ReceiverClassObject) => Some(quote! { this }), + _ => None, + }) + .collect() +} + +impl Method { + #[inline] + pub fn get_name_ident(&self) -> Ident { + 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.get_arg_definitions(); + let output = self.output.as_ref().map(|(ty, nullable)| { + let ty: Type = syn::parse_str(ty).unwrap(); + + // TODO allow reference returns? + quote! { + .returns(<#ty as ::ext_php_rs::convert::IntoZval>::TYPE, false, #nullable) + } + }); + + quote! { + ::ext_php_rs::builders::FunctionBuilder::new(#name, #class_path :: #name_ident) + #(#args)* + #output + .build() + } + } + + pub fn get_flags(&self) -> TokenStream { + let mut flags = vec![match self.visibility { + Visibility::Public => quote! { Public }, + Visibility::Protected => quote! { Protected }, + Visibility::Private => quote! { Private }, + }]; + + if self._static { + flags.push(quote! { Static }); + } + + flags + .iter() + .map(|flag| quote! { ::ext_php_rs::flags::MethodFlags::#flag }) + .collect::>() + .to_token_stream() + } +} diff --git a/crates/macros/src/startup_function.rs b/crates/macros/src/startup_function.rs index 695753d6d9..da9ce38bbe 100644 --- a/crates/macros/src/startup_function.rs +++ b/crates/macros/src/startup_function.rs @@ -6,193 +6,149 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use syn::{AttributeArgs, Expr, ItemFn, Signature}; -use crate::{class::Class, constant::Constant, STATE}; +pub fn parser(input: ItemFn) -> Result { + Ok(quote! {}) + // let ItemFn { sig, block, .. } = input; + // let Signature { ident, .. } = sig; + // let stmts = &block.stmts; -#[derive(Default, Debug, FromMeta)] -#[darling(default)] -struct StartupArgs { - before: bool, -} + // // let mut state = STATE.lock(); + // // state.startup_function = Some(ident.to_string()); -pub fn parser(args: Option, input: ItemFn) -> Result { - let args = if let Some(args) = args { - StartupArgs::from_list(&args) - .map_err(|e| anyhow!("Unable to parse attribute arguments: {:?}", e))? - } else { - StartupArgs::default() - }; - - let ItemFn { sig, block, .. } = input; - let Signature { ident, .. } = sig; - let stmts = &block.stmts; - - let mut state = STATE.lock(); - state.startup_function = Some(ident.to_string()); - - let classes = build_classes(&state.classes)?; - let constants = build_constants(&state.constants); - let (before, after) = if args.before { - (Some(quote! { internal(ty, module_number); }), None) - } else { - (None, Some(quote! { internal(ty, module_number); })) - }; - - let func = quote! { - #[doc(hidden)] - pub extern "C" fn #ident(ty: i32, module_number: i32) -> i32 { - use ::ext_php_rs::constant::IntoConst; - use ::ext_php_rs::flags::PropertyFlags; - - fn internal(ty: i32, module_number: i32) { - #(#stmts)* - } - - ::ext_php_rs::internal::ext_php_rs_startup(); - - #before - #(#classes)* - #(#constants)* - #after - - 0 - } - }; - - Ok(func) -} + // // let classes = build_classes(&state.classes)?; + // // let constants = build_constants(&state.constants); -/// Returns a vector of `ClassBuilder`s for each class. -fn build_classes(classes: &HashMap) -> Result> { - classes - .iter() - .map(|(name, class)| { - let Class { class_name, .. } = &class; - let ident = Ident::new(name, Span::call_site()); - let meta = Ident::new(&format!("_{name}_META"), Span::call_site()); - let methods = class.methods.iter().map(|method| { - let builder = method.get_builder(&ident); - let flags = method.get_flags(); - quote! { .method(#builder.unwrap(), #flags) } - }); - let constants = class.constants.iter().map(|constant| { - let name = &constant.name; - let val = constant.val_tokens(); - quote! { .constant(#name, #val).unwrap() } - }); - let parent = { - if let Some(parent) = &class.parent { - let expr: Expr = syn::parse_str(parent).map_err(|_| { - anyhow!("Invalid expression given for `{}` parent", class_name) - })?; - Some(quote! { .extends(#expr) }) - } else { - None - } - }; - let interfaces = class - .interfaces - .iter() - .map(|interface| { - let expr: Expr = syn::parse_str(interface).map_err(|_| { - anyhow!( - "Invalid expression given for `{}` interface: `{}`", - class_name, - interface - ) - })?; - Ok(quote! { .implements(#expr) }) - }) - .collect::>>()?; - // TODO(david): register properties for reflection (somehow) - // let properties = class - // .properties - // .iter() - // .map(|(name, (default, flags))| { - // let default_expr: Expr = syn::parse_str(default).map_err(|_| { - // anyhow!( - // "Invalid default value given for property `{}` type: `{}`", - // name, - // default - // ) - // })?; - // let flags_expr: Expr = syn::parse_str( - // flags - // .as_ref() - // .map(|flags| flags.as_str()) - // .unwrap_or("PropertyFlags::Public"), - // ) - // .map_err(|_| { - // anyhow!( - // "Invalid default value given for property `{}` type: `{}`", - // name, - // default - // ) - // })?; - - // Ok(quote! { .property(#name, #default_expr, #flags_expr) }) - // }) - // .collect::>>()?; - let class_modifier = class.modifier.as_ref().map(|modifier| { - let modifier = Ident::new(modifier, Span::call_site()); - quote! { - let builder = #modifier(builder).expect(concat!("Failed to build class ", #class_name)); - } - }); - - let flags = { - if let Some(flags) = &class.flags { - let mut name = "::ext_php_rs::flags::ClassFlags::".to_owned(); - name.push_str(flags); - let expr: Expr = syn::parse_str(&name).map_err(|_| { - anyhow!("Invalid expression given for `{}` flags", class_name) - })?; - Some(quote! { .flags(#expr) }) - } else { - None - } - }; - - let object_override = { - if let Some(flags) = &class.flags { - if flags == "Interface" { - None - } else { - Some(quote! { .object_override::<#ident>() }) - } - } else { - Some(quote! { .object_override::<#ident>() }) - } - }; - - Ok(quote! {{ - let builder = ::ext_php_rs::builders::ClassBuilder::new(#class_name) - #(#methods)* - #(#constants)* - #(#interfaces)* - // #(#properties)* - #parent - #flags - #object_override - ; - #class_modifier - let class = builder.build() - .expect(concat!("Unable to build class `", #class_name, "`")); - - #meta.set_ce(class); - }}) - }) - .collect::>>() -} + // let func = quote! { + // #[doc(hidden)] + // pub extern "C" fn #ident(ty: i32, module_number: i32) -> i32 { + // use ::ext_php_rs::constant::IntoConst; + // use ::ext_php_rs::flags::PropertyFlags; + + // fn internal() { + // #(#stmts)* + // } + + // ::ext_php_rs::internal::ext_php_rs_startup(); -fn build_constants(constants: &[Constant]) -> Vec { - constants - .iter() - .map(|constant| { - let name = &constant.name; - let val = constant.val_tokens(); - quote! { - (#val).register_constant(#name, module_number).unwrap(); - } - }) - .collect() + // // #(#classes)* + // // #(#constants)* + + // // TODO return result? + // internal(); + + // 0 + // } + // }; + + // Ok(func) } + +// /// Returns a vector of `ClassBuilder`s for each class. +// fn build_classes(classes: &HashMap) -> +// Result> { classes +// .iter() +// .map(|(name, class)| { +// let Class { class_name, .. } = &class; +// let ident = Ident::new(name, Span::call_site()); +// let meta = Ident::new(&format!("_{}_META", name), +// Span::call_site()); let methods = +// class.methods.iter().map(|method| { let builder = +// method.get_builder(&ident); let flags = method.get_flags(); +// quote! { .method(#builder.unwrap(), #flags) } +// }); +// let constants = class.constants.iter().map(|constant| { +// let name = &constant.name; +// let val = constant.val_tokens(); +// quote! { .constant(#name, #val).unwrap() } +// }); +// let parent = { +// if let Some(parent) = &class.parent { +// let expr: Expr = syn::parse_str(parent).map_err(|_| { +// anyhow!("Invalid expression given for `{}` parent", +// class_name) })?; +// Some(quote! { .extends(#expr) }) +// } else { +// None +// } +// }; +// let interfaces = class +// .interfaces +// .iter() +// .map(|interface| { +// let expr: Expr = syn::parse_str(interface).map_err(|_| { +// anyhow!( +// "Invalid expression given for `{}` interface: +// `{}`", class_name, +// interface +// ) +// })?; +// Ok(quote! { .implements(#expr) }) +// }) +// .collect::>>()?; +// // TODO(david): register properties for reflection (somehow) +// // let properties = class +// // .properties +// // .iter() +// // .map(|(name, (default, flags))| { +// // let default_expr: Expr = +// syn::parse_str(default).map_err(|_| { // anyhow!( +// // "Invalid default value given for property `{}` +// type: `{}`", // name, +// // default +// // ) +// // })?; +// // let flags_expr: Expr = syn::parse_str( +// // flags +// // .as_ref() +// // .map(|flags| flags.as_str()) +// // .unwrap_or("PropertyFlags::Public"), +// // ) +// // .map_err(|_| { +// // anyhow!( +// // "Invalid default value given for property `{}` +// type: `{}`", // name, +// // default +// // ) +// // })?; + +// // Ok(quote! { .property(#name, #default_expr, +// #flags_expr) }) // }) +// // .collect::>>()?; +// let class_modifier = class.modifier.as_ref().map(|modifier| { +// let modifier = Ident::new(modifier, Span::call_site()); +// quote! { +// let builder = #modifier(builder).expect(concat!("Failed +// to build class ", #class_name)); } +// }); + +// Ok(quote! {{ +// let builder = +// ::ext_php_rs::builders::ClassBuilder::new(#class_name) +// #(#methods)* #(#constants)* +// #(#interfaces)* +// // #(#properties)* +// #parent +// .object_override::<#ident>(); +// #class_modifier +// let class = builder.build() +// .expect(concat!("Unable to build class `", #class_name, +// "`")); + +// #meta.set_ce(class); +// }}) +// }) +// .collect::>>() +// } + +// fn build_constants(constants: &[Constant]) -> Vec { +// constants +// .iter() +// .map(|constant| { +// let name = &constant.name; +// let val = constant.val_tokens(); +// quote! { +// (#val).register_constant(#name, module_number).unwrap(); +// } +// }) +// .collect() +// } diff --git a/examples/hello_world.rs b/examples/hello_world.rs new file mode 100644 index 0000000000..a43516efa4 --- /dev/null +++ b/examples/hello_world.rs @@ -0,0 +1,154 @@ +use ext_php_rs::{ + args::Arg, + builders::{ClassBuilder, FunctionBuilder, ModuleStartup}, + class::{ClassMetadata, ConstructorMeta, ConstructorResult, RegisteredClass}, + convert::IntoZval, + flags::DataType, + internal::class::{PhpClassImpl, PhpClassImplCollector}, + prelude::*, + types::Zval, + zend::ExecuteData, +}; +use parking_lot::{const_mutex, Mutex}; + +// struct MyClass { +// a: i32, +// b: i32, +// } + +// static __INTERNAL_MYCLASS_METADATA: ClassMetadata = +// ClassMetadata::new(); + +// fn test_modifier(c: ClassBuilder) -> ClassBuilder { +// println!("in test modifier"); +// c.constant("FAKE_CONST", 5).unwrap() +// } + +// impl RegisteredClass for MyClass { +// const CLASS_NAME: &'static str = "MyClass"; +// const BUILDER_MODIFIER: Option< +// fn(ext_php_rs::builders::ClassBuilder) -> +// ext_php_rs::builders::ClassBuilder, > = Some(test_modifier); + +// #[inline] +// fn get_metadata() -> &'static ext_php_rs::class::ClassMetadata { +// &__INTERNAL_MYCLASS_METADATA +// } + +// #[inline] +// fn get_properties<'a>( +// ) -> std::collections::HashMap<&'static str, +// ext_php_rs::props::Property<'a, Self>> { Default::default() +// } + +// #[inline] +// fn method_builders() -> Vec> { +// PhpClassImplCollector::::default().get_methods() +// } + +// #[inline] +// fn constructor() -> Option> { +// PhpClassImplCollector::::default().get_constructor() +// } +// } + +// impl MyClass { +// pub fn __construct(a: i32, b: i32) -> Self { +// Self { a, b } +// } + +// pub fn calc(&self, c: i32) -> i32 { +// self.a * self.b * c +// } +// } + +// impl PhpClassImpl for PhpClassImplCollector { +// fn get_methods(self) -> +// Vec> { vec![{ +// ext_php_rs::zend_fastcall! { +// extern fn handler(ex: &mut ExecuteData, retval: &mut Zval) { +// let (parser, this) = ex.parser_method::(); +// let mut c = Arg::new("c", DataType::Long); +// if parser.arg(&mut c).parse().is_err() { +// return; +// } +// let ret = this.unwrap().calc(c.val().unwrap()); +// ret.set_zval(retval, false).unwrap(); +// } +// } +// FunctionBuilder::new("calc", handler) +// .arg(Arg::new("c", DataType::Long)) +// .returns(::TYPE, false, +// false) }] +// } + +// fn get_method_props<'a>( +// self, +// ) -> std::collections::HashMap<&'static str, +// ext_php_rs::props::Property<'a, MyClass>> { Default::default() +// } + +// fn get_constructor(self) -> +// Option> { fn +// constructor(ex: &mut ExecuteData) -> ConstructorResult { +// let mut a = Arg::new("a", DataType::Long); let mut b = +// Arg::new("b", DataType::Long); if ex.parser().arg(&mut +// a).arg(&mut b).parse().is_err() { return +// ConstructorResult::ArgError; } +// ConstructorResult::Ok(MyClass { +// a: a.val().unwrap(), +// b: b.val().unwrap(), +// }) +// } +// fn build_fn(func: FunctionBuilder) -> FunctionBuilder { +// func.arg(Arg::new("a", DataType::Long)) +// .arg(Arg::new("b", DataType::Long)) +// } +// Some(ConstructorMeta { +// constructor, +// build_fn, +// }) +// } +// } + +#[php_class] +pub struct TestClass { + #[prop] + a: i32, + #[prop] + b: i32, +} + +#[php_impl] +impl TestClass { + #[rename("NEW_CONSTANT_NAME")] + pub const SOME_CONSTANT: i32 = 5; + pub const SOME_OTHER_STR: &'static str = "Hello, world!"; + + #[constructor] + pub fn some_other_func(a: i32, b: i32) -> Self { + Self { a, b } + } + + #[optional(test)] + #[defaults(a = 5, test = 100)] + pub fn test_camel_case(&self, a: i32, test: i32) { + println!("a: {} test: {}", a, test); + } + + fn x(&self) -> i32 { + 5 + } +} + +#[php_function] +pub fn new_class() -> TestClass { + TestClass { a: 1, b: 2 } +} + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module + .class::() + .function(wrap_function!(new_class)) +} diff --git a/php b/php new file mode 100755 index 0000000000..299c74e1a3 --- /dev/null +++ b/php @@ -0,0 +1,4 @@ +#!/bin/bash + +cargo build --example hello_world +php -dextension=target/debug/examples/libhello_world.dylib $@ diff --git a/src/builders/class.rs b/src/builders/class.rs index 82c4576e92..a8d7340d47 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -3,7 +3,7 @@ use std::{ffi::CString, mem::MaybeUninit}; use crate::{ builders::FunctionBuilder, class::{ConstructorMeta, ConstructorResult, RegisteredClass}, - convert::IntoZval, + convert::{IntoZval, IntoZvalDyn}, error::{Error, Result}, exception::PhpException, ffi::{ @@ -136,6 +136,27 @@ impl ClassBuilder { Ok(self) } + /// Adds a constant to the class from a `dyn` object. The type of the + /// constant is defined by the type of the value. + /// + /// Returns a result containing the class builder if the constant was + /// successfully added. + /// + /// # Parameters + /// + /// * `name` - The name of the constant to add to the class. + /// * `value` - The value of the constant. + pub fn dyn_constant>( + mut self, + name: T, + value: &dyn IntoZvalDyn, + ) -> Result { + let value = value.as_zval(true)?; + + self.constants.push((name.into(), value)); + Ok(self) + } + /// Sets the flags for the class. /// /// # Parameters diff --git a/src/builders/module.rs b/src/builders/module.rs index c0e8341b02..63851dc352 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -187,8 +187,19 @@ impl ModuleBuilder { pub fn class(mut self) -> Self { self.classes.push(|| { let mut builder = ClassBuilder::new(T::CLASS_NAME); - for method in T::method_builders() { - builder = builder.method(method.build().expect(""), MethodFlags::Public); + for (method, flags) in T::method_builders() { + builder = builder.method(method.build().expect("Failed to build method"), flags); + } + if let Some(extends) = T::EXTENDS { + builder = builder.extends(extends()); + } + for iface in T::IMPLEMENTS { + builder = builder.implements(iface()); + } + for (name, value) in T::constants() { + builder = builder + .dyn_constant(*name, *value) + .expect("Failed to register constant"); } if let Some(modifier) = T::BUILDER_MODIFIER { builder = modifier(builder); diff --git a/src/class.rs b/src/class.rs index ca6ed21a89..9ab5051bab 100644 --- a/src/class.rs +++ b/src/class.rs @@ -10,7 +10,9 @@ use once_cell::sync::OnceCell; use crate::{ builders::{ClassBuilder, FunctionBuilder}, + convert::IntoZvalDyn, exception::PhpException, + flags::{MethodFlags, ClassFlags}, props::Property, zend::{ClassEntry, ExecuteData, ZendObjectHandlers}, }; @@ -25,6 +27,15 @@ pub trait RegisteredClass: Sized + 'static { /// class at runtime (add runtime constants etc). const BUILDER_MODIFIER: Option ClassBuilder>; + /// Parent class entry. Optional. + const EXTENDS: Option &'static ClassEntry>; + + /// Interfaces implemented by the class. + const IMPLEMENTS: &'static [fn() -> &'static ClassEntry]; + + /// PHP flags applied to the class. + const FLAGS: ClassFlags = ClassFlags::empty(); + /// Returns a reference to the class metadata, which stores the class entry /// and handlers. /// @@ -46,10 +57,13 @@ pub trait RegisteredClass: Sized + 'static { fn get_properties<'a>() -> HashMap<&'static str, Property<'a, Self>>; /// Returns the method builders required to build the class. - fn method_builders() -> Vec>; + fn method_builders() -> Vec<(FunctionBuilder<'static>, MethodFlags)>; /// Returns the class constructor (if any). fn constructor() -> Option>; + + /// Returns the constants provided by the class. + fn constants() -> &'static [(&'static str, &'static dyn IntoZvalDyn)]; } /// Stores metadata about a classes Rust constructor, including the function diff --git a/src/internal/class.rs b/src/internal/class.rs index 376efb93c9..baa43fbd4d 100644 --- a/src/internal/class.rs +++ b/src/internal/class.rs @@ -3,6 +3,8 @@ use std::{collections::HashMap, marker::PhantomData}; use crate::{ builders::FunctionBuilder, class::{ConstructorMeta, RegisteredClass}, + convert::{IntoZval, IntoZvalDyn}, + flags::MethodFlags, props::Property, }; @@ -17,9 +19,10 @@ impl Default for PhpClassImplCollector { } pub trait PhpClassImpl { - fn get_methods(self) -> Vec>; + fn get_methods(self) -> Vec<(FunctionBuilder<'static>, MethodFlags)>; fn get_method_props<'a>(self) -> HashMap<&'static str, Property<'a, T>>; fn get_constructor(self) -> Option>; + fn get_constants(self) -> &'static [(&'static str, &'static dyn IntoZvalDyn)]; } // Default implementation for classes without an `impl` block. Classes that do @@ -28,20 +31,33 @@ pub trait PhpClassImpl { // `dtolnay` specialisation: https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md impl PhpClassImpl for &'_ PhpClassImplCollector { #[inline] - fn get_methods(self) -> Vec> { - println!("&get_methods"); + fn get_methods(self) -> Vec<(FunctionBuilder<'static>, MethodFlags)> { Default::default() } #[inline] fn get_method_props<'a>(self) -> HashMap<&'static str, Property<'a, T>> { - println!("&get_method_props"); Default::default() } #[inline] fn get_constructor(self) -> Option> { - println!("&get_constructor"); Default::default() } + + #[inline] + fn get_constants(self) -> &'static [(&'static str, &'static dyn IntoZvalDyn)] { + &[] + } +} + +// This implementation is only used for `TYPE` and `NULLABLE`. +impl IntoZval for PhpClassImplCollector { + const TYPE: crate::flags::DataType = T::TYPE; + const NULLABLE: bool = T::NULLABLE; + + #[inline] + fn set_zval(self, _: &mut crate::types::Zval, _: bool) -> crate::error::Result<()> { + unreachable!(); + } } diff --git a/src/macros.rs b/src/macros.rs index 07d60717e5..5b1a396bef 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -301,6 +301,7 @@ macro_rules! class_derives { const TYPE: $crate::flags::DataType = $crate::flags::DataType::Object(Some( <$type as $crate::class::RegisteredClass>::CLASS_NAME, )); + const NULLABLE: bool = false; #[inline] fn set_zval( From 2e2f36151131cf95614df945087ad25ab552a61e Mon Sep 17 00:00:00 2001 From: David Cole Date: Fri, 11 Nov 2022 13:59:44 +1300 Subject: [PATCH 04/16] tidy up --- crates/macros/src/class.rs | 23 +++-------------------- crates/macros/src/function.rs | 7 +++---- crates/macros/src/module.rs | 30 ++++++++++++++---------------- examples/hello_world.rs | 6 ++---- src/internal/mod.rs | 11 +++++++++++ 5 files changed, 33 insertions(+), 44 deletions(-) diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index b2225bfc49..9032b80d07 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -66,10 +66,8 @@ pub fn parser(args: AttributeArgs, mut input: syn::ItemStruct) -> Result parse_fields(fields.named.iter_mut())?, _ => vec![], }; - let (metadata, metadata_ident) = class_metadata(ident); let class_impl = generate_registered_class_impl( ident, - &metadata_ident, args.name.as_deref(), args.modifier.as_ref(), class_attrs.extends.as_ref(), @@ -80,7 +78,6 @@ pub fn parser(args: AttributeArgs, mut input: syn::ItemStruct) -> Result( Ok(result) } -/// Returns a class metadata definition alongside the ident to access the -/// metadata. -fn class_metadata(ident: &syn::Ident) -> (TokenStream, syn::Ident) { - let meta_ident = format_ident!( - "__INTERNAL_{}_METADATA", - ident.to_string().to_ascii_uppercase() - ); - ( - quote! { - static #meta_ident: ::ext_php_rs::class::ClassMetadata<#ident> = ::ext_php_rs::class::ClassMetadata::new(); - }, - meta_ident, - ) -} - /// Generates an implementation of `RegisteredClass` for struct `ident`. fn generate_registered_class_impl( ident: &syn::Ident, - metadata_ident: &syn::Ident, class_name: Option<&str>, modifier: Option<&syn::Ident>, extends: Option<&syn::Expr>, @@ -182,7 +163,9 @@ fn generate_registered_class_impl( #[inline] fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { - &#metadata_ident + static METADATA: ::ext_php_rs::class::ClassMetadata<#ident> = + ::ext_php_rs::class::ClassMetadata::new(); + &METADATA } fn get_properties<'a>() -> ::std::collections::HashMap< diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index 167225a407..7258771180 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -20,11 +20,10 @@ pub struct FnArgs { } pub fn wrap(input: syn::Path) -> Result { - let func_name = match input.get_ident() { - Some(ident) => ident, - None => bail!(input => "Pass a PHP function name into `wrap_function!()`."), + let Some(func_name) = input.get_ident() else { + bail!(input => "Pass a PHP function name into `wrap_function!()`."); }; - let builder_func = Ident::new(&format!("_internal_{}", func_name), Span::call_site()); + let builder_func = format_ident!("_internal_{func_name}"); let err = format!("Failed to build function `{}`.", func_name); Ok(quote! {{ diff --git a/crates/macros/src/module.rs b/crates/macros/src/module.rs index 97f230c741..d1c6b2e34d 100644 --- a/crates/macros/src/module.rs +++ b/crates/macros/src/module.rs @@ -8,24 +8,22 @@ pub fn parser(input: ItemFn) -> TokenStream { let stmts = &block.stmts; quote! { - static __EXT_PHP_RS_MODULE_STARTUP: ::parking_lot::Mutex< - ::std::option::Option<::ext_php_rs::builders::ModuleStartup> - > = ::parking_lot::const_mutex(::std::option::Option::None); - - #[doc(hidden)] - extern "C" fn __ext_php_rs_startup(ty: i32, mod_num: i32) -> i32 { - __EXT_PHP_RS_MODULE_STARTUP - .lock() - .take() - .expect("Module startup function has already been called.") - .startup(ty, mod_num) - .map(|_| 0) - .unwrap_or(1) - } - #[doc(hidden)] #[no_mangle] extern "C" fn get_module() -> *mut ::ext_php_rs::zend::ModuleEntry { + static __EXT_PHP_RS_MODULE_STARTUP: ::ext_php_rs::internal::ModuleStartupMutex = + ::ext_php_rs::internal::MODULE_STARTUP_INIT; + + extern "C" fn ext_php_rs_startup(ty: i32, mod_num: i32) -> i32 { + __EXT_PHP_RS_MODULE_STARTUP + .lock() + .take() + .expect("Module startup function has already been called.") + .startup(ty, mod_num) + .map(|_| 0) + .unwrap_or(1) + } + #[inline] fn internal(#inputs) #output { #(#stmts)* @@ -35,7 +33,7 @@ pub fn parser(input: ItemFn) -> TokenStream { env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION") )) - .startup_function(__ext_php_rs_startup); + .startup_function(ext_php_rs_startup); match builder.build() { Ok((entry, startup)) => { diff --git a/examples/hello_world.rs b/examples/hello_world.rs index a43516efa4..090369216b 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -9,7 +9,6 @@ use ext_php_rs::{ types::Zval, zend::ExecuteData, }; -use parking_lot::{const_mutex, Mutex}; // struct MyClass { // a: i32, @@ -125,9 +124,8 @@ impl TestClass { pub const SOME_CONSTANT: i32 = 5; pub const SOME_OTHER_STR: &'static str = "Hello, world!"; - #[constructor] - pub fn some_other_func(a: i32, b: i32) -> Self { - Self { a, b } + pub fn __construct(a: i32, b: i32) -> Self { + Self { a: a + 10, b: b + 10 } } #[optional(test)] diff --git a/src/internal/mod.rs b/src/internal/mod.rs index 559036fc17..b2209df249 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -1,8 +1,19 @@ //! Internal, public functions that are called from downstream extensions. +use parking_lot::{const_mutex, Mutex}; + +use crate::builders::ModuleStartup; + pub mod class; pub mod function; +/// A mutex type that contains a [`ModuleStartup`] instance. +pub type ModuleStartupMutex = Mutex>; + +/// The initialisation value for [`ModuleStartupMutex`]. By default the mutex +/// contains [`None`]. +pub const MODULE_STARTUP_INIT: ModuleStartupMutex = const_mutex(None); + /// Called by startup functions registered with the [`#[php_startup]`] macro. /// Initializes all classes that are defined by ext-php-rs (i.e. `Closure`). /// From 706803f9dcd50a1f26ea2af6f4fc8d5066619049 Mon Sep 17 00:00:00 2001 From: David Cole Date: Fri, 11 Nov 2022 15:07:37 +1300 Subject: [PATCH 05/16] update docs --- NEW_MACROS.md | 114 ++++++++++++++++++++++++ crates/macros/src/class.rs | 2 +- crates/macros/src/constant.rs | 49 ----------- crates/macros/src/lib.rs | 11 --- crates/macros/src/module.rs | 2 +- crates/macros/src/startup_function.rs | 2 +- examples/hello_world.rs | 120 ++++---------------------- src/builders/module.rs | 8 +- src/class.rs | 2 +- src/lib.rs | 32 ------- src/macros.rs | 27 ++++++ 11 files changed, 168 insertions(+), 201 deletions(-) create mode 100644 NEW_MACROS.md diff --git a/NEW_MACROS.md b/NEW_MACROS.md new file mode 100644 index 0000000000..97a661238e --- /dev/null +++ b/NEW_MACROS.md @@ -0,0 +1,114 @@ +# New Macro Transition + +The old macro system used a global state to be able to automatically register +functions and classes when the `#[php_module]` attribute is used. However, +global state can cause problems with incremental compilation and is not +recommended. + +To solve this, the macro system has been re-written but this will require +changes to user code. This document summarises the changes. + +## Startup Function + +TODO: allow users to pass in their own startup function (or maybe just let them +give builders to ModuleBuilder + +## Functions + +Mostly unchanged in terms of function definition, however you now need to +register the function with the module builder: + +```rs +use ext_php_rs::prelude::*; + +#[php_function] +pub fn hello_world() -> &'static str { + "Hello, world!" +} + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module + .function(wrap_function!(hello_world)) +} +``` + +## Classes + +Mostly unchanged in terms of the class and impl definitions, however you now +need to register the classes with the module builder: + +```rs +use ext_php_rs::prelude::*; + +#[php_class] +pub struct TestClass { + #[prop] + a: i32, + #[prop] + b: i32, +} + +#[php_impl] +impl TestClass { + #[rename("NEW_CONSTANT_NAME")] + pub const SOME_CONSTANT: i32 = 5; + pub const SOME_OTHER_STR: &'static str = "Hello, world!"; + + pub fn __construct(a: i32, b: i32) -> Self { + Self { a: a + 10, b: b + 10 } + } + + #[optional(test)] + #[defaults(a = 5, test = 100)] + pub fn test_camel_case(&self, a: i32, test: i32) { + println!("a: {} test: {}", a, test); + } + + fn x(&self) -> i32 { + 5 + } +} + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module + .class::() +} +``` + +## Constants + +The `#[php_const]` attribute has been deprecated. Register the constant +directly with the module builder: + +```rs +use ext_php_rs::prelude::*; + +const SOME_CONSTANT: i32 = 100; + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module + .constant(wrap_constant!(SOME_CONSTANT)) // SOME_CONSTANT = 100 + .constant(("CONST_NAME", SOME_CONSTANT)) // CONST_NAME = 100 +} +``` + +## Extern Block + +No changes. + +```rs +use ext_php_rs::prelude::*; + +#[php_extern] +extern "C" { + fn phpinfo() -> bool; +} + +fn some_rust_func() { + let x = unsafe { phpinfo() }; + println!("phpinfo: {x}"); +} +``` diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index 9032b80d07..e45138c396 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -145,7 +145,7 @@ fn generate_registered_class_impl( }); let flags = match flags { Some(flags) => flags.to_token_stream(), - None => quote! { ::ext_php_rs::flags::ClassFlags::empty() }.to_token_stream() + None => quote! { ::ext_php_rs::flags::ClassFlags::empty() }.to_token_stream(), }; quote! { impl ::ext_php_rs::class::RegisteredClass for #ident { diff --git a/crates/macros/src/constant.rs b/crates/macros/src/constant.rs index 1ae9644ff0..8b13789179 100644 --- a/crates/macros/src/constant.rs +++ b/crates/macros/src/constant.rs @@ -1,50 +1 @@ -use anyhow::{Result}; -use darling::ToTokens; -use proc_macro2::TokenStream; -use quote::quote; -use syn::{Expr, ItemConst}; -#[derive(Debug)] -pub struct Constant { - pub name: String, - // pub visibility: Visibility, - pub docs: Vec, - pub value: String, -} - -pub fn parser(input: ItemConst) -> Result { - // let mut state = STATE.lock(); - - // if state.startup_function.is_some() { - // bail!("Constants must be declared before you declare your startup - // function and module function."); } - - // state.constants.push(Constant { - // name: input.ident.to_string(), - // docs: get_docs(&input.attrs), - // value: input.expr.to_token_stream().to_string(), - // }); - - Ok(quote! { - #[allow(dead_code)] - #input - }) -} - -impl Constant { - pub fn val_tokens(&self) -> TokenStream { - let expr: Expr = - syn::parse_str(&self.value).expect("failed to parse previously parsed expr"); - expr.to_token_stream() - } - - // pub fn get_flags(&self) -> TokenStream { - // let flag = match self.visibility { - // Visibility::Public => quote! { Public }, - // Visibility::Protected => quote! { Protected }, - // Visibility::Private => quote! { Private }, - // }; - - // quote! { ::ext_php_rs::flags::ConstantFlags} - // } -} diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index d767e06462..9a91d842bd 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -73,17 +73,6 @@ pub fn php_impl(args: TokenStream, input: TokenStream) -> TokenStream { .into() } -#[proc_macro_attribute] -pub fn php_const(_: TokenStream, input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemConst); - - match constant::parser(input) { - Ok(parsed) => parsed, - Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), - } - .into() -} - #[proc_macro_attribute] pub fn php_extern(_: TokenStream, input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemForeignMod); diff --git a/crates/macros/src/module.rs b/crates/macros/src/module.rs index d1c6b2e34d..a96e6a2eda 100644 --- a/crates/macros/src/module.rs +++ b/crates/macros/src/module.rs @@ -1,4 +1,4 @@ -use proc_macro2::{TokenStream}; +use proc_macro2::TokenStream; use quote::quote; use syn::{ItemFn, Signature}; diff --git a/crates/macros/src/startup_function.rs b/crates/macros/src/startup_function.rs index da9ce38bbe..f7ae8d13f8 100644 --- a/crates/macros/src/startup_function.rs +++ b/crates/macros/src/startup_function.rs @@ -123,7 +123,7 @@ pub fn parser(input: ItemFn) -> Result { // Ok(quote! {{ // let builder = -// ::ext_php_rs::builders::ClassBuilder::new(#class_name) +// ::ext_php_rs::builders::ClassBuilder::new(#class_name) // #(#methods)* #(#constants)* // #(#interfaces)* // // #(#properties)* diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 090369216b..d2aea50e2e 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -10,106 +10,6 @@ use ext_php_rs::{ zend::ExecuteData, }; -// struct MyClass { -// a: i32, -// b: i32, -// } - -// static __INTERNAL_MYCLASS_METADATA: ClassMetadata = -// ClassMetadata::new(); - -// fn test_modifier(c: ClassBuilder) -> ClassBuilder { -// println!("in test modifier"); -// c.constant("FAKE_CONST", 5).unwrap() -// } - -// impl RegisteredClass for MyClass { -// const CLASS_NAME: &'static str = "MyClass"; -// const BUILDER_MODIFIER: Option< -// fn(ext_php_rs::builders::ClassBuilder) -> -// ext_php_rs::builders::ClassBuilder, > = Some(test_modifier); - -// #[inline] -// fn get_metadata() -> &'static ext_php_rs::class::ClassMetadata { -// &__INTERNAL_MYCLASS_METADATA -// } - -// #[inline] -// fn get_properties<'a>( -// ) -> std::collections::HashMap<&'static str, -// ext_php_rs::props::Property<'a, Self>> { Default::default() -// } - -// #[inline] -// fn method_builders() -> Vec> { -// PhpClassImplCollector::::default().get_methods() -// } - -// #[inline] -// fn constructor() -> Option> { -// PhpClassImplCollector::::default().get_constructor() -// } -// } - -// impl MyClass { -// pub fn __construct(a: i32, b: i32) -> Self { -// Self { a, b } -// } - -// pub fn calc(&self, c: i32) -> i32 { -// self.a * self.b * c -// } -// } - -// impl PhpClassImpl for PhpClassImplCollector { -// fn get_methods(self) -> -// Vec> { vec![{ -// ext_php_rs::zend_fastcall! { -// extern fn handler(ex: &mut ExecuteData, retval: &mut Zval) { -// let (parser, this) = ex.parser_method::(); -// let mut c = Arg::new("c", DataType::Long); -// if parser.arg(&mut c).parse().is_err() { -// return; -// } -// let ret = this.unwrap().calc(c.val().unwrap()); -// ret.set_zval(retval, false).unwrap(); -// } -// } -// FunctionBuilder::new("calc", handler) -// .arg(Arg::new("c", DataType::Long)) -// .returns(::TYPE, false, -// false) }] -// } - -// fn get_method_props<'a>( -// self, -// ) -> std::collections::HashMap<&'static str, -// ext_php_rs::props::Property<'a, MyClass>> { Default::default() -// } - -// fn get_constructor(self) -> -// Option> { fn -// constructor(ex: &mut ExecuteData) -> ConstructorResult { -// let mut a = Arg::new("a", DataType::Long); let mut b = -// Arg::new("b", DataType::Long); if ex.parser().arg(&mut -// a).arg(&mut b).parse().is_err() { return -// ConstructorResult::ArgError; } -// ConstructorResult::Ok(MyClass { -// a: a.val().unwrap(), -// b: b.val().unwrap(), -// }) -// } -// fn build_fn(func: FunctionBuilder) -> FunctionBuilder { -// func.arg(Arg::new("a", DataType::Long)) -// .arg(Arg::new("b", DataType::Long)) -// } -// Some(ConstructorMeta { -// constructor, -// build_fn, -// }) -// } -// } - #[php_class] pub struct TestClass { #[prop] @@ -125,7 +25,10 @@ impl TestClass { pub const SOME_OTHER_STR: &'static str = "Hello, world!"; pub fn __construct(a: i32, b: i32) -> Self { - Self { a: a + 10, b: b + 10 } + Self { + a: a + 10, + b: b + 10, + } } #[optional(test)] @@ -144,9 +47,24 @@ pub fn new_class() -> TestClass { TestClass { a: 1, b: 2 } } +#[php_function] +pub fn hello_world() -> &'static str { + "Hello, world!" +} + +pub const HELLO_WORLD: i32 = 100; + +#[php_extern] +extern "C" { + fn phpinfo() -> bool; +} + #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module .class::() + .function(wrap_function!(hello_world)) .function(wrap_function!(new_class)) + .constant(wrap_constant!(HELLO_WORLD)) + .constant(("CONST_NAME", HELLO_WORLD)) } diff --git a/src/builders/module.rs b/src/builders/module.rs index 63851dc352..bbe842389e 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -173,12 +173,12 @@ impl ModuleBuilder { /// /// # Arguments /// - /// * `const_` - Tuple containing the name and value of the constant. This - /// is a tuple to support the [`wrap_constant`] macro. + /// * `const` - Tuple containing the name and value of the constant. This is + /// a tuple to support the [`wrap_constant`] macro. /// /// [`wrap_constant`]: crate::wrap_constant - pub fn constant(mut self, const_: (&str, impl IntoConst + Send + 'static)) -> Self { - let (name, val) = const_; + pub fn constant(mut self, r#const: (&str, impl IntoConst + Send + 'static)) -> Self { + let (name, val) = r#const; self.constants .push((name.into(), Box::new(val) as Box)); self diff --git a/src/class.rs b/src/class.rs index 9ab5051bab..9c4fccc0ce 100644 --- a/src/class.rs +++ b/src/class.rs @@ -12,7 +12,7 @@ use crate::{ builders::{ClassBuilder, FunctionBuilder}, convert::IntoZvalDyn, exception::PhpException, - flags::{MethodFlags, ClassFlags}, + flags::{ClassFlags, MethodFlags}, props::Property, zend::{ClassEntry, ExecuteData, ZendObjectHandlers}, }; diff --git a/src/lib.rs b/src/lib.rs index c17a9c8ba0..1d7d86ad75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,6 @@ pub mod prelude { pub use crate::closure::Closure; pub use crate::exception::{PhpException, PhpResult}; pub use crate::php_class; - pub use crate::php_const; pub use crate::php_extern; pub use crate::php_function; pub use crate::php_impl; @@ -67,33 +66,6 @@ pub const PHP_DEBUG: bool = cfg!(php_debug); /// Whether the extension is compiled for PHP thread-safe mode. pub const PHP_ZTS: bool = cfg!(php_zts); -/// Attribute used to annotate constants to be exported to PHP. -/// -/// The declared constant is left intact (apart from the addition of the -/// `#[allow(dead_code)]` attribute in the case that you do not use the Rust -/// constant). -/// -/// These declarations must happen before you declare your [`macro@php_startup`] -/// function (or [`macro@php_module`] function if you do not have a startup -/// function). -/// -/// # Example -/// -/// ``` -/// # #![cfg_attr(windows, feature(abi_vectorcall))] -/// # use ext_php_rs::prelude::*; -/// #[php_const] -/// const TEST_CONSTANT: i32 = 100; -/// -/// #[php_const] -/// const ANOTHER_CONST: &str = "Hello, world!"; -/// # #[php_module] -/// # pub fn module(module: ModuleBuilder) -> ModuleBuilder { -/// # module -/// # } -/// ``` -pub use ext_php_rs_derive::php_const; - /// Attribute used to annotate `extern` blocks which are deemed as PHP /// functions. /// @@ -313,10 +285,6 @@ pub use ext_php_rs_derive::php_function; /// be called from Rust. Methods do generate an additional function, with an /// identifier in the format `_internal_php_#ident`. /// -/// Methods and constants are declared mostly the same as their global -/// counterparts, so read the documentation on the [`macro@php_function`] and -/// [`macro@php_const`] macros for more details. -/// /// The main difference is that the contents of the `impl` block *do not* need /// to be tagged with additional attributes - this macro assumes that all /// contents of the `impl` block are to be exported to PHP. diff --git a/src/macros.rs b/src/macros.rs index 5b1a396bef..908de5dd4d 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -410,6 +410,33 @@ macro_rules! php_println { }; } +/// Wraps a constant into the form expected by [`ModuleBuilder`]. +/// +/// All this does is return a tuple containg two values: +/// +/// * The name of the constant +/// * The value of the constant +/// +/// # Example +/// +/// ``` +/// use ext_php_rs::wrap_constant; +/// +/// const HELLO_WORLD: i32 = 150; +/// +/// assert_eq!(wrap_constant!(HELLO_WORLD), ("HELLO_WORLD", HELLO_WORLD)); +/// ``` +/// +/// ```no_run +/// use ext_php_rs::prelude::*; +/// +/// const HELLO_WORLD: i32 = 150; +/// +/// ModuleBuilder::new("ext-php-rs", "0.1.0") +/// .constant(wrap_constant!(HELLO_WORLD)); +/// ``` +/// +/// [`ModuleBuilder`]: crate::builders::ModuleBuilder #[macro_export] macro_rules! wrap_constant { ($name:ident) => { From 47c6e4f9c29f2270469e450e91f8927d59cf08a4 Mon Sep 17 00:00:00 2001 From: David Cole Date: Fri, 11 Nov 2022 15:28:51 +1300 Subject: [PATCH 06/16] add alternative startup func --- NEW_MACROS.md | 28 +- crates/macros/src/class.rs | 2 +- crates/macros/src/constant.rs | 1 - crates/macros/src/fastcall.rs | 5 + crates/macros/src/lib.rs | 20 +- crates/macros/src/method.rs | 1 - crates/macros/src/module.rs | 33 +- crates/macros/src/old_class.rs | 378 --------------------- crates/macros/src/old_impl.rs | 371 --------------------- crates/macros/src/old_method.rs | 456 -------------------------- crates/macros/src/startup_function.rs | 154 --------- examples/hello_world.rs | 8 +- src/lib.rs | 36 -- 13 files changed, 67 insertions(+), 1426 deletions(-) delete mode 100644 crates/macros/src/constant.rs delete mode 100644 crates/macros/src/method.rs delete mode 100644 crates/macros/src/old_class.rs delete mode 100644 crates/macros/src/old_impl.rs delete mode 100644 crates/macros/src/old_method.rs delete mode 100644 crates/macros/src/startup_function.rs diff --git a/NEW_MACROS.md b/NEW_MACROS.md index 97a661238e..e9851595d6 100644 --- a/NEW_MACROS.md +++ b/NEW_MACROS.md @@ -8,11 +8,6 @@ recommended. To solve this, the macro system has been re-written but this will require changes to user code. This document summarises the changes. -## Startup Function - -TODO: allow users to pass in their own startup function (or maybe just let them -give builders to ModuleBuilder - ## Functions Mostly unchanged in terms of function definition, however you now need to @@ -95,7 +90,7 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { } ``` -## Extern Block +## Extern No changes. @@ -112,3 +107,24 @@ fn some_rust_func() { println!("phpinfo: {x}"); } ``` + +## Startup Function + +The `#[php_startup]` macro has been deprecated. Instead, define a function with +the signature `fn(ty: i32, mod_num: i32) -> i32` and provide the function name +to the `#[php_module]` attribute: + +```rs +use ext_php_rs::prelude::*; + +fn startup(ty: i32, mod_num: i32) -> i32 { + // register extra classes, constants etc + 5.register_constant("SOME_CONST", mod_num); + 0 +} + +#[php_module(startup = "startup")] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module +} +``` diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index e45138c396..13aa2a0ded 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -1,7 +1,7 @@ use crate::prelude::*; use darling::FromMeta; use proc_macro2::TokenStream; -use quote::{format_ident, quote, ToTokens}; +use quote::{quote, ToTokens}; use syn::AttributeArgs; #[derive(Debug, Default, FromMeta)] diff --git a/crates/macros/src/constant.rs b/crates/macros/src/constant.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/crates/macros/src/constant.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/macros/src/fastcall.rs b/crates/macros/src/fastcall.rs index 5d8e4a2bd6..7e9e94535d 100644 --- a/crates/macros/src/fastcall.rs +++ b/crates/macros/src/fastcall.rs @@ -8,6 +8,11 @@ const ABI: &str = "vectorcall"; #[cfg(not(windows))] const ABI: &str = "C"; +/// Parses a function and sets the correct ABI to interact with PHP depending +/// on the OS. +/// +/// On Windows, this sets the extern ABI to vectorcall while on all other OS +/// it it to C. pub fn parser(mut input: ItemFn) -> Result { if let Some(abi) = &mut input.sig.abi { abi.name = Some(LitStr::new(ABI, Span::call_site())); diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 9a91d842bd..50e168b90d 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -1,20 +1,16 @@ mod class; -mod constant; mod extern_; mod fastcall; mod function; mod impl_; -mod method; mod module; -mod startup_function; mod syn_ext; mod zval; use proc_macro::TokenStream; use proc_macro2::Span; use syn::{ - parse_macro_input, AttributeArgs, DeriveInput, ItemConst, ItemFn, ItemForeignMod, ItemImpl, - ItemStruct, + parse_macro_input, AttributeArgs, DeriveInput, ItemFn, ItemForeignMod, ItemImpl, ItemStruct, }; extern crate proc_macro; @@ -44,19 +40,13 @@ pub fn php_function(args: TokenStream, input: TokenStream) -> TokenStream { } #[proc_macro_attribute] -pub fn php_module(_: TokenStream, input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemFn); - - module::parser(input).into() -} - -#[proc_macro_attribute] -pub fn php_startup(_: TokenStream, input: TokenStream) -> TokenStream { +pub fn php_module(args: TokenStream, input: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as AttributeArgs); let input = parse_macro_input!(input as ItemFn); - match startup_function::parser(input) { + match module::parser(args, input) { Ok(parsed) => parsed, - Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), + Err(e) => e.to_compile_error(), } .into() } diff --git a/crates/macros/src/method.rs b/crates/macros/src/method.rs deleted file mode 100644 index aef8e0587a..0000000000 --- a/crates/macros/src/method.rs +++ /dev/null @@ -1 +0,0 @@ -// pub fn parser() -> Result {} diff --git a/crates/macros/src/module.rs b/crates/macros/src/module.rs index a96e6a2eda..e0416d3e8f 100644 --- a/crates/macros/src/module.rs +++ b/crates/macros/src/module.rs @@ -1,13 +1,32 @@ +use darling::FromMeta; use proc_macro2::TokenStream; use quote::quote; -use syn::{ItemFn, Signature}; +use syn::{AttributeArgs, Ident, ItemFn, Signature}; -pub fn parser(input: ItemFn) -> TokenStream { +use crate::prelude::*; + +#[derive(Debug, Default, FromMeta)] +#[darling(default)] +pub struct ModuleArgs { + /// Optional function that will be called when the module starts up. + startup: Option, +} + +pub fn parser(args: AttributeArgs, input: ItemFn) -> Result { + let opts = match ModuleArgs::from_list(&args) { + Ok(opts) => opts, + Err(e) => bail!(input => "Failed to parse attribute options: {:?}", e), + }; + eprintln!("{:?}", opts); let ItemFn { sig, block, .. } = input; let Signature { output, inputs, .. } = sig; let stmts = &block.stmts; + let startup = match opts.startup { + Some(startup) => quote! { #startup(ty, mod_num) }, + None => quote! { 0i32 }, + }; - quote! { + Ok(quote! { #[doc(hidden)] #[no_mangle] extern "C" fn get_module() -> *mut ::ext_php_rs::zend::ModuleEntry { @@ -15,13 +34,15 @@ pub fn parser(input: ItemFn) -> TokenStream { ::ext_php_rs::internal::MODULE_STARTUP_INIT; extern "C" fn ext_php_rs_startup(ty: i32, mod_num: i32) -> i32 { - __EXT_PHP_RS_MODULE_STARTUP + let a = #startup; + let b = __EXT_PHP_RS_MODULE_STARTUP .lock() .take() .expect("Module startup function has already been called.") .startup(ty, mod_num) .map(|_| 0) - .unwrap_or(1) + .unwrap_or(1); + a | b } #[inline] @@ -43,5 +64,5 @@ pub fn parser(input: ItemFn) -> TokenStream { Err(e) => panic!("Failed to build PHP module: {:?}", e), } } - } + }) } diff --git a/crates/macros/src/old_class.rs b/crates/macros/src/old_class.rs deleted file mode 100644 index 978106f106..0000000000 --- a/crates/macros/src/old_class.rs +++ /dev/null @@ -1,378 +0,0 @@ -use std::collections::HashMap; - -use anyhow::{anyhow, bail, Context, Result}; -use darling::{FromMeta, ToTokens}; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use syn::parse::ParseStream; -use syn::{Attribute, AttributeArgs, Expr, Fields, FieldsNamed, ItemStruct, LitStr, Token}; - -#[derive(Debug, Default)] -pub struct Class { - pub class_name: String, - pub struct_path: String, - pub parent: Option, - pub interfaces: Vec, - pub docs: Vec, - pub methods: Vec, - pub constructor: Option, - pub constants: Vec, - pub properties: HashMap, - /// A function name called when creating the class entry. Given an instance - /// of `ClassBuilder` and must return it. - pub modifier: Option, -} - -#[derive(Debug)] -pub enum ParsedAttribute { - Extends(Expr), - Implements(Expr), - Property(PropertyAttr), - Comment(String), -} - -#[derive(Default, Debug, FromMeta)] -#[darling(default)] -pub struct AttrArgs { - name: Option, - modifier: Option, -} - -pub fn parser(args: AttributeArgs, mut input: ItemStruct) -> Result { - let args = AttrArgs::from_list(&args) - .map_err(|e| anyhow!("Unable to parse attribute arguments: {:?}", e))?; - - let mut parent = None; - let mut interfaces = vec![]; - let mut properties = HashMap::new(); - let mut comments = vec![]; - - input.attrs = { - let mut unused = vec![]; - for attr in input.attrs.into_iter() { - match parse_attribute(&attr)? { - Some(parsed) => match parsed { - ParsedAttribute::Extends(class) => { - parent = Some(class.to_token_stream().to_string()); - } - ParsedAttribute::Implements(class) => { - interfaces.push(class.to_token_stream().to_string()); - } - ParsedAttribute::Comment(comment) => { - comments.push(comment); - } - attr => bail!("Attribute `{:?}` is not valid for structs.", attr), - }, - None => unused.push(attr), - } - } - unused - }; - - if let Fields::Named(FieldsNamed { - brace_token: _, - named, - }) = &mut input.fields - { - for field in named.iter_mut() { - let mut docs = vec![]; - let mut attrs = vec![]; - attrs.append(&mut field.attrs); - - for attr in attrs.into_iter() { - let mut result_prop = None; - match parse_attribute(&attr)? { - Some(parsed) => match parsed { - ParsedAttribute::Property(prop) => { - let field_name = field - .ident - .as_ref() - .ok_or_else(|| anyhow!("Only named fields can be properties."))? - .to_string(); - let prop_name = prop.rename.unwrap_or_else(|| field_name.clone()); - result_prop = Some(( - prop_name, - Property::field( - field_name, - vec![], - prop.flags.map(|flags| flags.to_token_stream().to_string()), - ), - )); - } - ParsedAttribute::Comment(doc) => docs.push(doc), - _ => bail!("Attribute {:?} is not valid for struct fields.", attr), - }, - None => field.attrs.push(attr), - } - - if let Some(mut prop) = result_prop { - prop.1.docs.append(&mut docs); - properties.insert(prop.0, prop.1); - } - } - } - } - - let ItemStruct { ident, .. } = &input; - let class_name = args.name.unwrap_or_else(|| ident.to_string()); - let struct_path = ident.to_string(); - let class = Class { - class_name, - struct_path, - parent, - interfaces, - docs: comments, - properties, - modifier: args.modifier, - ..Default::default() - }; - - // let mut state = STATE.lock(); - - // if state.built_module { - // bail!("The `#[php_module]` macro must be called last to ensure functions - // and classes are registered."); } - - // if state.startup_function.is_some() { - // bail!("The `#[php_startup]` macro must be called after all the classes - // have been defined."); } - - // state.classes.insert(ident.to_string(), class); - - Ok(quote! { - #input - - ::ext_php_rs::class_derives!(#ident); - }) -} - -impl Class { - pub fn generate_registered_class_impl(&self) -> Result { - let self_ty = Ident::new(&self.struct_path, Span::call_site()); - let class_name = &self.class_name; - let meta = Ident::new(&format!("_{}_META", &self.struct_path), Span::call_site()); - let prop_tuples = self - .properties - .iter() - .map(|(name, prop)| prop.as_prop_tuple(name)); - let constructor = if let Some(constructor) = &self.constructor { - let func = Ident::new(&constructor.ident, Span::call_site()); - let args = constructor.get_arg_definitions(); - quote! { - Some(::ext_php_rs::class::ConstructorMeta { - constructor: Self::#func, - build_fn: { - use ::ext_php_rs::builders::FunctionBuilder; - fn build_fn(func: FunctionBuilder) -> FunctionBuilder { - func - #(#args)* - } - build_fn - } - }) - } - } else { - quote! { None } - }; - - Ok(quote! { - static #meta: ::ext_php_rs::class::ClassMetadata<#self_ty> = ::ext_php_rs::class::ClassMetadata::new(); - - impl ::ext_php_rs::class::RegisteredClass for #self_ty { - const CLASS_NAME: &'static str = #class_name; - const CONSTRUCTOR: ::std::option::Option< - ::ext_php_rs::class::ConstructorMeta - > = #constructor; - - fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { - &#meta - } - - fn get_properties<'a>() -> ::std::collections::HashMap<&'static str, ::ext_php_rs::props::Property<'a, Self>> { - use ::std::iter::FromIterator; - - ::std::collections::HashMap::from_iter([ - #(#prop_tuples)* - ]) - } - } - }) - } -} - -#[derive(Debug)] -pub struct Property { - pub ty: PropertyType, - pub docs: Vec, - #[allow(dead_code)] - pub flags: Option, -} - -#[derive(Debug)] -pub enum PropertyType { - Field { - field_name: String, - }, - Method { - getter: Option, - setter: Option, - }, -} - -impl Property { - pub fn add_getter(&mut self, new_getter: String) -> Result<()> { - match &mut self.ty { - PropertyType::Field { .. } => bail!("Cannot add getter to field property."), - PropertyType::Method { getter, setter: _ } => match getter { - Some(getter) => bail!( - "Attempted to add getter `{}` to property that already has a getter `{}`.", - new_getter, - getter - ), - None => { - getter.replace(new_getter); - Ok(()) - } - }, - } - } - - pub fn add_setter(&mut self, new_setter: String) -> Result<()> { - match &mut self.ty { - PropertyType::Field { .. } => bail!("Cannot add setter to field property."), - PropertyType::Method { getter: _, setter } => match setter { - Some(getter) => bail!( - "Attempted to add setter `{}` to property that already has a setter `{}`.", - new_setter, - getter - ), - None => { - setter.replace(new_setter); - Ok(()) - } - }, - } - } - - pub fn field(field_name: String, docs: Vec, flags: Option) -> Self { - Self { - ty: PropertyType::Field { field_name }, - docs, - flags, - } - } - - pub fn method(docs: Vec, flags: Option) -> Self { - Self { - ty: PropertyType::Method { - getter: None, - setter: None, - }, - docs, - flags, - } - } - - pub fn as_prop_tuple(&self, name: &str) -> TokenStream { - match &self.ty { - PropertyType::Field { field_name } => { - let field_name = Ident::new(field_name, Span::call_site()); - quote! { - (#name, ::ext_php_rs::props::Property::field(|obj: &mut Self| &mut obj.#field_name)), - } - } - PropertyType::Method { getter, setter } => { - let getter = if let Some(getter) = getter { - let ident = Ident::new(getter, Span::call_site()); - quote! { Some(Self::#ident) } - } else { - quote! { None } - }; - let setter = if let Some(setter) = setter { - let ident = Ident::new(setter, Span::call_site()); - quote! { Some(Self::#ident) } - } else { - quote! { None } - }; - quote! { - (#name, ::ext_php_rs::props::Property::method(#getter, #setter)), - } - } - } - } -} - -#[derive(Debug, Default)] -pub struct PropertyAttr { - pub rename: Option, - pub flags: Option, -} - -impl syn::parse::Parse for PropertyAttr { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let mut this = Self::default(); - while !input.is_empty() { - let field = input.parse::()?.to_string(); - input.parse::()?; - - match field.as_str() { - "rename" => { - this.rename.replace(input.parse::()?.value()); - } - "flags" => { - this.flags.replace(input.parse::()?); - } - _ => return Err(input.error("invalid attribute field")), - } - - let _ = input.parse::(); - } - - Ok(this) - } -} - -pub fn parse_attribute(attr: &Attribute) -> Result> { - let name = attr.path.to_token_stream().to_string(); - - Ok(match name.as_ref() { - "extends" => { - let meta: Expr = attr - .parse_args() - .map_err(|_| anyhow!("Unable to parse `#[{}]` attribute.", name))?; - Some(ParsedAttribute::Extends(meta)) - } - "implements" => { - let meta: Expr = attr - .parse_args() - .map_err(|_| anyhow!("Unable to parse `#[{}]` attribute.", name))?; - Some(ParsedAttribute::Implements(meta)) - } - "doc" => { - struct DocComment(pub String); - - impl syn::parse::Parse for DocComment { - fn parse(input: ParseStream) -> syn::Result { - input.parse::()?; - let comment: LitStr = input.parse()?; - Ok(Self(comment.value())) - } - } - - let comment: DocComment = - syn::parse2(attr.tokens.clone()).with_context(|| "Failed to parse doc comment")?; - Some(ParsedAttribute::Comment(comment.0)) - } - "prop" | "property" => { - let attr = if attr.tokens.is_empty() { - PropertyAttr::default() - } else { - attr.parse_args() - .map_err(|e| anyhow!("Unable to parse `#[{}]` attribute: {}", name, e))? - }; - - Some(ParsedAttribute::Property(attr)) - } - _ => None, - }) -} diff --git a/crates/macros/src/old_impl.rs b/crates/macros/src/old_impl.rs deleted file mode 100644 index 3f6674b416..0000000000 --- a/crates/macros/src/old_impl.rs +++ /dev/null @@ -1,371 +0,0 @@ -use std::collections::HashMap; - -use anyhow::{anyhow, bail, Result}; -use darling::{FromMeta, ToTokens}; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use syn::spanned::Spanned; -use syn::{Attribute, AttributeArgs, ItemImpl, Lit, Meta, NestedMeta}; - -use crate::helpers::get_docs; -use crate::{ - class::{Property, PropertyAttr}, - constant::Constant, - method, -}; - -#[derive(Debug, Clone)] -pub enum Visibility { - Public, - Protected, - Private, -} - -#[derive(Debug, Copy, Clone, FromMeta)] -pub enum RenameRule { - #[darling(rename = "none")] - None, - #[darling(rename = "camelCase")] - Camel, - #[darling(rename = "snake_case")] - Snake, -} - -impl Default for RenameRule { - fn default() -> Self { - RenameRule::Camel - } -} - -impl RenameRule { - /// Change case of an identifier. - /// - /// Magic methods are handled specially to make sure they're always cased - /// correctly. - pub fn rename(&self, name: impl AsRef) -> String { - let name = name.as_ref(); - match self { - RenameRule::None => name.to_string(), - rule => match name { - "__construct" => "__construct".to_string(), - "__destruct" => "__destruct".to_string(), - "__call" => "__call".to_string(), - "__call_static" => "__callStatic".to_string(), - "__get" => "__get".to_string(), - "__set" => "__set".to_string(), - "__isset" => "__isset".to_string(), - "__unset" => "__unset".to_string(), - "__sleep" => "__sleep".to_string(), - "__wakeup" => "__wakeup".to_string(), - "__serialize" => "__serialize".to_string(), - "__unserialize" => "__unserialize".to_string(), - "__to_string" => "__toString".to_string(), - "__invoke" => "__invoke".to_string(), - "__set_state" => "__set_state".to_string(), - "__clone" => "__clone".to_string(), - "__debug_info" => "__debugInfo".to_string(), - field => match rule { - Self::Camel => ident_case::RenameRule::CamelCase.apply_to_field(field), - Self::Snake => ident_case::RenameRule::SnakeCase.apply_to_field(field), - Self::None => unreachable!(), - }, - }, - } - } -} - -#[derive(Debug)] -pub enum ParsedAttribute { - Default(HashMap), - Optional(String), - Visibility(Visibility), - Rename(String), - Property { - prop_name: Option, - ty: PropAttrTy, - }, - Constructor, - This, -} - -#[derive(Default, Debug, FromMeta)] -#[darling(default)] -pub struct AttrArgs { - rename_methods: Option, -} - -#[derive(Debug)] -pub enum PropAttrTy { - Getter, - Setter, -} - -pub fn parser(args: AttributeArgs, input: ItemImpl) -> Result { - let args = AttrArgs::from_list(&args) - .map_err(|e| anyhow!("Unable to parse attribute arguments: {:?}", e))?; - - let ItemImpl { self_ty, items, .. } = input; - let class_name = self_ty.to_token_stream().to_string(); - - if input.trait_.is_some() { - bail!("This macro cannot be used on trait implementations."); - } - - let mut constructor = None; - // let tokens = items - // .into_iter() - // .map(|item| { - // Ok(match item { - // syn::ImplItem::Const(constant) => { - // // class.constants.push(Constant { - // // name: constant.ident.to_string(), - // // // visibility: Visibility::Public, - // // docs: get_docs(&constant.attrs), - // // value: constant.expr.to_token_stream().to_string(), - // // }); - - // // quote! { - // // #[allow(dead_code)] - // // #constant - // // } - // todo!("class constants") - // } - // syn::ImplItem::Method(method) => { - // let parsed_method = - // method::parser(&self_ty, method, - // args.rename_methods.unwrap_or_default())?; - - // // TODO(david): How do we handle comments for getter/setter? - // Take the comments // // from the methods?? - // if let Some((prop, ty)) = parsed_method.property { - // // let prop = class - // // .properties - // // .entry(prop) - // // .or_insert_with(|| Property::method(vec![], - // None)); // let ident = - // parsed_method.method.orig_ident.clone(); - - // // match ty { - // // PropAttrTy::Getter => prop.add_getter(ident)?, - // // PropAttrTy::Setter => prop.add_setter(ident)?, - // // } - // todo!("class property methods") - // } - // if parsed_method.constructor { - // constructor = Some(parsed_method.method); - // } - // parsed_method.tokens - // } - // item => item.to_token_stream(), - // }) - // }) - // .collect::>>()?; - - let mut tokens = vec![]; - let mut methods = vec![]; - for item in items.into_iter() { - match item { - syn::ImplItem::Const(constant) => { - // class.constants.push(Constant { - // name: constant.ident.to_string(), - // // visibility: Visibility::Public, - // docs: get_docs(&constant.attrs), - // value: constant.expr.to_token_stream().to_string(), - // }); - - // quote! { - // #[allow(dead_code)] - // #constant - // } - todo!("class constants") - } - syn::ImplItem::Method(method) => { - let parsed_method = - method::parser(&self_ty, method, args.rename_methods.unwrap_or_default())?; - - // TODO(david): How do we handle comments for getter/setter? Take the comments - // // from the methods?? - if let Some((prop, ty)) = parsed_method.property { - // let prop = class - // .properties - // .entry(prop) - // .or_insert_with(|| Property::method(vec![], None)); - // let ident = parsed_method.method.orig_ident.clone(); - - // match ty { - // PropAttrTy::Getter => prop.add_getter(ident)?, - // PropAttrTy::Setter => prop.add_setter(ident)?, - // } - todo!("class property methods") - } - if parsed_method.constructor { - constructor = Some(parsed_method.method); - } else { - methods.push(parsed_method.method); - } - tokens.push(parsed_method.tokens); - } - item => tokens.push(item.to_token_stream()), - } - } - - let constructor = if let Some(constructor) = constructor { - let func = Ident::new(&constructor.ident, Span::call_site()); - let args = constructor.get_arg_definitions(); - quote! { - Some(::ext_php_rs::class::ConstructorMeta { - constructor: Self::#func, - build_fn: { - use ::ext_php_rs::builders::FunctionBuilder; - fn build_fn(func: FunctionBuilder) -> FunctionBuilder { - func - #(#args)* - } - build_fn - } - }) - } - } else { - quote! { None } - }; - let methods = methods.into_iter().map(|method| {}); - - Ok(quote! { - impl #self_ty { - #(#tokens)* - } - - impl ::ext_php_rs::internal::class::PhpClassMethods<#self_ty> for ::ext_php_rs::internal::class::PhpClassPropertyCollector<#self_ty> { - fn get_methods(self) -> ::std::vec::Vec<::ext_php_rs::builders::FunctionBuilder<'static>> { - } - - fn get_method_props<'a>(self) -> ::std::collections::HashMap<&'static str, ::ext_php_rs::props::Property<'a, #self_ty>> { - use ::std::iter::FromIterator; - - ::std::collections::HashMap::from_iter([]) - } - - fn get_constructor(self) -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta<#self_ty>> { - #constructor - } - } - }) -} - -pub fn parse_attribute(attr: &Attribute) -> Result> { - let name = attr.path.to_token_stream().to_string(); - let meta = attr - .parse_meta() - .map_err(|_| anyhow!("Unable to parse attribute."))?; - - Ok(Some(match name.as_ref() { - "defaults" => { - let defaults = HashMap::from_meta(&meta) - .map_err(|_| anyhow!("Unable to parse `#[default]` macro."))?; - ParsedAttribute::Default(defaults) - } - "optional" => { - let name = if let Meta::List(list) = meta { - if let Some(NestedMeta::Meta(meta)) = list.nested.first() { - Some(meta.to_token_stream().to_string()) - } else { - None - } - } else { - None - } - .ok_or_else(|| anyhow!("Invalid argument given for `#[optional]` macro."))?; - - ParsedAttribute::Optional(name) - } - "public" => ParsedAttribute::Visibility(Visibility::Public), - "protected" => ParsedAttribute::Visibility(Visibility::Protected), - "private" => ParsedAttribute::Visibility(Visibility::Private), - "rename" => { - let ident = if let Meta::List(list) = meta { - if let Some(NestedMeta::Lit(lit)) = list.nested.first() { - String::from_value(lit).ok() - } else { - None - } - } else { - None - } - .ok_or_else(|| anyhow!("Invalid argument given for `#[rename] macro."))?; - - ParsedAttribute::Rename(ident) - } - "getter" => { - let prop_name = if attr.tokens.is_empty() { - None - } else { - let parsed: PropertyAttr = attr - .parse_args() - .map_err(|e| anyhow!("Unable to parse `#[getter]` attribute: {}", e))?; - parsed.rename - }; - ParsedAttribute::Property { - prop_name, - ty: PropAttrTy::Getter, - } - } - "setter" => { - let prop_name = if attr.tokens.is_empty() { - None - } else { - let parsed: PropertyAttr = attr - .parse_args() - .map_err(|e| anyhow!("Unable to parse `#[setter]` attribute: {}", e))?; - parsed.rename - }; - ParsedAttribute::Property { - prop_name, - ty: PropAttrTy::Setter, - } - } - "constructor" => ParsedAttribute::Constructor, - "this" => ParsedAttribute::This, - _ => return Ok(None), - })) -} - -#[cfg(test)] -mod tests { - use super::RenameRule; - - #[test] - fn test_rename_magic() { - for &(magic, expected) in &[ - ("__construct", "__construct"), - ("__destruct", "__destruct"), - ("__call", "__call"), - ("__call_static", "__callStatic"), - ("__get", "__get"), - ("__set", "__set"), - ("__isset", "__isset"), - ("__unset", "__unset"), - ("__sleep", "__sleep"), - ("__wakeup", "__wakeup"), - ("__serialize", "__serialize"), - ("__unserialize", "__unserialize"), - ("__to_string", "__toString"), - ("__invoke", "__invoke"), - ("__set_state", "__set_state"), - ("__clone", "__clone"), - ("__debug_info", "__debugInfo"), - ] { - assert_eq!(magic, RenameRule::None.rename(magic)); - assert_eq!(expected, RenameRule::Camel.rename(magic)); - assert_eq!(expected, RenameRule::Snake.rename(magic)); - } - } - - #[test] - fn test_rename_php_methods() { - for &(original, camel, snake) in &[("get_name", "getName", "get_name")] { - assert_eq!(original, RenameRule::None.rename(original)); - assert_eq!(camel, RenameRule::Camel.rename(original)); - assert_eq!(snake, RenameRule::Snake.rename(original)); - } - } -} diff --git a/crates/macros/src/old_method.rs b/crates/macros/src/old_method.rs deleted file mode 100644 index d6bbaabc7c..0000000000 --- a/crates/macros/src/old_method.rs +++ /dev/null @@ -1,456 +0,0 @@ -use anyhow::{anyhow, bail, Result}; -use quote::ToTokens; -use std::collections::HashMap; -use syn::ReturnType; - -use crate::helpers::get_docs; -use crate::{ - function::{self, ParserType}, - impl_::{parse_attribute, ParsedAttribute, PropAttrTy, RenameRule, Visibility}, -}; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use syn::{punctuated::Punctuated, FnArg, ImplItemMethod, Lit, Pat, Token, Type}; - -#[derive(Debug, Clone)] -pub enum Arg { - Receiver(MethodType), - Typed(function::Arg), -} - -#[derive(Debug)] -pub struct AttrArgs { - pub defaults: HashMap, - pub optional: Option, - pub visibility: Visibility, -} - -#[derive(Debug, Clone)] -pub struct Method { - /// Method name - pub name: String, - /// extern "C" function ident - pub ident: String, - /// Rust internal function ident - pub orig_ident: String, - pub docs: Vec, - pub args: Vec, - pub optional: Option, - pub output: Option<(String, bool)>, - pub _static: bool, - pub visibility: Visibility, -} - -pub struct ParsedMethod { - pub tokens: TokenStream, - pub method: Method, - pub property: Option<(String, PropAttrTy)>, - pub constructor: bool, -} - -#[derive(Debug, Clone, Copy)] -pub enum MethodType { - Receiver { mutable: bool }, - ReceiverClassObject, - Static, -} - -impl ParsedMethod { - pub fn new( - tokens: TokenStream, - method: Method, - property: Option<(String, PropAttrTy)>, - constructor: bool, - ) -> Self { - Self { - tokens, - method, - property, - constructor, - } - } -} - -pub fn parser( - struct_ty: &Type, - mut input: ImplItemMethod, - rename_rule: RenameRule, -) -> Result { - let mut defaults = HashMap::new(); - let mut optional = None; - let mut visibility = Visibility::Public; - let mut as_prop = None; - let mut identifier = None; - let mut is_constructor = false; - let docs = get_docs(&input.attrs); - - for attr in input.attrs.iter() { - if let Some(attr) = parse_attribute(attr)? { - match attr { - ParsedAttribute::Default(list) => defaults = list, - ParsedAttribute::Optional(name) => optional = Some(name), - ParsedAttribute::Visibility(vis) => visibility = vis, - ParsedAttribute::Rename(ident) => identifier = Some(ident), - ParsedAttribute::Property { prop_name, ty } => { - if as_prop.is_some() { - bail!( - "Only one `#[getter]` and/or `#[setter]` attribute may be used per method." - ); - } - - let prop_name = prop_name.unwrap_or_else(|| { - input - .sig - .ident - .to_token_stream() - .to_string() - .trim_start_matches("get_") - .trim_start_matches("set_") - .to_string() - }); - as_prop = Some((prop_name, ty)) - } - ParsedAttribute::Constructor => is_constructor = true, - _ => bail!("Invalid attribute for method."), - } - } - } - - input.attrs.clear(); - - let ident = &input.sig.ident; - 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(struct_ty, &mut input.sig.inputs, &defaults)?; - let optional = function::find_optional_parameter( - args.iter().filter_map(|arg| match arg { - Arg::Typed(arg) => Some(arg), - _ => None, - }), - optional, - ); - let (arg_definitions, method_type) = build_arg_definitions(&args); - let arg_parser = build_arg_parser( - args.iter(), - &optional, - &bail, - match method_type { - MethodType::Static => ParserType::StaticMethod, - _ => ParserType::Method, - }, - )?; - let arg_accessors = build_arg_accessors(&args, &bail); - - let func = if is_constructor { - quote! { - #input - - #[doc(hidden)] - pub fn #internal_ident( - ex: &mut ::ext_php_rs::zend::ExecuteData - ) -> ::ext_php_rs::class::ConstructorResult { - use ::ext_php_rs::convert::IntoZval; - use ::ext_php_rs::class::ConstructorResult; - - #(#arg_definitions)* - #arg_parser - - Self::#ident(#(#arg_accessors,)*).into() - } - } - } else { - let this = match method_type { - MethodType::Receiver { .. } => quote! { this. }, - MethodType::ReceiverClassObject | MethodType::Static => quote! { Self:: }, - }; - - quote! { - #input - - ::ext_php_rs::zend_fastcall! { - #[doc(hidden)] - pub extern fn #internal_ident( - ex: &mut ::ext_php_rs::zend::ExecuteData, - retval: &mut ::ext_php_rs::types::Zval - ) { - use ::ext_php_rs::convert::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::exception::PhpException = e.into(); - e.throw().expect("Failed to throw exception"); - } - } - } - } - }; - - let method = Method { - name, - ident: internal_ident.to_string(), - orig_ident: ident.to_string(), - docs, - args, - optional, - output: get_return_type(struct_ty, &input.sig.output)?, - _static: matches!(method_type, MethodType::Static), - visibility, - }; - - Ok(ParsedMethod::new(func, method, as_prop, is_constructor)) -} - -pub fn get_return_type(self_ty: &Type, output_type: &ReturnType) -> Result> { - Ok(match output_type { - ReturnType::Default => None, - ReturnType::Type(_, ty) => { - let mut ty = ty.clone(); - replace_self(self_ty, &mut ty); - crate::function::Arg::from_type("".to_string(), &ty, None, true) - .map(|arg| (arg.ty, arg.nullable)) - } - }) -} - -/// Takes a type `ty` and replaces all instances of `Self` with the type -/// `self_ty`. -fn replace_self(self_ty: &Type, ty: &mut Type) { - match ty { - Type::Array(syn::TypeArray { elem, .. }) => replace_self(self_ty, elem), - Type::BareFn(syn::TypeBareFn { inputs, output, .. }) => { - for input in inputs { - replace_self(self_ty, &mut input.ty); - } - if let ReturnType::Type(_, ty) = output { - replace_self(self_ty, ty); - } - } - Type::Group(syn::TypeGroup { elem, .. }) => replace_self(self_ty, elem), - Type::Paren(syn::TypeParen { elem, .. }) => replace_self(self_ty, elem), - Type::Path(syn::TypePath { qself, path }) => { - if let Some(syn::QSelf { ty, .. }) = qself { - replace_self(self_ty, ty); - } - for seg in &mut path.segments { - if seg.ident == "Self" { - seg.ident = - Ident::new(&self_ty.to_token_stream().to_string(), Span::call_site()); - } - match &mut seg.arguments { - syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { - args, - .. - }) => { - for arg in args { - if let syn::GenericArgument::Type(ty) = arg { - replace_self(self_ty, ty); - } - } - } - syn::PathArguments::Parenthesized(syn::ParenthesizedGenericArguments { - inputs, - output, - .. - }) => { - for input in inputs { - replace_self(self_ty, input); - } - - if let ReturnType::Type(_, ty) = output { - replace_self(self_ty, ty); - } - } - _ => {} - } - } - } - Type::Ptr(syn::TypePtr { elem, .. }) => replace_self(self_ty, elem), - Type::Reference(syn::TypeReference { elem, .. }) => replace_self(self_ty, elem), - Type::Slice(syn::TypeSlice { elem, .. }) => replace_self(self_ty, elem), - Type::Tuple(syn::TypeTuple { elems, .. }) => { - for elem in elems { - replace_self(self_ty, elem); - } - } - _ => {} - } -} - -fn build_args( - struct_ty: &Type, - inputs: &mut Punctuated, - defaults: &HashMap, -) -> Result> { - inputs - .iter_mut() - .map(|arg| match arg { - FnArg::Receiver(receiver) => { - if receiver.reference.is_none() { - bail!("`self` parameter must be a reference."); - } - Ok(Arg::Receiver(MethodType::Receiver { - mutable: receiver.mutability.is_some(), - })) - } - FnArg::Typed(ty) => { - let mut this = false; - let attrs = std::mem::take(&mut ty.attrs); - for attr in attrs.into_iter() { - if let Some(attr) = parse_attribute(&attr)? { - match attr { - ParsedAttribute::This => this = true, - _ => bail!("Invalid attribute for argument."), - } - } - } - - if this { - Ok(Arg::Receiver(MethodType::ReceiverClassObject)) - } else { - let name = match &*ty.pat { - Pat::Ident(pat) => pat.ident.to_string(), - _ => bail!("Invalid parameter type."), - }; - let default = defaults.get(&name); - let mut ty = ty.ty.clone(); - replace_self(struct_ty, &mut ty); - - Ok(Arg::Typed( - crate::function::Arg::from_type(name.clone(), &ty, default, false) - .ok_or_else(|| anyhow!("Invalid parameter type for `{}`.", name))?, - )) - } - } - }) - .collect() -} - -fn build_arg_definitions(args: &[Arg]) -> (Vec, MethodType) { - let mut method_type = MethodType::Static; - - ( - args.iter() - .filter_map(|ty| match ty { - Arg::Receiver(t) => { - method_type = *t; - None - } - Arg::Typed(arg) => { - let ident = arg.get_name_ident(); - let definition = arg.get_arg_definition(); - Some(quote! { - let mut #ident = #definition; - }) - } - }) - .collect(), - method_type, - ) -} - -fn build_arg_parser<'a>( - args: impl Iterator, - optional: &Option, - ret: &TokenStream, - ty: ParserType, -) -> Result { - function::build_arg_parser( - args.filter_map(|arg| match arg { - Arg::Typed(arg) => Some(arg), - _ => None, - }), - optional, - ret, - ty, - ) -} - -fn build_arg_accessors(args: &[Arg], ret: &TokenStream) -> Vec { - args.iter() - .filter_map(|arg| match arg { - Arg::Typed(arg) => Some(arg.get_accessor(ret)), - Arg::Receiver(MethodType::ReceiverClassObject) => Some(quote! { this }), - _ => None, - }) - .collect() -} - -impl Method { - #[inline] - pub fn get_name_ident(&self) -> Ident { - 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.get_arg_definitions(); - let output = self.output.as_ref().map(|(ty, nullable)| { - let ty: Type = syn::parse_str(ty).unwrap(); - - // TODO allow reference returns? - quote! { - .returns(<#ty as ::ext_php_rs::convert::IntoZval>::TYPE, false, #nullable) - } - }); - - quote! { - ::ext_php_rs::builders::FunctionBuilder::new(#name, #class_path :: #name_ident) - #(#args)* - #output - .build() - } - } - - pub fn get_flags(&self) -> TokenStream { - let mut flags = vec![match self.visibility { - Visibility::Public => quote! { Public }, - Visibility::Protected => quote! { Protected }, - Visibility::Private => quote! { Private }, - }]; - - if self._static { - flags.push(quote! { Static }); - } - - flags - .iter() - .map(|flag| quote! { ::ext_php_rs::flags::MethodFlags::#flag }) - .collect::>() - .to_token_stream() - } -} diff --git a/crates/macros/src/startup_function.rs b/crates/macros/src/startup_function.rs deleted file mode 100644 index f7ae8d13f8..0000000000 --- a/crates/macros/src/startup_function.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::collections::HashMap; - -use anyhow::{anyhow, Result}; -use darling::FromMeta; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use syn::{AttributeArgs, Expr, ItemFn, Signature}; - -pub fn parser(input: ItemFn) -> Result { - Ok(quote! {}) - // let ItemFn { sig, block, .. } = input; - // let Signature { ident, .. } = sig; - // let stmts = &block.stmts; - - // // let mut state = STATE.lock(); - // // state.startup_function = Some(ident.to_string()); - - // // let classes = build_classes(&state.classes)?; - // // let constants = build_constants(&state.constants); - - // let func = quote! { - // #[doc(hidden)] - // pub extern "C" fn #ident(ty: i32, module_number: i32) -> i32 { - // use ::ext_php_rs::constant::IntoConst; - // use ::ext_php_rs::flags::PropertyFlags; - - // fn internal() { - // #(#stmts)* - // } - - // ::ext_php_rs::internal::ext_php_rs_startup(); - - // // #(#classes)* - // // #(#constants)* - - // // TODO return result? - // internal(); - - // 0 - // } - // }; - - // Ok(func) -} - -// /// Returns a vector of `ClassBuilder`s for each class. -// fn build_classes(classes: &HashMap) -> -// Result> { classes -// .iter() -// .map(|(name, class)| { -// let Class { class_name, .. } = &class; -// let ident = Ident::new(name, Span::call_site()); -// let meta = Ident::new(&format!("_{}_META", name), -// Span::call_site()); let methods = -// class.methods.iter().map(|method| { let builder = -// method.get_builder(&ident); let flags = method.get_flags(); -// quote! { .method(#builder.unwrap(), #flags) } -// }); -// let constants = class.constants.iter().map(|constant| { -// let name = &constant.name; -// let val = constant.val_tokens(); -// quote! { .constant(#name, #val).unwrap() } -// }); -// let parent = { -// if let Some(parent) = &class.parent { -// let expr: Expr = syn::parse_str(parent).map_err(|_| { -// anyhow!("Invalid expression given for `{}` parent", -// class_name) })?; -// Some(quote! { .extends(#expr) }) -// } else { -// None -// } -// }; -// let interfaces = class -// .interfaces -// .iter() -// .map(|interface| { -// let expr: Expr = syn::parse_str(interface).map_err(|_| { -// anyhow!( -// "Invalid expression given for `{}` interface: -// `{}`", class_name, -// interface -// ) -// })?; -// Ok(quote! { .implements(#expr) }) -// }) -// .collect::>>()?; -// // TODO(david): register properties for reflection (somehow) -// // let properties = class -// // .properties -// // .iter() -// // .map(|(name, (default, flags))| { -// // let default_expr: Expr = -// syn::parse_str(default).map_err(|_| { // anyhow!( -// // "Invalid default value given for property `{}` -// type: `{}`", // name, -// // default -// // ) -// // })?; -// // let flags_expr: Expr = syn::parse_str( -// // flags -// // .as_ref() -// // .map(|flags| flags.as_str()) -// // .unwrap_or("PropertyFlags::Public"), -// // ) -// // .map_err(|_| { -// // anyhow!( -// // "Invalid default value given for property `{}` -// type: `{}`", // name, -// // default -// // ) -// // })?; - -// // Ok(quote! { .property(#name, #default_expr, -// #flags_expr) }) // }) -// // .collect::>>()?; -// let class_modifier = class.modifier.as_ref().map(|modifier| { -// let modifier = Ident::new(modifier, Span::call_site()); -// quote! { -// let builder = #modifier(builder).expect(concat!("Failed -// to build class ", #class_name)); } -// }); - -// Ok(quote! {{ -// let builder = -// ::ext_php_rs::builders::ClassBuilder::new(#class_name) -// #(#methods)* #(#constants)* -// #(#interfaces)* -// // #(#properties)* -// #parent -// .object_override::<#ident>(); -// #class_modifier -// let class = builder.build() -// .expect(concat!("Unable to build class `", #class_name, -// "`")); - -// #meta.set_ce(class); -// }}) -// }) -// .collect::>>() -// } - -// fn build_constants(constants: &[Constant]) -> Vec { -// constants -// .iter() -// .map(|constant| { -// let name = &constant.name; -// let val = constant.val_tokens(); -// quote! { -// (#val).register_constant(#name, module_number).unwrap(); -// } -// }) -// .collect() -// } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index d2aea50e2e..ec0d14f922 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -2,6 +2,7 @@ use ext_php_rs::{ args::Arg, builders::{ClassBuilder, FunctionBuilder, ModuleStartup}, class::{ClassMetadata, ConstructorMeta, ConstructorResult, RegisteredClass}, + constant::IntoConst, convert::IntoZval, flags::DataType, internal::class::{PhpClassImpl, PhpClassImplCollector}, @@ -59,7 +60,12 @@ extern "C" { fn phpinfo() -> bool; } -#[php_module] +fn startup(ty: i32, mod_num: i32) -> i32 { + 5.register_constant("SOME_CONST", mod_num); + 0 +} + +#[php_module(startup = "startup")] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module .class::() diff --git a/src/lib.rs b/src/lib.rs index 1d7d86ad75..7213078bc1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,7 +50,6 @@ pub mod prelude { pub use crate::php_module; pub use crate::php_print; pub use crate::php_println; - pub use crate::php_startup; pub use crate::types::ZendCallable; pub use crate::wrap_constant; pub use crate::wrap_function; @@ -477,41 +476,6 @@ pub use ext_php_rs_derive::php_module; /// ``` pub use ext_php_rs_derive::php_class; -/// Annotates a function that will be called by PHP when the module starts up. -/// Generally used to register classes and constants. -/// -/// As well as annotating the function, any classes and constants that had been -/// declared using the [`macro@php_class`], [`macro@php_const`] and -/// [`macro@php_impl`] attributes will be registered inside this function. -/// -/// This function *must* be declared before the [`macro@php_module`] function, -/// as this function needs to be declared when building the module. -/// -/// This function will automatically be generated if not already declared with -/// this macro if you have registered any classes or constants when using the -/// [`macro@php_module`] macro. -/// -/// The attribute accepts one optional flag -- `#[php_startup(before)]` -- -/// which forces the annotated function to be called _before_ the other classes -/// and constants are registered. By default the annotated function is called -/// after these classes and constants are registered. -/// -/// # Example -/// -/// ``` -/// # #![cfg_attr(windows, feature(abi_vectorcall))] -/// # use ext_php_rs::prelude::*; -/// #[php_startup] -/// pub fn startup_function() { -/// // do whatever you need to do... -/// } -/// # #[php_module] -/// # pub fn module(module: ModuleBuilder) -> ModuleBuilder { -/// # module -/// # } -/// ``` -pub use ext_php_rs_derive::php_startup; - /// Derives the traits required to convert a struct or enum to and from a /// [`Zval`]. Both [`FromZval`] and [`IntoZval`] are implemented on types which /// use this macro. From 807073331dc833f4c10b014f1dd4d0a1a47edad8 Mon Sep 17 00:00:00 2001 From: David Cole Date: Fri, 11 Nov 2022 18:08:47 +1300 Subject: [PATCH 07/16] intozval etc. --- crates/macros/src/fastcall.rs | 5 ++--- crates/macros/src/function.rs | 7 ++++++- crates/macros/src/lib.rs | 6 +----- crates/macros/src/module.rs | 1 - crates/macros/src/syn_ext.rs | 9 +++++++++ crates/macros/src/zval.rs | 25 ++++++++++++++----------- examples/hello_world.rs | 16 +++++++++++++++- 7 files changed, 47 insertions(+), 22 deletions(-) diff --git a/crates/macros/src/fastcall.rs b/crates/macros/src/fastcall.rs index 7e9e94535d..b26ae2a10a 100644 --- a/crates/macros/src/fastcall.rs +++ b/crates/macros/src/fastcall.rs @@ -1,4 +1,3 @@ -use anyhow::Result; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use syn::{ItemFn, LitStr}; @@ -13,9 +12,9 @@ const ABI: &str = "C"; /// /// On Windows, this sets the extern ABI to vectorcall while on all other OS /// it it to C. -pub fn parser(mut input: ItemFn) -> Result { +pub fn parser(mut input: ItemFn) -> TokenStream { if let Some(abi) = &mut input.sig.abi { abi.name = Some(LitStr::new(ABI, Span::call_site())); } - Ok(input.to_token_stream()) + input.to_token_stream() } diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index 7258771180..9bc9804bfa 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -132,7 +132,12 @@ impl<'a> Function<'a> { .map(TypedArg::arg_decleration) .collect::>>()?; let arg_accessors = self.args.typed.iter().map(|arg| { - arg.accessor(|e| quote! { #e.throw().expect("Failed to throw PHP exception.") }) + arg.accessor(|e| { + quote! { + #e.throw().expect("Failed to throw PHP exception."); + return; + } + }) }); // `entry` impl diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 50e168b90d..a71b409d24 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -89,11 +89,7 @@ pub fn zval_convert_derive(input: TokenStream) -> TokenStream { pub fn zend_fastcall(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemFn); - match fastcall::parser(input) { - Ok(parsed) => parsed, - Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), - } - .into() + fastcall::parser(input).into() } #[proc_macro] diff --git a/crates/macros/src/module.rs b/crates/macros/src/module.rs index e0416d3e8f..cbec7dd3ea 100644 --- a/crates/macros/src/module.rs +++ b/crates/macros/src/module.rs @@ -17,7 +17,6 @@ pub fn parser(args: AttributeArgs, input: ItemFn) -> Result { Ok(opts) => opts, Err(e) => bail!(input => "Failed to parse attribute options: {:?}", e), }; - eprintln!("{:?}", opts); let ItemFn { sig, block, .. } = input; let Signature { output, inputs, .. } = sig; let stmts = &block.stmts; diff --git a/crates/macros/src/syn_ext.rs b/crates/macros/src/syn_ext.rs index 5c971476bd..ea2463845e 100644 --- a/crates/macros/src/syn_ext.rs +++ b/crates/macros/src/syn_ext.rs @@ -136,6 +136,7 @@ impl DropLifetimes for syn::TypePath { if let Some(qself) = &mut self.qself { qself.ty.drop_lifetimes(); } + self.path.segments.drop_lifetimes(); } } @@ -169,3 +170,11 @@ impl DropLifetimes for syn::TypeTuple { self.elems.iter_mut().for_each(|i| i.drop_lifetimes()); } } + +impl DropLifetimes for syn::punctuated::Punctuated { + fn drop_lifetimes(&mut self) { + for item in self { + item.drop_lifetimes(); + } + } +} diff --git a/crates/macros/src/zval.rs b/crates/macros/src/zval.rs index ca60bb2d80..f040e8265d 100644 --- a/crates/macros/src/zval.rs +++ b/crates/macros/src/zval.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Context, Result}; +use crate::prelude::*; use darling::ToTokens; use proc_macro2::{Span, TokenStream}; use quote::quote; @@ -83,7 +83,9 @@ pub fn parser(input: DeriveInput) -> Result { from_where_clause, ty_generics, ), - _ => bail!("Only structs and enums are supported by the `#[derive(ZvalConvert)]` macro."), + _ => { + bail!(ident => "Only structs and enums are supported by the `#[derive(ZvalConvert)]` macro.") + } } } @@ -100,9 +102,9 @@ fn parse_struct( .fields .iter() .map(|field| { - let ident = field.ident.as_ref().with_context(|| { - "Fields require names when using the `#[derive(ZvalConvert)]` macro on a struct." - })?; + let Some(ident) = field.ident.as_ref() else { + bail!(field.ident => "Fields require names when using `#[derive(ZvalConvert)]` on a struct."); + }; let field_name = ident.to_string(); Ok(quote! { @@ -115,9 +117,9 @@ fn parse_struct( .fields .iter() .map(|field| { - let ident = field.ident.as_ref().with_context(|| { - "Fields require names when using the `#[derive(ZvalConvert)]` macro on a struct." - })?; + let Some(ident) = field.ident.as_ref() else { + bail!(field.ident => "Fields require names when using `#[derive(ZvalConvert)]` on a struct."); + }; let field_name = ident.to_string(); Ok(quote! { @@ -143,6 +145,7 @@ fn parse_struct( impl #into_impl_generics ::ext_php_rs::convert::IntoZval for #ident #ty_generics #into_where_clause { const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(None); + const NULLABLE: bool = false; fn set_zval(self, zv: &mut ::ext_php_rs::types::Zval, persistent: bool) -> ::ext_php_rs::error::Result<()> { use ::ext_php_rs::convert::{IntoZval, IntoZendObject}; @@ -203,7 +206,7 @@ fn parse_enum( match fields { syn::Fields::Unnamed(fields) => { if fields.unnamed.len() != 1 { - bail!("Enum variant must only have one field when using `#[derive(ZvalConvert)]`."); + bail!(variant => "Enum variant must only have one field when using `#[derive(ZvalConvert)]`."); } let ty = &fields.unnamed.first().unwrap().ty; @@ -216,7 +219,7 @@ fn parse_enum( }, syn::Fields::Unit => { if default.is_some() { - bail!("Only one enum unit type is valid as a default when using `#[derive(ZvalConvert)]`."); + bail!(variant => "Only one enum unit type is valid as a default when using `#[derive(ZvalConvert)]`."); } default.replace(quote! { @@ -224,7 +227,7 @@ fn parse_enum( }); Ok(None) } - _ => bail!("Enum variants must be unnamed and have only one field inside the variant when using `#[derive(ZvalConvert)]`.") + _ => bail!(variant => "Enum variants must be unnamed and have only one field inside the variant when using `#[derive(ZvalConvert)]`.") } }).collect::>>()?; let default = default.unwrap_or_else(|| quote! { None }); diff --git a/examples/hello_world.rs b/examples/hello_world.rs index ec0d14f922..66a7844cd7 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -60,8 +60,21 @@ extern "C" { fn phpinfo() -> bool; } +#[derive(Debug, ZvalConvert)] +pub struct TestZvalConvert<'a> { + a: i32, + b: i32, + c: &'a str, +} + +#[php_function] +pub fn get_zval_convert<'a>(z: TestZvalConvert<'a>) -> i32 { + dbg!(z); + 5 +} + fn startup(ty: i32, mod_num: i32) -> i32 { - 5.register_constant("SOME_CONST", mod_num); + 5.register_constant("SOME_CONST", mod_num).unwrap(); 0 } @@ -71,6 +84,7 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { .class::() .function(wrap_function!(hello_world)) .function(wrap_function!(new_class)) + .function(wrap_function!(get_zval_convert)) .constant(wrap_constant!(HELLO_WORLD)) .constant(("CONST_NAME", HELLO_WORLD)) } From ee4555190d8168698f43dcfd0ac302c1255424fd Mon Sep 17 00:00:00 2001 From: David Cole Date: Sat, 12 Nov 2022 21:22:37 +1300 Subject: [PATCH 08/16] start updating docs --- guide/src/getting-started/hello_world.md | 13 ++++--- guide/src/macros/index.md | 21 ----------- guide/src/macros/module.md | 45 ++++++++++++++---------- 3 files changed, 33 insertions(+), 46 deletions(-) diff --git a/guide/src/getting-started/hello_world.md b/guide/src/getting-started/hello_world.md index 05918bdd4c..a580fe1eba 100644 --- a/guide/src/getting-started/hello_world.md +++ b/guide/src/getting-started/hello_world.md @@ -57,13 +57,12 @@ Let's actually write the extension code now. We start by importing the basic extension. We will then write our basic `hello_world` function, which will take a string argument for the callers name, and we will return another string. Finally, we write a `get_module` function which is used by PHP to find out about -your module. The `#[php_module]` attribute automatically registers your new -function so we don't need to do anything except return the `ModuleBuilder` that -we were given. +your module. We must provide the defined function to the given `ModuleBuilder` +and then return the same object. -We also need to enable the `abi_vectorcall` feature when compiling for Windows. -This is a nightly-only feature so it is recommended to use the `#[cfg_attr]` -macro to not enable the feature on other operating systems. +We also need to enable the `abi_vectorcall` feature when compiling for Windows +(the first line). This is a nightly-only feature so it is recommended to use +the `#[cfg_attr]` macro to not enable the feature on other operating systems. ```rust,ignore #![cfg_attr(windows, feature(abi_vectorcall))] @@ -76,7 +75,7 @@ pub fn hello_world(name: &str) -> String { #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { - module + module.function(wrap_function!(hello_world)) } ``` diff --git a/guide/src/macros/index.md b/guide/src/macros/index.md index b77c76cef9..6767958579 100644 --- a/guide/src/macros/index.md +++ b/guide/src/macros/index.md @@ -5,33 +5,12 @@ are to be exported to PHP. This allows you to write Rust-like APIs that can be used from PHP without fiddling around with zvals. - [`php_module`] - Defines the function used by PHP to retrieve your extension. -- [`php_startup`] - Defines the extension startup function used by PHP to - initialize your extension. - [`php_function`] - Used to export a Rust function to PHP. - [`php_class`] - Used to export a Rust struct or enum as a PHP class. - [`php_impl`] - Used to export a Rust `impl` block to PHP, including all methods and constants. -- [`php_const`] - Used to export a Rust constant to PHP as a global constant. - -These macros do abuse the fact that (at the moment) proc macro expansion _seems_ -to happen orderly, on one single thread. It has been stated many times that this -order is undefined behaviour ([see here]), so these macros _could_ break at any -time with a `rustc` update (let's just keep our fingers crossed). - -The macros abuse this fact by storing a global state, which stores information -about all the constants, functions, methods and classes you have registered -throughout your crate. It is then read out of the state in the function tagged -with the `#[php_module]` attribute. This is why this function **must** be the -last function in your crate. - -In the case the ordering does change (or we find out that it already was not in -order), the most likely solution will be having to register your PHP exports -manually inside the `#[php_module]` function. [`php_module`]: ./module.md -[`php_startup`]: ./module_startup.md [`php_function`]: ./function.md [`php_class`]: ./structs.md [`php_impl`]: ./impl.md -[`php_const`]: ./constant.md -[see here]: https://github.com/rust-lang/reference/issues/578 diff --git a/guide/src/macros/module.md b/guide/src/macros/module.md index 273fd43872..783e4c09d4 100644 --- a/guide/src/macros/module.md +++ b/guide/src/macros/module.md @@ -6,38 +6,43 @@ name, version, functions and extra initialization functions. Regardless if you use this macro, your extension requires a `extern "C" fn get_module()` so that PHP can get this information. -Using the macro, any functions annotated with the `php_function` macro will be -automatically registered with the extension in this function. If you have -defined any constants or classes with their corresponding macros, a 'module -startup' function will also be generated if it has not already been defined. - -Automatically registering these functions requires you to define the module -function **after** all other functions have been registered, as macros are -expanded in-order, therefore this macro will not know that other functions have -been used after. - The function is renamed to `get_module` if you have used another name. The function is passed an instance of `ModuleBuilder` which allows you to register the following (if required): +- Functions, classes and constants - Extension and request startup and shutdown functions. - Read more about the PHP extension lifecycle [here](https://www.phpinternalsbook.com/php7/extensions_design/php_lifecycle.html). - PHP extension information function - Used by the `phpinfo()` function to get information about your extension. -- Functions not automatically registered - -Classes and constants are not registered in the `get_module` function. These are -registered inside the extension startup function. ## Usage ```rust,ignore # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; -# use ext_php_rs::prelude::*; -# use ext_php_rs::{info_table_start, info_table_row, info_table_end}; -# use ext_php_rs::php::module::ModuleEntry; +use ext_php_rs::{ + prelude::*, + php::module::ModuleEntry, + info_table_start, + info_table_row, + info_table_end +}; + +pub const MY_CUSTOM_CONST: &'static str = "Hello, world!"; + +#[php_class] +pub struct Test { + a: i32, + b: i32 +} + +#[php_function] +pub fn hello_world() -> &'static str { + "Hello, world!" +} + /// Used by the `phpinfo()` function and when you run `php -i`. /// This will probably be simplified with another macro eventually! pub extern "C" fn php_module_info(_module: *mut ModuleEntry) { @@ -48,6 +53,10 @@ pub extern "C" fn php_module_info(_module: *mut ModuleEntry) { #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { - module.info_function(php_module_info) + module + .constant(wrap_constant!(MY_CUSTOM_CONST)) + .class::() + .function(wrap_function!(hello_world)) + .info_function(php_module_info) } ``` From ab0bbe9cd74c519632119d66c6fe702c150e5006 Mon Sep 17 00:00:00 2001 From: David Cole Date: Sat, 12 Nov 2022 21:45:22 +1300 Subject: [PATCH 09/16] documentation --- NEW_MACROS.md | 7 ++++ crates/macros/src/class.rs | 3 ++ crates/macros/src/function.rs | 4 +- crates/macros/src/impl_.rs | 21 ++++++++-- crates/macros/src/zval.rs | 1 + examples/hello_world.rs | 9 ++++- guide/src/SUMMARY.md | 1 - guide/src/macros/classes.md | 61 ++++++++++++++++++------------ guide/src/macros/constant.md | 17 +++++---- guide/src/macros/impl.md | 52 +++++++++++++++---------- guide/src/macros/module_startup.md | 28 -------------- guide/src/macros/zval_convert.md | 22 +++++++++-- guide/src/types/class_object.md | 21 +++++----- guide/src/types/object.md | 18 +++++---- src/builders/module.rs | 6 +-- src/lib.rs | 2 +- src/macros.rs | 19 ++++++++-- 17 files changed, 178 insertions(+), 114 deletions(-) delete mode 100644 guide/src/macros/module_startup.md diff --git a/NEW_MACROS.md b/NEW_MACROS.md index e9851595d6..48b5745e38 100644 --- a/NEW_MACROS.md +++ b/NEW_MACROS.md @@ -37,6 +37,7 @@ need to register the classes with the module builder: use ext_php_rs::prelude::*; #[php_class] +#[derive(Debug)] pub struct TestClass { #[prop] a: i32, @@ -63,6 +64,12 @@ impl TestClass { fn x(&self) -> i32 { 5 } + + pub fn builder_pattern( + self_: &mut ZendClassObject, + ) -> &mut ZendClassObject { + dbg!(self_) + } } #[php_module] diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index 13aa2a0ded..2db1910f37 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -181,16 +181,19 @@ fn generate_registered_class_impl( fn method_builders() -> ::std::vec::Vec< (::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags) > { + use ::ext_php_rs::internal::class::PhpClassImpl; ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_methods() } #[inline] fn constructor() -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta> { + use ::ext_php_rs::internal::class::PhpClassImpl; ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_constructor() } #[inline] fn constants() -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn)] { + use ::ext_php_rs::internal::class::PhpClassImpl; ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_constants() } } diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index 9bc9804bfa..4236e48280 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -454,7 +454,7 @@ impl<'a> TypedArg<'a> { } }); Ok(quote! { - ::ext_php_rs::args::Arg::new(#name, <#ty as ::ext_php_rs::convert::FromZval>::TYPE) + ::ext_php_rs::args::Arg::new(#name, <#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE) #null #default }) @@ -465,7 +465,7 @@ impl<'a> TypedArg<'a> { let name = self.name; if let Some(default) = &self.default { quote! { - #name.val().unwrap_or(#default) + #name.val().unwrap_or(#default.into()) } } else if self.nullable { // Originally I thought we could just use the below case for `null` options, as diff --git a/crates/macros/src/impl_.rs b/crates/macros/src/impl_.rs index 2bad0edb63..0904da1855 100644 --- a/crates/macros/src/impl_.rs +++ b/crates/macros/src/impl_.rs @@ -266,21 +266,36 @@ impl<'a> ParsedImpl<'a> { opts.parse(&mut method.attrs)?; let args = Args::parse_from_fnargs(method.sig.inputs.iter(), opts.defaults)?; - let func = Function::parse(&method.sig, Some(opts.name), args, opts.optional)?; + let mut func = + Function::parse(&method.sig, Some(opts.name), args, opts.optional)?; if matches!(opts.ty, MethodTy::Constructor) { if self.constructor.replace(func).is_some() { bail!(method => "Only one constructor can be provided per class."); } } else { - let builder = func.function_builder(CallType::Method { + let call_type = CallType::Method { class: self.path, receiver: if func.args.receiver.is_some() { + // `&self` or `&mut self` MethodReceiver::Class + } else if func + .args + .typed + .first() + .map(|arg| arg.name == "self_") + .unwrap_or_default() + { + // `self_: &[mut] ZendClassObject` + // Need to remove arg from argument list + func.args.typed.pop(); + MethodReceiver::ZendClassObject } else { + // Static method MethodReceiver::Static }, - })?; + }; + let builder = func.function_builder(call_type)?; self.functions.push(FnBuilder { builder, vis: opts.vis, diff --git a/crates/macros/src/zval.rs b/crates/macros/src/zval.rs index f040e8265d..a5ae98b1ff 100644 --- a/crates/macros/src/zval.rs +++ b/crates/macros/src/zval.rs @@ -235,6 +235,7 @@ fn parse_enum( Ok(quote! { impl #into_impl_generics ::ext_php_rs::convert::IntoZval for #ident #ty_generics #into_where_clause { const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Mixed; + const NULLABLE: bool = false; fn set_zval( self, diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 66a7844cd7..1c59595299 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -7,10 +7,11 @@ use ext_php_rs::{ flags::DataType, internal::class::{PhpClassImpl, PhpClassImplCollector}, prelude::*, - types::Zval, + types::{ZendClassObject, Zval}, zend::ExecuteData, }; +#[derive(Debug)] #[php_class] pub struct TestClass { #[prop] @@ -41,6 +42,12 @@ impl TestClass { fn x(&self) -> i32 { 5 } + + pub fn builder_pattern( + self_: &mut ZendClassObject, + ) -> &mut ZendClassObject { + dbg!(self_) + } } #[php_function] diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 4108d1bda7..7e10011862 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -27,7 +27,6 @@ - [Async futures](./macros/impl.md#async) - [Macros](./macros/index.md) - [Module](./macros/module.md) - - [Module Startup Function](./macros/module_startup.md) - [Function](./macros/function.md) - [Classes](./macros/classes.md) - [`impl`s](./macros/impl.md) diff --git a/guide/src/macros/classes.md b/guide/src/macros/classes.md index 0935a02dc8..cbad051340 100644 --- a/guide/src/macros/classes.md +++ b/guide/src/macros/classes.md @@ -16,11 +16,10 @@ There are also additional macros that modify the class. These macros **must** be placed underneath the `#[php_class]` attribute. - `#[extends(ce)]` - Sets the parent class of the class. Can only be used once. - `ce` must be a valid Rust expression when it is called inside the - `#[php_module]` function. + `ce` must be a function with the signature `fn() -> &'static ClassEntry`. - `#[implements(ce)]` - Implements the given interface on the class. Can be used - multiple times. `ce` must be a valid Rust expression when it is called inside - the `#[php_module]` function. + multiple times. `ce` must be a function with the signature + `fn() -> &'static ClassEntry`. 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 publicly with @@ -67,7 +66,8 @@ This example creates a PHP class `Human`, adding a PHP property `address`. ```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; -# use ext_php_rs::prelude::*; +use ext_php_rs::prelude::*; + #[php_class] pub struct Human { name: String, @@ -75,10 +75,11 @@ pub struct Human { #[prop] address: String, } -# #[php_module] -# pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { -# module -# } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.class::() +} # fn main() {} ``` @@ -88,11 +89,14 @@ it in the `Redis\Exception` namespace: ```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; -use ext_php_rs::prelude::*; -use ext_php_rs::{exception::PhpException, zend::ce}; +use ext_php_rs::{ + prelude::*, + exception::PhpException, + zend::ce, +}; #[php_class(name = "Redis\\Exception\\RedisException")] -#[extends(ce::exception())] +#[extends(ce::exception)] #[derive(Default)] pub struct RedisException; @@ -101,25 +105,33 @@ pub struct RedisException; pub fn throw_exception() -> PhpResult { Err(PhpException::from_class::("Not good!".into())) } -# #[php_module] -# pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { -# module -# } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module + .class::() + .function(wrap_function!(throw_exception)) +} # fn main() {} ``` ## Implementing an Interface -To implement an interface, use `#[implements(ce)]` where `ce` is an expression returning a `ClassEntry`. +To implement an interface, use `#[implements(ce)]` where `ce` is a function returning a `ClassEntry`. The following example implements [`ArrayAccess`](https://www.php.net/manual/en/class.arrayaccess.php): + ```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; -use ext_php_rs::prelude::*; -use ext_php_rs::{exception::PhpResult, types::Zval, zend::ce}; +use ext_php_rs::{ + prelude::*, + exception::PhpResult, + types::Zval, + zend::ce, +}; #[php_class] -#[implements(ce::arrayaccess())] +#[implements(ce::arrayaccess)] #[derive(Default)] pub struct EvenNumbersArray; @@ -154,9 +166,10 @@ impl EvenNumbersArray { Err("Setting values is not supported".into()) } } -# #[php_module] -# pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { -# module -# } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.class::() +} # fn main() {} ``` diff --git a/guide/src/macros/constant.md b/guide/src/macros/constant.md index 72f83446ad..c09692c00b 100644 --- a/guide/src/macros/constant.md +++ b/guide/src/macros/constant.md @@ -1,4 +1,4 @@ -# `#[php_const]` +# `wrap_constant!()` Exports a Rust constant as a global PHP constant. The constant can be any type that implements `IntoConst`. @@ -8,14 +8,17 @@ that implements `IntoConst`. ```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; -# use ext_php_rs::prelude::*; -#[php_const] -const TEST_CONSTANT: i32 = 100; +use ext_php_rs::prelude::*; -#[php_const] +const TEST_CONSTANT: i32 = 100; const ANOTHER_STRING_CONST: &'static str = "Hello world!"; -# #[php_module] -# pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module + .constant(wrap_constant!(TEST_CONSTANT)) + .constant(wrap_constant!(ANOTHER_STRING_CONST)) +} # fn main() {} ``` diff --git a/guide/src/macros/impl.md b/guide/src/macros/impl.md index 1b872f1944..236b4dfc87 100644 --- a/guide/src/macros/impl.md +++ b/guide/src/macros/impl.md @@ -3,7 +3,8 @@ You can export an entire `impl` block to PHP. This exports all methods as well as constants to PHP on the class that it is implemented on. This requires the `#[php_class]` macro to already be used on the underlying struct. Trait -implementations cannot be exported to PHP. +implementations cannot be exported to PHP. Only one `impl` block can be exported +per class. If you do not want a function exported to PHP, you should place it in a separate `impl` block. @@ -20,9 +21,8 @@ Class methods can take a `&self` or `&mut self` parameter. They cannot take a consuming `self` parameter. Static methods can omit this `self` parameter. To access the underlying Zend object, you can take a reference to a -`ZendClassObject` in place of the self parameter, where the parameter is -annotated with the `#[this]` attribute. This can also be used to return a -reference to `$this`. +`ZendClassObject` in place of the self parameter, where the parameter must +be named `self_`. This can also be used to return a reference to `$this`. By default, all methods are renamed in PHP to the camel-case variant of the Rust method name. This can be changed on the `#[php_impl]` attribute, by passing one @@ -114,22 +114,28 @@ constant for the maximum age of a `Human`. ```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; -# use ext_php_rs::{prelude::*, types::ZendClassObject}; -# #[php_class] -# #[derive(Debug, Default)] -# pub struct Human { -# name: String, -# age: i32, -# #[prop] -# address: String, -# } +use ext_php_rs::{prelude::*, types::ZendClassObject}; + +#[php_class] +#[derive(Debug, Default)] +pub struct Human { + name: String, + age: i32, + #[prop] + address: String, +} + #[php_impl] impl Human { const MAX_AGE: i32 = 100; // No `#[constructor]` attribute required here - the name is `__construct`. pub fn __construct(name: String, age: i32) -> Self { - Self { name, age, address: String::new() } + Self { + name, + age, + address: String::new(), + } } #[getter] @@ -148,21 +154,25 @@ impl Human { } pub fn introduce(&self) { - println!("My name is {} and I am {} years old. I live at {}.", self.name, self.age, self.address); + println!( + "My name is {} and I am {} years old. I live at {}.", + self.name, self.age, self.address + ); } - pub fn get_raw_obj(#[this] this: &mut ZendClassObject) { - dbg!(this); + pub fn get_raw_obj(self_: &mut ZendClassObject) -> &mut ZendClassObject { + dbg!(self_) } pub fn get_max_age() -> i32 { Self::MAX_AGE } } -# #[php_module] -# pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { -# module -# } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.class::() +} # fn main() {} ``` diff --git a/guide/src/macros/module_startup.md b/guide/src/macros/module_startup.md deleted file mode 100644 index cd950171d9..0000000000 --- a/guide/src/macros/module_startup.md +++ /dev/null @@ -1,28 +0,0 @@ -# `#[php_startup]` - -Used to define the PHP extension startup function. This function is used to -register extension classes and constants with the PHP interpreter. - -This function is automatically generated if you have registered classes or -constants and have not already used this macro. If you do use this macro, it -will be automatically registered in the `get_module` function when you use the -`#[php_module]` attribute. - -Most of the time you won't need to use this macro as the startup function will -be automatically generated when required (if not already defined). - -Read more about what the module startup function is used for -[here.](https://www.phpinternalsbook.com/php7/extensions_design/php_lifecycle.html#module-initialization-minit) - -## Example - -```rust,no_run -# #![cfg_attr(windows, feature(abi_vectorcall))] -# extern crate ext_php_rs; -# use ext_php_rs::prelude::*; -#[php_startup] -pub fn startup_function() { - -} -# fn main() {} -``` diff --git a/guide/src/macros/zval_convert.md b/guide/src/macros/zval_convert.md index 876a4b1757..0033ef6bf6 100644 --- a/guide/src/macros/zval_convert.md +++ b/guide/src/macros/zval_convert.md @@ -38,7 +38,13 @@ pub fn give_object() -> ExampleClass<'static> { c: "Borrowed", } } -# #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module + .function(wrap_function!(take_object)) + .function(wrap_function!(give_object)) +} # fn main() {} ``` @@ -74,7 +80,11 @@ pub struct CompareVals> { pub fn take_object(obj: CompareVals) { dbg!(obj); } -# #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.function(wrap_function!(take_object)) +} # fn main() {} ``` @@ -120,7 +130,13 @@ pub fn test_union(val: UnionExample) { pub fn give_union() -> UnionExample<'static> { UnionExample::Long(5) } -# #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module + .function(wrap_function!(test_union)) + .function(wrap_function!(give_union)) +} # fn main() {} ``` diff --git a/guide/src/types/class_object.md b/guide/src/types/class_object.md index ed6125cf4a..fe90e66b56 100644 --- a/guide/src/types/class_object.md +++ b/guide/src/types/class_object.md @@ -25,17 +25,20 @@ pub struct Example { #[php_impl] impl Example { - // Even though this function doesn't have a `self` type, it is still treated as an associated method - // and not a static method. - pub fn builder_pattern(#[this] this: &mut ZendClassObject) -> &mut ZendClassObject { - // do something with `this` - this + // ext-php-rs treats the method as associated due to the `self_` argument. + // The argument _must_ be called `self_`. + pub fn builder_pattern( + self_: &mut ZendClassObject, + ) -> &mut ZendClassObject { + // do something with `self_` + self_ } } -# #[php_module] -# pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { -# module -# } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.class::() +} # fn main() {} ``` diff --git a/guide/src/types/object.md b/guide/src/types/object.md index 57f9371d28..546d5fa7ed 100644 --- a/guide/src/types/object.md +++ b/guide/src/types/object.md @@ -47,10 +47,11 @@ pub fn take_obj(obj: &mut ZendObject) -> &mut ZendObject { let _ = obj.set_property("hello", 5); dbg!(obj) } -# #[php_module] -# pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { -# module -# } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.function(wrap_function!(take_obj)) +} # fn main() {} ``` @@ -68,10 +69,11 @@ pub fn make_object() -> ZBox { let _ = obj.set_property("hello", 5); obj } -# #[php_module] -# pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { -# module -# } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.function(wrap_function!(make_object)) +} # fn main() {} ``` diff --git a/src/builders/module.rs b/src/builders/module.rs index bbe842389e..6fe83e2544 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -31,11 +31,11 @@ use std::{ffi::CString, fmt::Debug, mem, ptr}; /// /// #[no_mangle] /// pub extern "C" fn get_module() -> *mut ModuleEntry { -/// ModuleBuilder::new("ext-name", "ext-version") +/// let (entry, _) = ModuleBuilder::new("ext-name", "ext-version") /// .info_function(php_module_info) /// .build() -/// .unwrap() -/// .into_raw() +/// .unwrap(); +/// entry.into_raw() /// } /// ``` #[derive(Debug)] diff --git a/src/lib.rs b/src/lib.rs index 7213078bc1..6d042a1505 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -461,7 +461,7 @@ pub use ext_php_rs_derive::php_module; /// use ext_php_rs::zend::ce; /// /// #[php_class(name = "Redis\\Exception\\RedisException")] -/// #[extends(ce::exception())] +/// #[extends(ce::exception)] /// pub struct Example; /// /// #[php_function] diff --git a/src/macros.rs b/src/macros.rs index 908de5dd4d..bb08a6f239 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -200,7 +200,7 @@ macro_rules! throw { /// # Examples /// /// ``` -/// # use ext_php_rs::{convert::{IntoZval, FromZval}, types::{Zval, ZendObject}, class::{RegisteredClass}}; +/// # use ext_php_rs::{convert::{IntoZval, FromZval, IntoZvalDyn}, types::{Zval, ZendObject}, class::{RegisteredClass, ConstructorMeta}, builders::{ClassBuilder, FunctionBuilder}, zend::ClassEntry, flags::MethodFlags}; /// use ext_php_rs::class_derives; /// /// struct Test { @@ -210,8 +210,9 @@ macro_rules! throw { /// /// impl RegisteredClass for Test { /// const CLASS_NAME: &'static str = "Test"; -/// -/// const CONSTRUCTOR: Option> = None; +/// const BUILDER_MODIFIER: Option ClassBuilder> = None; +/// const EXTENDS: Option &'static ClassEntry> = None; +/// const IMPLEMENTS: &'static [fn() -> &'static ClassEntry] = &[]; /// /// fn get_metadata() -> &'static ext_php_rs::class::ClassMetadata { /// todo!() @@ -222,6 +223,18 @@ macro_rules! throw { /// { /// todo!() /// } +/// +/// fn method_builders() -> Vec<(FunctionBuilder<'static>, MethodFlags)> { +/// todo!() +/// } +/// +/// fn constructor() -> Option> { +/// todo!() +/// } +/// +/// fn constants() -> &'static [(&'static str, &'static dyn IntoZvalDyn)] { +/// todo!() +/// } /// } /// /// class_derives!(Test); From a98129daae2f07bb586671fc2aa749a950617133 Mon Sep 17 00:00:00 2001 From: Pierre Tondereau Date: Wed, 25 Oct 2023 17:39:22 +0200 Subject: [PATCH 10/16] correct clippy and stuff --- crates/macros/src/function.rs | 4 ++-- crates/macros/src/impl_.rs | 2 +- examples/hello_world.rs | 17 +++-------------- src/builders/module.rs | 1 - src/internal/mod.rs | 1 + tests/module.rs | 4 ++-- 6 files changed, 9 insertions(+), 20 deletions(-) diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index 4236e48280..41960b8281 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -375,10 +375,10 @@ impl<'a> Args<'a> { syn::Pat::Ident(syn::PatIdent { ident, .. }) => ident, _ => bail!(pat => "Unsupported argument."), }; - let nullable = type_is_nullable(&**ty)?; + let nullable = type_is_nullable(ty)?; result.typed.push(TypedArg { name: ident, - ty: &**ty, + ty, nullable, default: defaults.remove(ident), }); diff --git a/crates/macros/src/impl_.rs b/crates/macros/src/impl_.rs index 0904da1855..58860e82f4 100644 --- a/crates/macros/src/impl_.rs +++ b/crates/macros/src/impl_.rs @@ -38,7 +38,7 @@ pub struct ImplArgs { } /// Different types of rename rules for methods. -#[derive(Debug, Copy, Clone, FromMeta)] +#[derive(Debug, Default, Copy, Clone, FromMeta)] pub enum RenameRule { /// Methods won't be renamed. #[darling(rename = "none")] diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 1c59595299..0b73beb8a1 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,15 +1,4 @@ -use ext_php_rs::{ - args::Arg, - builders::{ClassBuilder, FunctionBuilder, ModuleStartup}, - class::{ClassMetadata, ConstructorMeta, ConstructorResult, RegisteredClass}, - constant::IntoConst, - convert::IntoZval, - flags::DataType, - internal::class::{PhpClassImpl, PhpClassImplCollector}, - prelude::*, - types::{ZendClassObject, Zval}, - zend::ExecuteData, -}; +use ext_php_rs::{constant::IntoConst, prelude::*, types::ZendClassObject}; #[derive(Debug)] #[php_class] @@ -75,12 +64,12 @@ pub struct TestZvalConvert<'a> { } #[php_function] -pub fn get_zval_convert<'a>(z: TestZvalConvert<'a>) -> i32 { +pub fn get_zval_convert(z: TestZvalConvert) -> i32 { dbg!(z); 5 } -fn startup(ty: i32, mod_num: i32) -> i32 { +fn startup(_ty: i32, mod_num: i32) -> i32 { 5.register_constant("SOME_CONST", mod_num).unwrap(); 0 } diff --git a/src/builders/module.rs b/src/builders/module.rs index 6fe83e2544..2137c9edc8 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -4,7 +4,6 @@ use crate::{ constant::IntoConst, error::Result, ffi::{ext_php_rs_php_build_id, ZEND_MODULE_API_NO}, - flags::MethodFlags, zend::{FunctionEntry, ModuleEntry}, PHP_DEBUG, PHP_ZTS, }; diff --git a/src/internal/mod.rs b/src/internal/mod.rs index b2209df249..5afbc7b1ee 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -12,6 +12,7 @@ pub type ModuleStartupMutex = Mutex>; /// The initialisation value for [`ModuleStartupMutex`]. By default the mutex /// contains [`None`]. +#[allow(clippy::declare_interior_mutable_const)] pub const MODULE_STARTUP_INIT: ModuleStartupMutex = const_mutex(None); /// Called by startup functions registered with the [`#[php_startup]`] macro. diff --git a/tests/module.rs b/tests/module.rs index f1ef22b852..0d7296f9ed 100644 --- a/tests/module.rs +++ b/tests/module.rs @@ -39,6 +39,6 @@ pub fn hello_world(name: String) -> String { } #[php_module] -pub fn module(module: ModuleBuilder) -> ModuleBuilder { - module +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.function(wrap_function!(hello_world)) } From f605c54346d6756b6a6bbb158c685799b618c927 Mon Sep 17 00:00:00 2001 From: Pierre Tondereau Date: Wed, 25 Oct 2023 17:57:44 +0200 Subject: [PATCH 11/16] fix closure impl --- php | 4 ---- src/closure.rs | 19 ++++++++++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) delete mode 100755 php diff --git a/php b/php deleted file mode 100755 index 299c74e1a3..0000000000 --- a/php +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -cargo build --example hello_world -php -dextension=target/debug/examples/libhello_world.dylib $@ diff --git a/src/closure.rs b/src/closure.rs index 03e02653c7..6f74354ff3 100644 --- a/src/closure.rs +++ b/src/closure.rs @@ -11,7 +11,7 @@ use crate::{ flags::{DataType, MethodFlags}, props::Property, types::Zval, - zend::ExecuteData, + zend::{ClassEntry, ExecuteData}, zend_fastcall, }; @@ -152,6 +152,10 @@ impl Closure { impl RegisteredClass for Closure { const CLASS_NAME: &'static str = "RustClosure"; + const BUILDER_MODIFIER: Option ClassBuilder> = None; + const EXTENDS: Option &'static ClassEntry> = None; + const IMPLEMENTS: &'static [fn() -> &'static ClassEntry] = &[]; + fn get_metadata() -> &'static ClassMetadata { &CLOSURE_META } @@ -159,6 +163,18 @@ impl RegisteredClass for Closure { fn get_properties<'a>() -> HashMap<&'static str, Property<'a, Self>> { HashMap::new() } + + fn method_builders() -> Vec<(FunctionBuilder<'static>, MethodFlags)> { + unimplemented!() + } + + fn constructor() -> Option> { + None + } + + fn constants() -> &'static [(&'static str, &'static dyn crate::convert::IntoZvalDyn)] { + unimplemented!() + } } class_derives!(Closure); @@ -171,6 +187,7 @@ class_derives!(Closure); /// /// This trait is automatically implemented on functions with up to 8 /// parameters. +#[allow(clippy::missing_safety_doc)] pub unsafe trait PhpClosure { /// Invokes the closure. fn invoke<'a>(&'a mut self, parser: ArgParser<'a, '_>, ret: &mut Zval); From b55e6c2cb313225460b3694e1ee64f0f6b8a9019 Mon Sep 17 00:00:00 2001 From: Pierre Tondereau Date: Wed, 25 Oct 2023 18:15:29 +0200 Subject: [PATCH 12/16] fix doc+skeptic tests --- guide/src/ini-settings.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/guide/src/ini-settings.md b/guide/src/ini-settings.md index 22820c5e2e..6ce4220179 100644 --- a/guide/src/ini-settings.md +++ b/guide/src/ini-settings.md @@ -14,8 +14,7 @@ All PHP INI definitions must be registered with PHP to get / set their values vi # use ext_php_rs::zend::IniEntryDef; # use ext_php_rs::flags::IniEntryPermission; -#[php_startup] -pub fn startup_function(ty: i32, module_number: i32) { +pub fn startup(ty: i32, mod_num: i32) -> i32 { let ini_entries: Vec = vec![ IniEntryDef::new( "my_extension.display_emoji".to_owned(), @@ -23,7 +22,13 @@ pub fn startup_function(ty: i32, module_number: i32) { IniEntryPermission::All, ), ]; - IniEntryDef::register(ini_entries, module_number); + IniEntryDef::register(ini_entries, mod_num); + 0 +} + +#[php_module(startup = "startup")] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module } # fn main() {} ``` @@ -38,11 +43,17 @@ The INI values are stored as part of the `GlobalExecutor`, and can be accessed v # use ext_php_rs::prelude::*; # use ext_php_rs::zend::ExecutorGlobals; -#[php_startup] -pub fn startup_function(ty: i32, module_number: i32) { +pub fn startup(ty: i32, module_number: i32) -> i32 { // Get all INI values let ini_values = ExecutorGlobals::get().ini_values(); // HashMap> let my_ini_value = ini_values.get("my_extension.display_emoji"); // Option> + 0 } + +#[php_module(startup = "startup")] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module +} + # fn main() {} ``` From 0823e5e7e23d0130a37c2a240b9124da33e930c9 Mon Sep 17 00:00:00 2001 From: Pierre Tondereau Date: Thu, 26 Oct 2023 11:50:40 +0200 Subject: [PATCH 13/16] fix and add some doc for startup function --- NEW_MACROS.md | 3 +++ src/lib.rs | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/NEW_MACROS.md b/NEW_MACROS.md index 48b5745e38..94ef2f03fe 100644 --- a/NEW_MACROS.md +++ b/NEW_MACROS.md @@ -120,6 +120,9 @@ fn some_rust_func() { The `#[php_startup]` macro has been deprecated. Instead, define a function with the signature `fn(ty: i32, mod_num: i32) -> i32` and provide the function name to the `#[php_module]` attribute: +`mod_num` is the module number assigned by PHP for its internal management. The `ModuleBuilder` always defaults this parameter to `0`. +`ty` is the type of module (flag) which can be set to `MODULE_PERSISTENT = 0`. +The return number is for internal purposes and should always be `0` in this case. It's an internal mechanism to tell to the ModuleBuilder weither to use the defautlt startup function or the overriden one. (see: `crates/macros/src/module.rs:L35`) ```rs use ext_php_rs::prelude::*; diff --git a/src/lib.rs b/src/lib.rs index 6d042a1505..b66fe8bab8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -424,10 +424,6 @@ pub use ext_php_rs_derive::php_module; /// * `#[implements(ce)]` - Implements an interface on the new class. Can be /// used multiple times, and `ce` may be any valid expression. /// -/// This attribute (and its associated structs) must be defined *above* the -/// startup function (which is annotated by the [`macro@php_startup`] macro, or -/// automatically generated just above the [`macro@php_module`] function). -/// /// Fields defined on the struct *are not* the same as PHP properties, and are /// only accessible from Rust. /// From 417f495090178cc07df9d00d827d0ee57fb59bef Mon Sep 17 00:00:00 2001 From: Pierre Tondereau Date: Thu, 26 Oct 2023 12:24:26 +0200 Subject: [PATCH 14/16] add attribute for windows feature --- examples/hello_world.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 0b73beb8a1..d135037494 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,3 +1,4 @@ +#![cfg_attr(windows, feature(abi_vectorcall))] use ext_php_rs::{constant::IntoConst, prelude::*, types::ZendClassObject}; #[derive(Debug)] From 397e805c26b2667f33050db05de1be3ee1591d38 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 24 Nov 2023 22:40:20 +0100 Subject: [PATCH 15/16] Cleanup --- crates/macros/src/class.rs | 56 ++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index 2db1910f37..5a384adc30 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -1,8 +1,8 @@ use crate::prelude::*; use darling::FromMeta; -use proc_macro2::TokenStream; +use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; -use syn::AttributeArgs; +use syn::{AttributeArgs, Expr, LitStr, Token}; #[derive(Debug, Default, FromMeta)] #[darling(default)] @@ -85,7 +85,7 @@ pub fn parser(args: AttributeArgs, mut input: syn::ItemStruct) -> Result( fields: impl Iterator, -) -> Result> { +) -> Result> { #[derive(Debug, Default, FromMeta)] #[darling(default)] struct FieldAttr { @@ -98,21 +98,20 @@ fn parse_fields<'a>( unparsed.append(&mut field.attrs); for attr in unparsed { if attr.path.is_ident("prop") { - let meta = match attr.parse_meta() { - Ok(meta) => meta, + let prop: PropertyAttr = match attr.parse_args() { + Ok(prop) => prop, Err(_) => bail!(attr => "Failed to parse attribute arguments"), }; let ident = field .ident .as_ref() .expect("Named field struct should have ident."); - let field_name = match meta { - syn::Meta::List(_) => FieldAttr::from_meta(&meta).unwrap(), - _ => FieldAttr::default(), - } - .rename - .unwrap_or_else(|| ident.to_string()); - result.push((field_name, ident)) + + let renamed = match &prop.rename { + Some(v) => v.clone(), + None => ident.to_string(), + }; + result.push((ident, renamed, prop)) } else { field.attrs.push(attr); } @@ -120,6 +119,35 @@ fn parse_fields<'a>( } Ok(result) } +#[derive(Debug, Default)] +pub struct PropertyAttr { + pub rename: Option, + pub flags: Option, +} + +impl syn::parse::Parse for PropertyAttr { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut this = Self::default(); + while !input.is_empty() { + let field = input.parse::()?.to_string(); + input.parse::()?; + + match field.as_str() { + "rename" => { + this.rename.replace(input.parse::()?.value()); + } + "flags" => { + this.flags.replace(input.parse::()?); + } + _ => return Err(input.error("invalid attribute field")), + } + + let _ = input.parse::(); + } + + Ok(this) + } +} /// Generates an implementation of `RegisteredClass` for struct `ident`. fn generate_registered_class_impl( @@ -128,7 +156,7 @@ fn generate_registered_class_impl( modifier: Option<&syn::Ident>, extends: Option<&syn::Expr>, implements: &[syn::Expr], - fields: &[(String, &syn::Ident)], + fields: &[(&syn::Ident, String, PropertyAttr)], flags: Option<&syn::Expr>, ) -> TokenStream { let ident_str = ident.to_string(); @@ -138,7 +166,7 @@ fn generate_registered_class_impl( }; let modifier = modifier.option_tokens(); let extends = extends.option_tokens(); - let fields = fields.iter().map(|(name, ident)| { + let fields = fields.iter().map(|(ident, name, _)| { quote! { (#name, ::ext_php_rs::props::Property::field(|this: &mut Self| &mut this.#ident)) } From ebe41701f3567b6f38c93cfb9d2374dff193da49 Mon Sep 17 00:00:00 2001 From: Pierre Tondereau Date: Tue, 28 Nov 2023 15:38:07 +0100 Subject: [PATCH 16/16] defaulting property attributes when `#[prop]` fails to parse. --- crates/macros/src/class.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index 5a384adc30..a40e77547f 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -100,7 +100,9 @@ fn parse_fields<'a>( if attr.path.is_ident("prop") { let prop: PropertyAttr = match attr.parse_args() { Ok(prop) => prop, - Err(_) => bail!(attr => "Failed to parse attribute arguments"), + // TODO: Add a better error explanation instead of just defaulting. + // `attr.parse_args()` require that `#[prop]` is followed by an expression (no empty content in parentheses or no parentheses). + Err(_) => PropertyAttr::default(), }; let ident = field .ident @@ -119,6 +121,7 @@ fn parse_fields<'a>( } Ok(result) } + #[derive(Debug, Default)] pub struct PropertyAttr { pub rename: Option,