diff --git a/Cargo.toml b/Cargo.toml index 528bfa8a3a..28c60e0260 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,9 @@ license = "MIT OR Apache-2.0" keywords = ["php", "ffi", "zend"] version = "0.13.1" authors = ["David Cole "] -edition = "2018" +edition = "2021" categories = ["api-bindings"] -exclude = ["/.github", "/.crates", "/guide"] +exclude = ["/.github", "/.crates"] [dependencies] bitflags = "2" @@ -49,3 +49,7 @@ members = [ [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docs"] + +[[example]] +name = "hello_world" +crate-type = ["cdylib"] diff --git a/README.md b/README.md index bd71e1650a..58e88f8ab1 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,8 @@ Contributions welcome include: When contributing, please keep in mind the following: - Create tests if possible. - Update the documentation if necessary. + - If your change is a [braking change](https://semver.org) a migration guide MUST be included. This + should be placed in the `guide/src/migration-guides` directory. - Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). We use these to automatically generate changelogs. Unless you explicitly state otherwise, any contribution intentionally submitted diff --git a/build.rs b/build.rs index 28da2a2466..25461ae580 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,7 @@ +//! The build script for ext-php-rs. +//! This script is responsible for generating the bindings to the PHP Zend API. +//! It also checks the PHP version for compatibility with ext-php-rs and sets +//! configuration flags accordingly. #[cfg_attr(windows, path = "windows_build.rs")] #[cfg_attr(not(windows), path = "unix_build.rs")] mod impl_; @@ -18,6 +22,7 @@ use impl_::Provider; const MIN_PHP_API_VER: u32 = 20200930; const MAX_PHP_API_VER: u32 = 20240924; +/// Provides information about the PHP installation. pub trait PHPProvider<'a>: Sized { /// Create a new PHP provider. fn new(info: &'a PHPInfo) -> Result; @@ -75,9 +80,11 @@ fn find_php() -> Result { }) } +/// Output of `php -i`. pub struct PHPInfo(String); impl PHPInfo { + /// Get the PHP info. pub fn get(php: &Path) -> Result { let cmd = Command::new(php) .arg("-i") @@ -100,6 +107,7 @@ impl PHPInfo { .try_into() } + /// Checks if thread safety is enabled. pub fn thread_safety(&self) -> Result { Ok(self .get_key("Thread Safety") @@ -107,6 +115,7 @@ impl PHPInfo { == "enabled") } + /// Checks if PHP was built with debug. pub fn debug(&self) -> Result { Ok(self .get_key("Debug Build") @@ -114,11 +123,13 @@ impl PHPInfo { == "yes") } + /// Get the php version. pub fn version(&self) -> Result<&str> { self.get_key("PHP Version") .context("Failed to get PHP version") } + /// Get the zend version. pub fn zend_version(&self) -> Result { self.get_key("PHP API") .context("Failed to get Zend version") diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index 67d05e25fb..96c3e176c6 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -19,3 +19,6 @@ quote = "1.0.9" proc-macro2 = "1.0.26" lazy_static = "1.4.0" anyhow = "1.0" + +[lints.rust] +missing_docs = "warn" diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index c10a015429..08587cf19b 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -1,255 +1,154 @@ -use std::collections::HashMap; - -use crate::STATE; -use anyhow::{anyhow, bail, Context, Result}; use darling::{FromMeta, ToTokens}; -use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro2::{Ident, TokenStream}; use quote::quote; use syn::parse::ParseStream; -use syn::{Attribute, AttributeArgs, Expr, Fields, FieldsNamed, ItemStruct, LitStr, Token}; +use syn::{Attribute, AttributeArgs, Expr, Fields, 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, -} +use crate::helpers::get_docs; +use crate::prelude::*; -#[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, + docs: 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); } } + self.docs = get_docs(attrs); + 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: 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 state = STATE.lock(); - - if state.built_module { - bail!("The `#[php_module]` macro must be called last to ensure functions and classes are registered."); - } + let mut class_attrs = ClassAttrs::default(); + class_attrs.parse(&mut input.attrs)?; - if state.startup_function.is_some() { - bail!("The `#[php_startup]` macro must be called after all the classes have been defined."); - } + let fields = match &mut input.fields { + Fields::Named(fields) => parse_fields(fields.named.iter_mut())?, + _ => vec![], + }; - state.classes.insert(ident.to_string(), class); + let class_impl = generate_registered_class_impl( + ident, + args.name.as_deref(), + args.modifier.as_ref(), + class_attrs.extends.as_ref(), + &class_attrs.implements, + &fields, + args.flags.as_ref(), + &class_attrs.docs, + ); Ok(quote! { #input + #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(()) - } - }, - } +fn parse_fields<'a>(fields: impl Iterator) -> Result>> { + #[derive(Debug, Default, FromMeta)] + #[darling(default)] + struct FieldAttr { + rename: Option, } - 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(()) + let mut result = vec![]; + for field in fields { + let mut docs = vec![]; + let mut property = None; + let mut unparsed = vec![]; + unparsed.append(&mut field.attrs); + + for attr in unparsed { + if let Some(parsed) = parse_attribute(&attr)? { + match parsed { + ParsedAttribute::Property(prop) => { + let ident = field + .ident + .as_ref() + .ok_or_else(|| err!(attr => "Only named fields can be properties."))?; + + property = Some((ident, prop)); + } + ParsedAttribute::Comment(doc) => docs.push(doc), } - }, + } else { + field.attrs.push(attr); + } } - } - pub fn field(field_name: String, docs: Vec, flags: Option) -> Self { - Self { - ty: PropertyType::Field { field_name }, - docs, - flags, + if let Some((ident, prop)) = property { + result.push(Property { + ident, + attr: prop, + docs, + }); } } - pub fn method(docs: Vec, flags: Option) -> Self { - Self { - ty: PropertyType::Method { - getter: None, - setter: None, - }, - docs, - flags, - } - } + Ok(result) +} - 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)] +pub struct Property<'a> { + pub ident: &'a syn::Ident, + pub attr: PropertyAttr, + pub docs: Vec, +} + +impl Property<'_> { + pub fn name(&self) -> String { + self.attr + .rename + .to_owned() + .unwrap_or_else(|| self.ident.to_string()) } } @@ -283,22 +182,16 @@ impl syn::parse::Parse for PropertyAttr { } } +#[derive(Debug)] +pub enum ParsedAttribute { + Property(PropertyAttr), + Comment(String), +} + 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); @@ -310,8 +203,8 @@ pub fn parse_attribute(attr: &Attribute) -> Result> { } } - let comment: DocComment = - syn::parse2(attr.tokens.clone()).with_context(|| "Failed to parse doc comment")?; + let comment: DocComment = syn::parse2(attr.tokens.clone()) + .map_err(|e| err!(attr => "Failed to parse doc comment {}", e))?; Some(ParsedAttribute::Comment(comment.0)) } "prop" | "property" => { @@ -319,7 +212,7 @@ pub fn parse_attribute(attr: &Attribute) -> Result> { PropertyAttr::default() } else { attr.parse_args() - .map_err(|e| anyhow!("Unable to parse `#[{}]` attribute: {}", name, e))? + .map_err(|e| err!(attr => "Unable to parse `#[{}]` attribute: {}", name, e))? }; Some(ParsedAttribute::Property(attr)) @@ -327,3 +220,108 @@ pub fn parse_attribute(attr: &Attribute) -> Result> { _ => None, }) } + +/// Generates an implementation of `RegisteredClass` for struct `ident`. +#[allow(clippy::too_many_arguments)] +fn generate_registered_class_impl( + ident: &syn::Ident, + class_name: Option<&str>, + modifier: Option<&syn::Ident>, + extends: Option<&syn::Expr>, + implements: &[syn::Expr], + fields: &[Property], + flags: Option<&syn::Expr>, + docs: &[String], +) -> 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(|prop| { + let name = prop.name(); + let ident = prop.ident; + let flags = prop + .attr + .flags + .as_ref() + .map(|flags| flags.to_token_stream()) + .unwrap_or(quote! { ::ext_php_rs::flags::PropertyFlags::Public }); + let docs = &prop.docs; + + quote! { + (#name, ::ext_php_rs::internal::property::PropertyInfo { + prop: ::ext_php_rs::props::Property::field(|this: &mut Self| &mut this.#ident), + flags: #flags, + docs: &[#(#docs,)*] + }) + } + }); + + let flags = match flags { + Some(flags) => flags.to_token_stream(), + None => quote! { ::ext_php_rs::flags::ClassFlags::empty() }.to_token_stream(), + }; + + let docs = quote! { + #(#docs)* + }; + + 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; + const DOC_COMMENTS: &'static [&'static str] = &[ + #docs + ]; + + #[inline] + fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { + static METADATA: ::ext_php_rs::class::ClassMetadata<#ident> = + ::ext_php_rs::class::ClassMetadata::new(); + &METADATA + } + + fn get_properties<'a>() -> ::std::collections::HashMap< + &'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self> + > { + use ::std::iter::FromIterator; + ::std::collections::HashMap::from_iter([ + #(#fields,)* + ]) + } + + #[inline] + 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, &'static [&'static str])] { + use ::ext_php_rs::internal::class::PhpClassImpl; + ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_constants() + } + } + } +} diff --git a/crates/macros/src/constant.rs b/crates/macros/src/constant.rs index 3a9516c26b..14c0383e67 100644 --- a/crates/macros/src/constant.rs +++ b/crates/macros/src/constant.rs @@ -1,53 +1,31 @@ -use crate::helpers::get_docs; -use anyhow::{bail, Result}; -use darling::ToTokens; use proc_macro2::TokenStream; -use quote::quote; -use syn::{Expr, ItemConst}; +use quote::{format_ident, quote}; +use syn::ItemConst; -use crate::STATE; +use crate::helpers::get_docs; +use crate::prelude::*; -#[derive(Debug)] -pub struct Constant { - pub name: String, - // pub visibility: Visibility, - pub docs: Vec, - pub value: String, -} +const INTERNAL_CONST_DOC_PREFIX: &str = "_internal_const_docs_"; -pub fn parser(input: ItemConst) -> Result { - let mut state = STATE.lock(); +pub fn parser(item: ItemConst) -> TokenStream { + let docs = get_docs(&item.attrs); + let docs_ident = format_ident!("{INTERNAL_CONST_DOC_PREFIX}{}", item.ident); - if state.startup_function.is_some() { - bail!("Constants must be declared before you declare your startup function and module function."); + quote! { + #item + #[allow(non_upper_case_globals)] + const #docs_ident: &[&str] = &[#(#docs),*]; } - - 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 wrap(input: syn::Path) -> Result { + let Some(const_name) = input.get_ident().map(|i| i.to_string()) else { + bail!(input => "Pass a PHP const into `wrap_constant!()`."); + }; + let doc_const = format_ident!("{INTERNAL_CONST_DOC_PREFIX}{const_name}"); - // pub fn get_flags(&self) -> TokenStream { - // let flag = match self.visibility { - // Visibility::Public => quote! { Public }, - // Visibility::Protected => quote! { Protected }, - // Visibility::Private => quote! { Private }, - // }; + Ok(quote! { + (#const_name, #input, #doc_const) - // quote! { ::ext_php_rs::flags::ConstantFlags} - // } + }) } diff --git a/crates/macros/src/extern_.rs b/crates/macros/src/extern_.rs index a629e82d01..22c47499d7 100644 --- a/crates/macros/src/extern_.rs +++ b/crates/macros/src/extern_.rs @@ -1,7 +1,11 @@ -use anyhow::{anyhow, bail, Result}; use proc_macro2::TokenStream; use quote::quote; -use syn::{punctuated::Punctuated, ForeignItemFn, ItemForeignMod, ReturnType, Signature, Token}; +use syn::{ + punctuated::Punctuated, spanned::Spanned as _, ForeignItemFn, ItemForeignMod, ReturnType, + Signature, Token, +}; + +use crate::prelude::*; pub fn parser(input: ItemForeignMod) -> Result { input @@ -9,7 +13,7 @@ pub fn parser(input: ItemForeignMod) -> Result { .into_iter() .map(|item| match item { syn::ForeignItem::Fn(func) => parse_function(func), - _ => bail!("Only `extern` functions are supported by PHP."), + _ => bail!(item => "Only `extern` functions are supported by PHP."), }) .collect::>>() .map(|vec| quote! { #(#vec)* }) @@ -36,7 +40,7 @@ fn parse_function(mut func: ForeignItemFn) -> Result { }) .collect::>>() .ok_or_else(|| { - anyhow!("`self` parameters are not permitted inside `#[php_extern]` blocks.") + err!(sig.span() => "`self` parameters are not permitted inside `#[php_extern]` blocks.") })?; let ret = build_return(&name, &sig.output, params); diff --git a/crates/macros/src/fastcall.rs b/crates/macros/src/fastcall.rs index 5d8e4a2bd6..5dbd2bd071 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}; @@ -8,9 +7,14 @@ const ABI: &str = "vectorcall"; #[cfg(not(windows))] const ABI: &str = "C"; -pub fn parser(mut input: ItemFn) -> Result { +/// 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 sets it to C. +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 670d32fedc..c3e704c327 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -1,479 +1,623 @@ use std::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 quote::quote; -use syn::{ - punctuated::Punctuated, AttributeArgs, FnArg, GenericArgument, ItemFn, Lit, PathArguments, - ReturnType, Signature, Token, Type, TypePath, -}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote}; +use syn::spanned::Spanned as _; +use syn::PatType; +use syn::{AttributeArgs, FnArg, GenericArgument, ItemFn, Lit, PathArguments, Type, TypePath}; + +use crate::helpers::get_docs; +use crate::prelude::*; +use crate::syn_ext::DropLifetimes; + +pub fn wrap(input: syn::Path) -> Result { + let Some(func_name) = input.get_ident() else { + bail!(input => "Pass a PHP function name into `wrap_function!()`."); + }; + let builder_func = format_ident!("_internal_{func_name}"); + + Ok(quote! {{ + (<#builder_func as ::ext_php_rs::internal::function::PhpFunction>::FUNCTION_ENTRY)() + }}) +} #[derive(Default, Debug, 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 variadic: bool, +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 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 docs = get_docs(&input.attrs); + + let func = Function::new(&input.sig, opts.name, args, opts.optional, docs)?; + let function_impl = func.php_function_impl()?; + + Ok(quote! { + #input + #function_impl + }) } -#[derive(Debug, Clone)] -pub struct Function { +#[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, + /// Doc comments for the function. pub docs: Vec, - pub ident: String, - pub args: Vec, - pub optional: Option, - pub output: Option<(String, bool)>, } -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), - }; +#[derive(Debug)] +pub enum CallType<'a> { + Function, + Method { + class: &'a syn::Path, + receiver: MethodReceiver, + }, +} - 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 +/// 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, +} - ::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; +impl<'a> Function<'a> { + /// 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 new( + sig: &'a syn::Signature, + name: Option, + args: Args<'a>, + optional: Option, + docs: Vec, + ) -> Result { + Ok(Self { + ident: &sig.ident, + name: name.unwrap_or_else(|| sig.ident.to_string()), + args, + output: match &sig.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, ty) => Some(&**ty), + }, + optional, + docs, + }) + } - #(#arg_definitions)* - #arg_parser + /// Generates an internal identifier for the function. + pub fn internal_ident(&self) -> Ident { + format_ident!("_internal_{}", &self.ident) + } - let result = #ident(#(#arg_accessors, )*); + /// 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 (required, not_required) = self.args.split_args(self.optional.as_ref()); - 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"); + // `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_declarations = self + .args + .typed + .iter() + .map(TypedArg::arg_declaration) + .collect::>>()?; + let arg_accessors = self.args.typed.iter().map(|arg| { + arg.accessor(|e| { + quote! { + #e.throw().expect("Failed to throw PHP exception."); + return; } - } - } - }; - - 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 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()); + // `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 returns = 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, + ) + } + }); - Ok((func, function)) -} + 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; + } -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."), + #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; + } + }; + }, }; - Arg::from_type(name.clone(), &ty.ty, defaults.get(&name), false) - .ok_or_else(|| anyhow!("Invalid parameter type for parameter `{}`.", name)) + 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 + } } - }) - .collect::>>() -} + }; -fn build_arg_definitions(args: &[Arg]) -> Vec { - args.iter() - .map(|ty| { - let ident = ty.get_name_ident(); - let definition = ty.get_arg_definition(); + let docs = if !self.docs.is_empty() { + let docs = &self.docs; quote! { - let mut #ident = #definition; + .docs(&[#(#docs),*]) } + } else { + quote! {} + }; + + 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_declarations)* + 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))* + #returns + #docs }) - .collect() -} - -pub fn find_optional_parameter<'a>( - args: impl DoubleEndedIterator, - optional: Option, -) -> Option { - if optional.is_some() { - return optional; } - let mut optional = None; + /// 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)?; - for arg in args.rev() { - if arg.nullable { - optional.replace(arg.name.clone()); - } else { - break; - } + 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::builders::FunctionBuilder<'static> = { + fn entry() -> ::ext_php_rs::builders::FunctionBuilder<'static> + { + #builder + } + entry + }; + } + }) } - optional -} - -pub enum ParserType { - Function, - Method, - StaticMethod, -} + /// 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::>>()?; -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 - }); + 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_declarations = self + .args + .typed + .iter() + .map(TypedArg::arg_declaration) + .collect::>>()?; + let arg_accessors = self.args.typed.iter().map(|arg| { + arg.accessor( + |e| quote! { return ::ext_php_rs::class::ConstructorResult::Exception(#e); }, + ) + }); + let variadic = self.args.typed.iter().any(|arg| arg.variadic).then(|| { + quote! { + .variadic() + } + }); - 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 - ) - } else { - Ok(quote! { - #prelude - .arg(&mut #name) - }) + Ok(quote! { + ::ext_php_rs::class::ConstructorMeta { + constructor: { + fn inner(ex: &mut ::ext_php_rs::zend::ExecuteData) -> ::ext_php_rs::class::ConstructorResult<#class> { + #(#arg_declarations)* + let parse = ex.parser() + #(.arg(&mut #required_arg_names))* + .not_required() + #(.arg(&mut #not_required_arg_names))* + #variadic + .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))* + #variadic + } + inner + } } }) - .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 - }) +#[derive(Debug)] +pub struct ReceiverArg { + pub _mutable: bool, + pub span: Span, } -fn build_arg_accessors(args: &[Arg]) -> Vec { - args.iter() - .map(|arg| arg.get_accessor("e! { return; })) - .collect() +#[derive(Debug)] +pub struct TypedArg<'a> { + pub name: &'a Ident, + pub ty: Type, + pub nullable: bool, + pub default: Option, + pub as_ref: bool, + pub variadic: bool, } -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 Args<'a> { + pub receiver: Option, + pub typed: Vec>, } -impl Arg { - pub fn new( - name: String, - ty: String, - nullable: bool, - default: Option, - as_ref: bool, - variadic: bool, - ) -> Self { - Self { - name, - ty, - nullable, - default, - as_ref, - variadic, +impl<'a> Args<'a> { + pub fn parse_from_fnargs( + args: impl Iterator, + mut defaults: 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(), + }); + } + FnArg::Typed(PatType { pat, ty, .. }) => { + let ident = match &**pat { + syn::Pat::Ident(syn::PatIdent { ident, .. }) => ident, + _ => bail!(pat => "Unsupported argument."), + }; + + // If the variable is `&[&Zval]` treat it as the variadic argument. + let default = defaults.remove(ident); + let nullable = type_is_nullable(ty.as_ref(), default.is_some())?; + let (variadic, as_ref, ty) = Self::parse_typed(ty); + result.typed.push(TypedArg { + name: ident, + ty, + nullable, + default, + as_ref, + variadic, + }); + } + } } + Ok(result) } - 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()); + fn parse_typed(ty: &Type) -> (bool, bool, Type) { match ty { + Type::Reference(ref_) => { + let as_ref = ref_.mutability.is_some(); + match ref_.elem.as_ref() { + Type::Slice(slice) => ( + // TODO: Allow specifying the variadic type. + slice.elem.to_token_stream().to_string() == "& Zval", + as_ref, + ty.clone(), + ), + _ => (false, as_ref, ty.clone()), + } + } Type::Path(TypePath { path, .. }) => { - let mut path = path.clone(); - let mut pass_by_ref = false; - path.drop_lifetimes(); + let mut as_ref = false; - 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 - } - }); - - // 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) + // For for types that are `Option<&mut T>` to turn them into + // `Option<&T>`, marking the Arg as as "passed by reference". + let ty = path + .segments + .last() .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) + .and_then(|ga| match ga { + GenericArgument::Type(ty) => Some(match ty { + Type::Reference(r) => { + let mut new_ref = r.clone(); + new_ref.mutability = None; + as_ref = true; + Type::Reference(new_ref) } - _ => ga.clone(), - }; - new_ga.to_token_stream().to_string() + _ => ty.clone(), + }), + _ => None, }) } else { None } - }); - - 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, - false, - )) + }) + .unwrap_or_else(|| ty.clone()); + (false, as_ref, ty.clone()) } - Type::Reference(ref_) => { - // If the variable is `&[&Zval]` treat it as the variadic argument. - let is_variadic = match ref_.elem.as_ref() { - Type::Slice(slice) => slice.elem.to_token_stream().to_string() == "& Zval", - _ => false, - }; - // 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(), - is_variadic, - )) - } - _ => None, + _ => (false, false, ty.clone()), } } - #[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()) - } - - /// 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(); +impl TypedArg<'_> { + /// 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(); - 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() }); + // Variadic arguments are passed as slices, so we need to extract the + // inner type. + if self.variadic { + let reference = if let Type::Reference(r) = &ty { + r + } else { + return ty; + }; - quote! { #name_ident.val().unwrap_or(#val.into()) } - } else if self.nullable { - quote! { #name_ident.val() } - } else if self.variadic { - quote! { &#name_ident.variadic_vals() } - } else { - quote! { - match #name_ident.val() { - Some(val) => val, - None => { - ::ext_php_rs::exception::PhpException::default( - concat!("Invalid value given for argument `", #name, "`.").into() - ) - .throw() - .expect(concat!("Failed to throw exception: Invalid value given for argument `", #name, "`.")); - #ret - } - } + if let Type::Slice(inner) = &*reference.elem { + return *inner.elem.clone(); } } + + ty } - /// Returns a [`TokenStream`] containing the line required to instantiate - /// the argument. - pub fn get_arg_definition(&self) -> TokenStream { - let name = &self.name; - let mut ty = self.get_type_ident(); + /// Returns a token stream containing an argument declaration, where the + /// name of the variable holding the arg is the name of the argument. + fn arg_declaration(&self) -> Result { + let name = self.name; + let val = self.arg_builder()?; + Ok(quote! { + let mut #name = #val; + }) + } - let null = self.nullable.then(|| quote! { .allow_null() }); - let passed_by_ref = self.as_ref.then(|| quote! { .as_ref() }); - let is_variadic = self.variadic.then(|| quote! { .is_variadic() }); - if self.variadic { - ty = quote! { ::ext_php_rs::flags::DataType::Mixed } - } + /// 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) } }); - - quote! { - ::ext_php_rs::args::Arg::new(#name, #ty) #null #passed_by_ref #default #is_variadic - } - } -} - -impl Function { - #[inline] - pub fn get_name_ident(&self) -> Ident { - Ident::new(&self.ident, Span::call_site()) + let as_ref = if self.as_ref { + Some(quote! { .as_ref() }) + } else { + None + }; + let variadic = self.variadic.then(|| quote! { .is_variadic() }); + Ok(quote! { + ::ext_php_rs::args::Arg::new(#name, <#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE) + #null + #default + #as_ref + #variadic + }) } - pub fn get_builder(&self) -> TokenStream { - let name = &self.name; - let name_ident = self.get_name_ident(); - let args = self - .args - .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? + /// Get the accessor used to access the value of the argument. + fn accessor(&self, bail_fn: impl Fn(TokenStream) -> TokenStream) -> TokenStream { + let name = self.name; + if let Some(default) = &self.default { quote! { - .returns(<#ty as ::ext_php_rs::convert::IntoZval>::TYPE, false, #nullable) + #name.val().unwrap_or(#default.into()) + } + } else if self.variadic { + quote! { + &#name.variadic_vals() + } + } else if self.nullable { + // 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 { + 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 => { + #bail; + } + } } - }); - - quote! { - ::ext_php_rs::builders::FunctionBuilder::new(#name, #name_ident) - #(#args)* - #output - .build() } } } + +/// 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, has_default: bool) -> Result { + Ok(match ty { + syn::Type::Path(path) => { + has_default + || path + .path + .segments + .iter() + .next_back() + .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/crates/macros/src/impl_.rs b/crates/macros/src/impl_.rs index 821ead014a..5970318f49 100644 --- a/crates/macros/src/impl_.rs +++ b/crates/macros/src/impl_.rs @@ -1,31 +1,23 @@ -use anyhow::{anyhow, bail, Result}; -use darling::{FromMeta, ToTokens}; +use darling::FromMeta; use proc_macro2::TokenStream; use quote::quote; use std::collections::HashMap; -use syn::{Attribute, AttributeArgs, ItemImpl, Lit, Meta, NestedMeta}; +use syn::{AttributeArgs, Ident, ItemImpl, Lit}; +use crate::function::{Args, CallType, Function, MethodReceiver}; use crate::helpers::get_docs; -use crate::{ - class::{Property, PropertyAttr}, - constant::Constant, - method, -}; - -#[derive(Debug, Clone)] -pub enum Visibility { - Public, - Protected, - Private, -} +use crate::prelude::*; #[derive(Debug, Copy, Clone, FromMeta, Default)] pub enum RenameRule { + /// Methods won't be renamed. #[darling(rename = "none")] None, + /// Methods will be converted to camelCase. #[darling(rename = "camelCase")] #[default] Camel, + /// Methods will be converted to snake_case. #[darling(rename = "snake_case")] Snake, } @@ -67,18 +59,26 @@ impl RenameRule { } } +/// Method visibilities. #[derive(Debug)] -pub enum ParsedAttribute { - Default(HashMap), - Optional(String), - Visibility(Visibility), - Rename(String), - Property { - prop_name: Option, - ty: PropAttrTy, - }, +enum MethodVis { + Public, + Private, + Protected, +} + +/// Method types. +#[derive(Debug)] +enum MethodTy { + /// Regular PHP method. + Normal, + /// Constructor method. Constructor, - This, + /// Property getter method. + Getter, + /// Property setter method. + Setter, + /// Abstract method. Abstract, } @@ -88,172 +88,310 @@ pub struct AttrArgs { rename_methods: Option, } -#[derive(Debug)] -pub enum PropAttrTy { - Getter, - Setter, +/// Attribute arguments for `impl` blocks. +#[derive(Debug, Default, FromMeta)] +#[darling(default)] +pub struct ImplArgs { + /// How the methods are renamed. + rename_methods: RenameRule, } -pub fn parser(args: AttributeArgs, input: ItemImpl) -> Result { - let args = AttrArgs::from_list(&args) - .map_err(|e| anyhow!("Unable to parse attribute arguments: {:?}", e))?; +pub fn parser(args: AttributeArgs, mut input: ItemImpl) -> Result { + let args = match ImplArgs::from_list(&args) { + Ok(args) => args, + Err(e) => bail!(input => "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 for struct implementations.") + } + }; - let ItemImpl { self_ty, items, .. } = input; - let class_name = self_ty.to_token_stream().to_string(); + let mut parsed = ParsedImpl::new(path, args.rename_methods); + parsed.parse(input.items.iter_mut())?; - if input.trait_.is_some() { - bail!("This macro cannot be used on trait implementations."); - } + let php_class_impl = parsed.generate_php_class_impl()?; + Ok(quote::quote! { + #input + #php_class_impl + }) +} - let mut state = crate::STATE.lock(); +/// Arguments applied to methods. +#[derive(Debug)] +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, +} - if state.startup_function.is_some() { - bail!( - "Impls must be declared before you declare your startup function and module function." - ); +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, + } + } + + 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") { + // x + 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") { + // x + 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") { + // x + self.vis = MethodVis::Public; + } else if attr.path.is_ident("protected") { + // x + self.vis = MethodVis::Protected; + } else if attr.path.is_ident("private") { + // x + 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") { + // x + self.ty = MethodTy::Getter; + } else if attr.path.is_ident("setter") { + // x + self.ty = MethodTy::Setter; + } else if attr.path.is_ident("constructor") { + // x + self.ty = MethodTy::Constructor; + } else if attr.path.is_ident("abstract_method") { + // x + self.ty = MethodTy::Abstract; + } else { + attrs.push(attr); + } + } + Ok(()) } +} - 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." - ) - })?; +#[derive(Debug)] +struct ParsedImpl<'a> { + path: &'a syn::Path, + rename: RenameRule, + functions: Vec, + constructor: Option>, + constants: Vec>, +} - 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(), - }); +#[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, +} - quote! { - #[allow(dead_code)] - #constant +#[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, + /// Documentation for the constant. + docs: Vec, +} + +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(), + } + } + + /// 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 docs = get_docs(&c.attrs); + + self.constants.push(Constant { + name: name.unwrap_or_else(|| c.ident.to_string()), + ident: &c.ident, + docs, + }); } 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 docs = get_docs(&method.attrs); + 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 mut func = + Function::new(&method.sig, Some(opts.name), args, opts.optional, docs)?; - match ty { - PropAttrTy::Getter => prop.add_getter(ident)?, - PropAttrTy::Setter => prop.add_setter(ident)?, + if matches!(opts.ty, MethodTy::Constructor) { + if self.constructor.replace(func).is_some() { + bail!(method => "Only one constructor can be provided per class."); } - } - if parsed_method.constructor { - if class.constructor.is_some() { - bail!("You cannot have two constructors on the same class."); - } - class.constructor = Some(parsed_method.method); } else { - class.methods.push(parsed_method.method); + 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, + r#abstract: matches!(opts.ty, MethodTy::Abstract), + }); } - parsed_method.tokens } - item => item.to_token_stream(), - }) - }) - .collect::>>()?; + _ => {} + } + } + Ok(()) + } - let output = quote! { - impl #self_ty { - #(#tokens)* + /// 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; + let docs = &c.docs; + quote! { + (#name, &#path::#ident, &[#(#docs),*]) + } + }); - Ok(output) -} + 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),*] + } -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."))?; + fn get_method_props<'a>(self) -> ::std::collections::HashMap<&'static str, ::ext_php_rs::props::Property<'a, #path>> { + todo!() + } - Ok(Some(match name.as_ref() { - "defaults" => { - let defaults = HashMap::from_meta(&meta) - .map_err(|_| anyhow!("Unable to parse `#[defaults]` 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_constructor(self) -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta<#path>> { + #constructor } - } 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_constants(self) -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn, &'static [&'static str])] { + &[#(#constants),*] } - } else { - None } - .ok_or_else(|| anyhow!("Invalid argument given for `#[rename] macro."))?; + }) + } +} - ParsedAttribute::Rename(ident) +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 }); } - "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, - } + quote! { + (#builder, #(#flags)*) } - "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), - })) + .to_tokens(tokens); + } } #[cfg(test)] diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 2e6deaf8ab..f1e6f7f344 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -1,3 +1,4 @@ +//! Macros for the `php-ext` crate. mod class; mod constant; mod extern_; @@ -5,20 +6,11 @@ mod fastcall; mod function; mod helpers; mod impl_; -mod method; mod module; -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::{ parse_macro_input, AttributeArgs, DeriveInput, ItemConst, ItemFn, ItemForeignMod, ItemImpl, ItemStruct, @@ -26,130 +18,987 @@ 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()) - } -} - +/// # `#[php_class]` Attribute +/// +/// Structs can be exported to PHP as classes with the `#[php_class]` attribute +/// macro. This attribute derives the `RegisteredClass` trait on your struct, as +/// well as registering the class to be registered with the `#[php_module]` macro. +/// +/// ## Options +/// +/// The attribute takes some options to modify the output of the class: +/// +/// - `name` - Changes the name of the class when exported to PHP. The Rust struct +/// name is kept the same. If no name is given, the name of the struct is used. +/// Useful for namespacing classes. +/// +/// 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 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 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 +/// the same name as the field. Property types must implement `IntoZval` and +/// `FromZval`. +/// +/// You can rename the property with options: +/// +/// - `rename` - Allows you to rename the property, e.g. +/// `#[prop(rename = "new_name")]` +/// +/// ## Restrictions +/// +/// ### No lifetime parameters +/// +/// Rust lifetimes are used by the Rust compiler to reason about a program's memory safety. +/// They are a compile-time only concept; +/// there is no way to access Rust lifetimes at runtime from a dynamic language like PHP. +/// +/// As soon as Rust data is exposed to PHP, +/// there is no guarantee which the Rust compiler can make on how long the data will live. +/// PHP is a reference-counted language and those references can be held +/// for an arbitrarily long time, which is untraceable by the Rust compiler. +/// The only possible way to express this correctly is to require that any `#[php_class]` +/// does not borrow data for any lifetime shorter than the `'static` lifetime, +/// i.e. the `#[php_class]` cannot have any lifetime parameters. +/// +/// When you need to share ownership of data between PHP and Rust, +/// instead of using borrowed references with lifetimes, consider using +/// reference-counted smart pointers such as [Arc](https://doc.rust-lang.org/std/sync/struct.Arc.html). +/// +/// ### No generic parameters +/// +/// A Rust struct `Foo` with a generic parameter `T` generates new compiled implementations +/// each time it is used with a different concrete type for `T`. +/// These new implementations are generated by the compiler at each usage site. +/// This is incompatible with wrapping `Foo` in PHP, +/// where there needs to be a single compiled implementation of `Foo` which is integrated with the PHP interpreter. +/// +/// ## Example +/// +/// This example creates a PHP class `Human`, adding a PHP property `address`. +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// +/// #[php_class] +/// pub struct Human { +/// name: String, +/// age: i32, +/// #[prop] +/// address: String, +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module.class::() +/// } +/// # fn main() {} +/// ``` +/// +/// Create a custom exception `RedisException`, which extends `Exception`, and put +/// it in the `Redis\Exception` namespace: +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::{ +/// prelude::*, +/// exception::PhpException, +/// zend::ce +/// }; +/// +/// #[php_class(name = "Redis\\Exception\\RedisException")] +/// #[extends(ce::exception)] +/// #[derive(Default)] +/// pub struct RedisException; +/// +/// // Throw our newly created exception +/// #[php_function] +/// pub fn throw_exception() -> PhpResult { +/// Err(PhpException::from_class::("Not good!".into())) +/// } +/// +/// #[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 function returning a `ClassEntry`. +/// The following example implements [`ArrayAccess`](https://www.php.net/manual/en/class.arrayaccess.php): +/// +/// ````rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::{ +/// prelude::*, +/// exception::PhpResult, +/// types::Zval, +/// zend::ce, +/// }; +/// +/// #[php_class] +/// #[implements(ce::arrayaccess)] +/// #[derive(Default)] +/// pub struct EvenNumbersArray; +/// +/// /// Returns `true` if the array offset is an even number. +/// /// Usage: +/// /// ```php +/// /// $arr = new EvenNumbersArray(); +/// /// var_dump($arr[0]); // true +/// /// var_dump($arr[1]); // false +/// /// var_dump($arr[2]); // true +/// /// var_dump($arr[3]); // false +/// /// var_dump($arr[4]); // true +/// /// var_dump($arr[5] = true); // Fatal error: Uncaught Exception: Setting values is not supported +/// /// ``` +/// #[php_impl] +/// impl EvenNumbersArray { +/// pub fn __construct() -> EvenNumbersArray { +/// EvenNumbersArray {} +/// } +/// // We need to use `Zval` because ArrayAccess needs $offset to be a `mixed` +/// pub fn offset_exists(&self, offset: &'_ Zval) -> bool { +/// offset.is_long() +/// } +/// pub fn offset_get(&self, offset: &'_ Zval) -> PhpResult { +/// let integer_offset = offset.long().ok_or("Expected integer offset")?; +/// Ok(integer_offset % 2 == 0) +/// } +/// pub fn offset_set(&mut self, _offset: &'_ Zval, _value: &'_ Zval) -> PhpResult { +/// Err("Setting values is not supported".into()) +/// } +/// pub fn offset_unset(&mut self, _offset: &'_ Zval) -> PhpResult { +/// Err("Setting values is not supported".into()) +/// } +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module.class::() +/// } +/// # fn main() {} +/// ```` #[proc_macro_attribute] pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream { let args = parse_macro_input!(args as AttributeArgs); let input = parse_macro_input!(input as ItemStruct); - match class::parser(args, input) { - Ok(parsed) => parsed, - Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), - } - .into() + class::parser(args, input) + .unwrap_or_else(|e| e.to_compile_error()) + .into() } +/// # `#[php_function]` Attribute +/// +/// Used to annotate functions which should be exported to PHP. Note that this +/// should not be used on class methods - see the `#[php_impl]` macro for that. +/// +/// See the [list of types](../types/index.md) that are valid as parameter and +/// return types. +/// +/// ## Optional parameters +/// +/// Optional parameters can be used by setting the Rust parameter type to a variant +/// of `Option`. The macro will then figure out which parameters are optional by +/// using the last consecutive arguments that are a variant of `Option` or have a +/// default value. +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// +/// #[php_function] +/// pub fn greet(name: String, age: Option) -> String { +/// let mut greeting = format!("Hello, {}!", name); +/// +/// if let Some(age) = age { +/// greeting += &format!(" You are {} years old.", age); +/// } +/// +/// greeting +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module.function(wrap_function!(greet)) +/// } +/// # fn main() {} +/// ``` +/// +/// Default parameter values can also be set for optional parameters. This is done +/// through the `defaults` attribute option. When an optional parameter has a +/// default, it does not need to be a variant of `Option`: +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// +/// #[php_function(defaults(offset = 0))] +/// pub fn rusty_strpos(haystack: &str, needle: &str, offset: i64) -> Option { +/// let haystack: String = haystack.chars().skip(offset as usize).collect(); +/// haystack.find(needle) +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module.function(wrap_function!(rusty_strpos)) +/// } +/// # fn main() {} +/// ``` +/// +/// Note that if there is a non-optional argument after an argument that is a +/// variant of `Option`, the `Option` argument will be deemed a nullable +/// argument rather than an optional argument. +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// +/// /// `age` will be deemed required and nullable rather than optional. +/// #[php_function] +/// pub fn greet(name: String, age: Option, description: String) -> String { +/// let mut greeting = format!("Hello, {}!", name); +/// +/// if let Some(age) = age { +/// greeting += &format!(" You are {} years old.", age); +/// } +/// +/// greeting += &format!(" {}.", description); +/// greeting +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module.function(wrap_function!(greet)) +/// } +/// # fn main() {} +/// ``` +/// +/// You can also specify the optional arguments if you want to have nullable +/// arguments before optional arguments. This is done through an attribute +/// parameter: +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// +/// /// `age` will be deemed required and nullable rather than optional, +/// /// while description will be optional. +/// #[php_function(optional = "description")] +/// pub fn greet(name: String, age: Option, description: Option) -> String { +/// let mut greeting = format!("Hello, {}!", name); +/// +/// if let Some(age) = age { +/// greeting += &format!(" You are {} years old.", age); +/// } +/// +/// if let Some(description) = description { +/// greeting += &format!(" {}.", description); +/// } +/// +/// greeting +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module.function(wrap_function!(greet)) +/// } +/// # fn main() {} +/// ``` +/// +/// ## Variadic Functions +/// +/// Variadic functions can be implemented by specifying the last argument in the Rust +/// function to the type `&[&Zval]`. This is the equivalent of a PHP function using +/// the `...$args` syntax. +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::{prelude::*, types::Zval}; +/// +/// /// This can be called from PHP as `add(1, 2, 3, 4, 5)` +/// #[php_function] +/// pub fn add(number: u32, numbers:&[&Zval]) -> u32 { +/// // numbers is a slice of 4 Zvals all of type long +/// number +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module.function(wrap_function!(add)) +/// } +/// # fn main() {} +/// ``` +/// +/// ## Returning `Result` +/// +/// You can also return a `Result` from the function. The error variant will be +/// translated into an exception and thrown. See the section on +/// [exceptions](../exceptions.md) for more details. #[proc_macro_attribute] pub fn php_function(args: TokenStream, input: TokenStream) -> TokenStream { let args = parse_macro_input!(args as AttributeArgs); 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(), - } - .into() + function::parser(args, input) + .unwrap_or_else(|e| e.to_compile_error()) + .into() } +/// # `#[php_const]` Attribute +/// +/// Exports a Rust constant as a global PHP constant. The constant can be any type +/// that implements `IntoConst`. +/// +/// The `wrap_constant!()` macro can be used to simplify the registration of constants. +/// It sets the name and doc comments for the constant. +/// +/// ## Examples +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// +/// #[php_const] +/// const TEST_CONSTANT: i32 = 100; +/// +/// #[php_const] +/// const ANOTHER_STRING_CONST: &'static str = "Hello world!"; +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module +/// .constant(wrap_constant!(TEST_CONSTANT)) +/// .constant(("MANUAL_CONSTANT", ANOTHER_STRING_CONST, &[])) +/// } +/// # fn main() {} +/// ``` +/// +/// ## PHP usage +/// +/// ```php +/// TokenStream { - let input = parse_macro_input!(input as ItemFn); +pub fn php_const(_args: TokenStream, input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemConst); - match module::parser(input) { - Ok(parsed) => parsed, - Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), - } - .into() + constant::parser(input).into() } +/// # `#[php_module]` Attribute +/// +/// The module macro is used to annotate the `get_module` function, which is used by +/// the PHP interpreter to retrieve information about your extension, including the +/// 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. +/// +/// 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. +/// +/// Classes and constants are not registered with PHP in the `get_module` function. These are +/// registered inside the extension startup function. +/// +/// ## Usage +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::{ +/// prelude::*, +/// zend::ModuleEntry, +/// info_table_start, +/// info_table_row, +/// info_table_end +/// }; +/// +/// #[php_const] +/// 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) { +/// info_table_start!(); +/// info_table_row!("my extension", "enabled"); +/// info_table_end!(); +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module +/// .constant(wrap_constant!(MY_CUSTOM_CONST)) +/// .class::() +/// .function(wrap_function!(hello_world)) +/// .info_function(php_module_info) +/// } +/// # fn main() {} +/// ``` #[proc_macro_attribute] -pub fn php_startup(args: 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(Some(args), input) { - Ok(parsed) => parsed, - Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), - } - .into() + module::parser(args, input) + .unwrap_or_else(|e| e.to_compile_error()) + .into() } +/// # `#[php_impl]` Attribute +/// +/// 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. 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. +/// +/// If you want to use async Rust, use `#[php_async_impl]`, instead: see [here »](./async_impl.md) for more info. +/// +/// ## Methods +/// +/// Methods basically follow the same rules as functions, so read about the +/// [`php_function`] macro first. The primary difference between functions and +/// methods is they are bounded by their class object. +/// +/// 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 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 +/// of the following as the `rename_methods` option: +/// +/// - `"none"` - does not rename the methods. +/// - `"camelCase"` - renames all methods to camel case (default). +/// - `"snake_case"` - renames all methods to snake case. +/// +/// For example, to disable renaming, change the `#[php_impl]` attribute to +/// `#[php_impl(rename_methods = "none")]`. +/// +/// The rest of the options are passed as separate attributes: +/// +/// - `#[defaults(i = 5, b = "hello")]` - Sets the default value for parameter(s). +/// - `#[optional(i)]` - Sets the first optional parameter. Note that this also sets +/// the remaining parameters as optional, so all optional parameters must be a +/// variant of `Option`. +/// - `#[public]`, `#[protected]` and `#[private]` - Sets the visibility of the +/// method. +/// - `#[rename("method_name")]` - Renames the PHP method to a different identifier, +/// without renaming the Rust method name. +/// +/// The `#[defaults]` and `#[optional]` attributes operate the same as the +/// equivalent function attribute parameters. +/// +/// ### Constructors +/// +/// By default, if a class does not have a constructor, it is not constructable from +/// PHP. It can only be returned from a Rust function to PHP. +/// +/// Constructors are Rust methods which can take any amount of parameters and +/// returns either `Self` or `Result`, where `E: Into`. When +/// the error variant of `Result` is encountered, it is thrown as an exception and +/// the class is not constructed. +/// +/// Constructors are designated by either naming the method `__construct` or by +/// annotating a method with the `#[constructor]` attribute. Note that when using +/// the attribute, the function is not exported to PHP like a regular method. +/// +/// Constructors cannot use the visibility or rename attributes listed above. +/// +/// ## Constants +/// +/// Constants are defined as regular Rust `impl` constants. Any type that implements +/// `IntoZval` can be used as a constant. Constant visibility is not supported at +/// the moment, and therefore no attributes are valid on constants. +/// +/// ## Property getters and setters +/// +/// You can add properties to classes which use Rust functions as getters and/or +/// setters. This is done with the `#[getter]` and `#[setter]` attributes. By +/// default, the `get_` or `set_` prefix is trimmed from the start of the function +/// name, and the remainder is used as the property name. +/// +/// If you want to use a different name for the property, you can pass a `rename` +/// option to the attribute which will change the property name. +/// +/// Properties do not necessarily have to have both a getter and a setter, if the +/// property is immutable the setter can be omitted, and vice versa for getters. +/// +/// The `#[getter]` and `#[setter]` attributes are mutually exclusive on methods. +/// Properties cannot have multiple getters or setters, and the property name cannot +/// conflict with field properties defined on the struct. +/// +/// As the same as field properties, method property types must implement both +/// `IntoZval` and `FromZval`. +/// +/// ## Example +/// +/// Continuing on from our `Human` example in the structs section, we will define a +/// constructor, as well as getters for the properties. We will also define a +/// constant for the maximum age of a `Human`. +/// +/// ```rust,no_run,ignore +/// # #![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, +/// } +/// +/// #[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() +/// } +/// } +/// +/// #[getter] +/// pub fn get_name(&self) -> String { +/// self.name.to_string() +/// } +/// +/// #[setter] +/// pub fn set_name(&mut self, name: String) { +/// self.name = name; +/// } +/// +/// #[getter] +/// pub fn get_age(&self) -> i32 { +/// self.age +/// } +/// +/// pub fn introduce(&self) { +/// println!("My name is {} and I am {} years old. I live at {}.", self.name, self.age, self.address); +/// } +/// +/// 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.class::() +/// } +/// # fn main() {} +/// ``` +/// +/// Using our newly created class in PHP: +/// +/// ```php +/// introduce(); // My name is David and I am 20 years old. +/// var_dump(Human::get_max_age()); // int(100) +/// var_dump(Human::MAX_AGE); // int(100) +/// ``` +/// +/// [`php_async_impl`]: ./async_impl.md #[proc_macro_attribute] pub fn php_impl(args: TokenStream, input: TokenStream) -> TokenStream { let args = parse_macro_input!(args as AttributeArgs); let input = parse_macro_input!(input as ItemImpl); - match impl_::parser(args, input) { - Ok(parsed) => parsed, - Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), - } - .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() + impl_::parser(args, input) + .unwrap_or_else(|e| e.to_compile_error()) + .into() } +/// # `#[php_extern]` Attribute +/// +/// Attribute used to annotate `extern` blocks which are deemed as PHP +/// functions. +/// +/// This allows you to 'import' PHP functions into Rust so that they can be +/// called like regular Rust functions. Parameters can be any type that +/// implements [`IntoZval`], and the return type can be anything that implements +/// [`From`] (notice how [`Zval`] is consumed rather than borrowed in this +/// case). +/// +/// Unlike most other attributes, this does not need to be placed inside a +/// `#[php_module]` block. +/// +/// # Panics +/// +/// The function can panic when called under a few circumstances: +/// +/// * The function could not be found or was not callable. +/// * One of the parameters could not be converted into a [`Zval`]. +/// * The actual function call failed internally. +/// * The output [`Zval`] could not be parsed into the output type. +/// +/// The last point can be important when interacting with functions that return +/// unions, such as [`strpos`] which can return an integer or a boolean. In this +/// case, a [`Zval`] should be returned as parsing a boolean to an integer is +/// invalid, and vice versa. +/// +/// # Example +/// +/// This `extern` block imports the [`strpos`] function from PHP. Notice that +/// the string parameters can take either [`String`] or [`&str`], the optional +/// parameter `offset` is an [`Option`], and the return value is a [`Zval`] +/// as the return type is an integer-boolean union. +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::{ +/// prelude::*, +/// types::Zval, +/// }; +/// +/// #[php_extern] +/// extern "C" { +/// fn strpos(haystack: &str, needle: &str, offset: Option) -> Zval; +/// } +/// +/// #[php_function] +/// pub fn my_strpos() { +/// assert_eq!(unsafe { strpos("Hello", "e", None) }.long(), Some(1)); +/// } +/// +/// #[php_module] +/// pub fn module(module: ModuleBuilder) -> ModuleBuilder { +/// module.function(wrap_function!(my_strpos)) +/// } +/// # fn main() {} +/// ``` +/// +/// [`strpos`]: https://www.php.net/manual/en/function.strpos.php +/// [`IntoZval`]: crate::convert::IntoZval +/// [`Zval`]: crate::types::Zval #[proc_macro_attribute] pub fn php_extern(_: TokenStream, input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemForeignMod); - match extern_::parser(input) { - Ok(parsed) => parsed, - Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), - } - .into() + extern_::parser(input) + .unwrap_or_else(|e| e.to_compile_error()) + .into() } +/// # `ZvalConvert` Derive Macro +/// +/// The `#[derive(ZvalConvert)]` macro derives the `FromZval` and `IntoZval` traits +/// on a struct or enum. +/// +/// ## Structs +/// +/// When used on a struct, the `FromZendObject` and `IntoZendObject` traits are also +/// implemented, mapping fields to properties in both directions. All fields on the +/// struct must implement `FromZval` as well. Generics are allowed on structs that +/// use the derive macro, however, the implementation will add a `FromZval` bound to +/// all generics types. +/// +/// ### Examples +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// +/// #[derive(ZvalConvert)] +/// pub struct ExampleClass<'a> { +/// a: i32, +/// b: String, +/// c: &'a str +/// } +/// +/// #[php_function] +/// pub fn take_object(obj: ExampleClass) { +/// dbg!(obj.a, obj.b, obj.c); +/// } +/// +/// #[php_function] +/// pub fn give_object() -> ExampleClass<'static> { +/// ExampleClass { +/// a: 5, +/// b: "String".to_string(), +/// c: "Borrowed", +/// } +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module +/// .function(wrap_function!(take_object)) +/// .function(wrap_function!(give_object)) +/// } +/// # fn main() {} +/// ``` +/// +/// Calling from PHP: +/// +/// ```php +/// a = 5; +/// $obj->b = 'Hello, world!'; +/// $obj->c = 'another string'; +/// +/// take_object($obj); +/// var_dump(give_object()); +/// ``` +/// +/// Another example involving generics: +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// +/// // T must implement both `PartialEq` and `FromZval`. +/// #[derive(Debug, ZvalConvert)] +/// pub struct CompareVals> { +/// a: T, +/// b: T +/// } +/// +/// #[php_function] +/// pub fn take_object(obj: CompareVals) { +/// dbg!(obj); +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module +/// .function(wrap_function!(take_object)) +/// } +/// # fn main() {} +/// ``` +/// +/// ## Enums +/// +/// When used on an enum, the `FromZval` implementation will treat the enum as a +/// tagged union with a mixed datatype. This allows you to accept multiple types in +/// a parameter, for example, a string and an integer. +/// +/// The enum variants must not have named fields, and each variant must have exactly +/// one field (the type to extract from the zval). Optionally, the enum may have one +/// default variant with no data contained, which will be used when the rest of the +/// variants could not be extracted from the zval. +/// +/// The ordering of the variants in the enum is important, as the `FromZval` +/// implementation will attempt to parse the zval data in order. For example, if you +/// put a `String` variant before an integer variant, the integer would be converted +/// to a string and passed as the string variant. +/// +/// ### Examples +/// +/// Basic example showing the importance of variant ordering and default field: +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// +/// #[derive(Debug, ZvalConvert)] +/// pub enum UnionExample<'a> { +/// Long(u64), // Long +/// ProperStr(&'a str), // Actual string - not a converted value +/// ParsedStr(String), // Potentially parsed string, i.e. a double +/// None // Zval did not contain anything that could be parsed above +/// } +/// +/// #[php_function] +/// pub fn test_union(val: UnionExample) { +/// dbg!(val); +/// } +/// +/// #[php_function] +/// pub fn give_union() -> UnionExample<'static> { +/// UnionExample::Long(5) +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module +/// .function(wrap_function!(test_union)) +/// .function(wrap_function!(give_union)) +/// } +/// # fn main() {} +/// ``` +/// +/// Use in PHP: +/// +/// ```php +/// test_union(5); // UnionExample::Long(5) +/// test_union("Hello, world!"); // UnionExample::ProperStr("Hello, world!") +/// test_union(5.66666); // UnionExample::ParsedStr("5.6666") +/// test_union(null); // UnionExample::None +/// var_dump(give_union()); // int(5) +/// ``` #[proc_macro_derive(ZvalConvert)] pub fn zval_convert_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - match zval::parser(input) { + zval::parser(input) + .unwrap_or_else(|e| e.to_compile_error()) + .into() +} + +/// Defines an `extern` function with the Zend fastcall convention based on +/// operating system. +/// +/// On Windows, Zend fastcall functions use the vector calling convention, while +/// on all other operating systems no fastcall convention is used (just the +/// regular C calling convention). +/// +/// This macro wraps a function and applies the correct calling convention. +/// +/// ## Examples +/// +/// ```rust,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// use ext_php_rs::zend_fastcall; +/// +/// zend_fastcall! { +/// pub extern fn test_hello_world(a: i32, b: i32) -> i32 { +/// a + b +/// } +/// } +/// ``` +/// +/// On Windows, this function will have the signature `pub extern "vectorcall" +/// fn(i32, i32) -> i32`, while on macOS/Linux the function will have the +/// signature `pub extern "C" fn(i32, i32) -> i32`. +/// +/// ## Support +/// +/// The `vectorcall` ABI is currently only supported on Windows with nightly +/// Rust and the `abi_vectorcall` feature enabled. +#[proc_macro] +pub fn zend_fastcall(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemFn); + + fastcall::parser(input).into() +} + +/// Wraps a function to be used in the [`Module::function`] method. +#[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) => syn::Error::new(Span::call_site(), e).to_compile_error(), + Err(e) => e.to_compile_error(), } .into() } +/// Wraps a constant to be used in the [`ModuleBuilder::constant`] method. #[proc_macro] -pub fn zend_fastcall(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemFn); +pub fn wrap_constant(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as syn::Path); - match fastcall::parser(input) { + match constant::wrap(input) { Ok(parsed) => parsed, - Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), + 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 deleted file mode 100644 index b5d7fad21e..0000000000 --- a/crates/macros/src/method.rs +++ /dev/null @@ -1,455 +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, 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, - 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)) - } - 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() - } -} diff --git a/crates/macros/src/module.rs b/crates/macros/src/module.rs index f350fd472b..231e5b3662 100644 --- a/crates/macros/src/module.rs +++ b/crates/macros/src/module.rs @@ -1,381 +1,86 @@ -use std::sync::MutexGuard; - -use anyhow::{anyhow, bail, Result}; -use proc_macro2::{Ident, Span, TokenStream}; +use darling::FromMeta; +use proc_macro2::{Ident, TokenStream}; use quote::quote; -use syn::{ItemFn, Signature, Type}; +use syn::{AttributeArgs, ItemFn, Signature}; + +use crate::prelude::*; -use crate::{ - class::{Class, Property}, - function::{Arg, Function}, - startup_function, State, STATE, -}; +#[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(input: ItemFn) -> Result { +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), + }; 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 + let startup = match opts.startup { + Some(startup) => quote! { #startup(ty, mod_num) }, + None => quote! { 0i32 }, }; - 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) - } - }); - 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 - + Ok(quote! { #[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 { + 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 { + let a = #startup; + let b = __EXT_PHP_RS_MODULE_STARTUP + .lock() + .take() + .inspect(|_| ::ext_php_rs::internal::ext_php_rs_startup()) + .expect("Module startup function has already been called.") + .startup(ty, mod_num) + .map(|_| 0) + .unwrap_or(1); + a | b + } + + #[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); - - match builder.build() { - Ok(module) => module.into_raw(), + )) + .startup_function(ext_php_rs_startup); + + match builder.try_into() { + 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 mut ty = - quote! { abi::Option::Some(<#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE) }; - if self.variadic { - ty = quote! { abi::Option::Some(::ext_php_rs::flags::DataType::Array) } - } - let default = if let Some(default) = &self.default { - quote! { Some(#default.into()) } - } else { - quote! { None } - }; - - quote! { - Parameter { - name: #name.into(), - ty: #ty, - 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() + #[inline] + fn internal(#inputs) #output { + #(#stmts)* } - }); - 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); + let builder = internal(::ext_php_rs::builders::ModuleBuilder::new( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )); - quote! { - Module { - name: env!("CARGO_PKG_NAME").into(), - functions: vec![#(#functs,)*].into(), - classes: vec![#(#classes,)*].into(), - constants: vec![#(#constants,)*].into(), - } + Description::new(builder.into()) } - } + }) } diff --git a/crates/macros/src/startup_function.rs b/crates/macros/src/startup_function.rs deleted file mode 100644 index 695753d6d9..0000000000 --- a/crates/macros/src/startup_function.rs +++ /dev/null @@ -1,198 +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}; - -use crate::{class::Class, constant::Constant, STATE}; - -#[derive(Default, Debug, FromMeta)] -#[darling(default)] -struct StartupArgs { - before: bool, -} - -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) -} - -/// 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::>>() -} - -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/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..e5c2b15aba 100644 --- a/crates/macros/src/zval.rs +++ b/crates/macros/src/zval.rs @@ -1,4 +1,3 @@ -use anyhow::{bail, Context, Result}; use darling::ToTokens; use proc_macro2::{Span, TokenStream}; use quote::quote; @@ -7,6 +6,8 @@ use syn::{ Lifetime, LifetimeDef, TypeGenerics, Variant, WhereClause, }; +use crate::prelude::*; + pub fn parser(input: DeriveInput) -> Result { let DeriveInput { generics, ident, .. @@ -83,7 +84,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.span() => "Only structs and enums are supported by the `#[derive(ZvalConvert)]` macro.") + } } } @@ -100,8 +103,8 @@ 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 ident = field.ident.as_ref().ok_or_else(|| { + err!(field => "Fields require names when using the `#[derive(ZvalConvert)]` macro on a struct.") })?; let field_name = ident.to_string(); @@ -115,8 +118,8 @@ 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 ident = field.ident.as_ref().ok_or_else(|| { + err!(field => "Fields require names when using the `#[derive(ZvalConvert)]` macro on a struct.") })?; let field_name = ident.to_string(); @@ -143,6 +146,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 +207,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!(fields => "Enum variant must only have one field when using `#[derive(ZvalConvert)]`."); } let ty = &fields.unnamed.first().unwrap().ty; @@ -216,7 +220,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!(fields => "Only one enum unit type is valid as a default when using `#[derive(ZvalConvert)]`."); } default.replace(quote! { @@ -224,7 +228,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!(fields => "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 }); @@ -232,6 +236,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 new file mode 100644 index 0000000000..411c4865ba --- /dev/null +++ b/examples/hello_world.rs @@ -0,0 +1,88 @@ +#![cfg_attr(windows, feature(abi_vectorcall))] +use ext_php_rs::{constant::IntoConst, prelude::*, types::ZendClassObject}; + +#[derive(Debug)] +#[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 + } + + pub fn builder_pattern( + self_: &mut ZendClassObject, + ) -> &mut ZendClassObject { + dbg!(self_) + } +} + +#[php_function] +pub fn new_class() -> TestClass { + TestClass { a: 1, b: 2 } +} + +#[php_function] +pub fn hello_world() -> &'static str { + "Hello, world!" +} + +#[php_const] +pub const HELLO_WORLD: i32 = 100; + +#[php_extern] +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(z: TestZvalConvert) -> i32 { + dbg!(z); + 5 +} + +fn startup(_ty: i32, mod_num: i32) -> i32 { + 5.register_constant("SOME_CONST", mod_num).unwrap(); + 0 +} + +#[php_module(startup = "startup")] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module + .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, &[])) +} diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index db614111d3..3922ef00b2 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -27,12 +27,16 @@ - [Async futures](./macros/async_impl.md) - [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) - [async `impl`s](./macros/async_impl.md) - [Constants](./macros/constant.md) + - [PHP Functions](./macros/extern.md) - [`ZvalConvert`](./macros/zval_convert.md) - [Exceptions](./exceptions.md) - [INI Settings](./ini-settings.md) + +# Migration Guides +--- +[v0.14](./migration-guides/v0.14.md) diff --git a/guide/src/getting-started/hello_world.md b/guide/src/getting-started/hello_world.md index 05918bdd4c..7144f8cdcc 100644 --- a/guide/src/getting-started/hello_world.md +++ b/guide/src/getting-started/hello_world.md @@ -57,16 +57,16 @@ 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 +```rust,no_run #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; use ext_php_rs::prelude::*; #[php_function] @@ -76,8 +76,9 @@ pub fn hello_world(name: &str) -> String { #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { - module + module.function(wrap_function!(hello_world)) } +# fn main() {} ``` ## Building the extension diff --git a/guide/src/ini-settings.md b/guide/src/ini-settings.md index 22820c5e2e..f56d72c947 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,14 @@ 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() {} ``` @@ -35,14 +41,22 @@ The INI values are stored as part of the `GlobalExecutor`, and can be accessed v ```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; -# use ext_php_rs::prelude::*; -# use ext_php_rs::zend::ExecutorGlobals; +use ext_php_rs::{ + prelude::*, + zend::ExecutorGlobals, +}; -#[php_startup] -pub fn startup_function(ty: i32, module_number: i32) { +pub fn startup(ty: i32, mod_num: 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() {} ``` diff --git a/guide/src/introduction.md b/guide/src/introduction.md index 17be2cf8c3..580d6863aa 100644 --- a/guide/src/introduction.md +++ b/guide/src/introduction.md @@ -32,6 +32,9 @@ Our main goal is to **make extension development easier.** guaranteed while we are at major version `0`, which is for the foreseeable future. It's recommended to lock the version at the patch level. +When introducing breaking changes a migration guide will be provided in this +guide. + ## Documentation - This guide! diff --git a/guide/src/macros/async_impl.md b/guide/src/macros/async_impl.md index 979c58bdee..f4fe50518f 100644 --- a/guide/src/macros/async_impl.md +++ b/guide/src/macros/async_impl.md @@ -1,10 +1,10 @@ -# `#[php_async_impl]` +# `#[php_async_impl]` Attribute -Using `#[php_async_impl]` instead of `#[php_impl]` allows us to expose any async Rust library to PHP, using [PHP fibers](https://www.php.net/manual/en/language.fibers.php), [php-tokio](https://github.com/danog/php-tokio) and the [PHP Revolt event loop](https://revolt.run) under the hood to handle async interoperability. +Using `#[php_async_impl]` instead of `#[php_impl]` allows us to expose any async Rust library to PHP, using [PHP fibers](https://www.php.net/manual/en/language.fibers.php), [php-tokio](https://github.com/danog/php-tokio) and the [PHP Revolt event loop](https://revolt.run) under the hood to handle async interoperability. This allows full compatibility with [amphp](https://amphp.org), [PSL](https://github.com/azjezz/psl), [reactphp](https://reactphp.org) and any other async PHP library based on [Revolt](https://revolt.run). -Traits annotated with `#[php_async_impl]` can freely expose any async function, using `await` and any async Rust library. +Traits annotated with `#[php_async_impl]` can freely expose any async function, using `await` and any async Rust library. Make sure to also expose the `php_tokio::EventLoop::init` and `php_tokio::EventLoop::wakeup` functions to PHP in order to initialize the event loop, as specified in the full example [here »](#async-example). @@ -12,9 +12,9 @@ Also, make sure to invoke `EventLoop::shutdown` in the request shutdown handler ## Async example -In this example, we're exposing an async Rust HTTP client library called [reqwest](https://docs.rs/reqwest/latest/reqwest/) to PHP, using [PHP fibers](https://www.php.net/manual/en/language.fibers.php), [php-tokio](https://github.com/danog/php-tokio) and the [PHP Revolt event loop](https://revolt.run) under the hood to handle async interoperability. +In this example, we're exposing an async Rust HTTP client library called [reqwest](https://docs.rs/reqwest/latest/reqwest/) to PHP, using [PHP fibers](https://www.php.net/manual/en/language.fibers.php), [php-tokio](https://github.com/danog/php-tokio) and the [PHP Revolt event loop](https://revolt.run) under the hood to handle async interoperability. -This allows full compatibility with [amphp](https://amphp.org), [PSL](https://github.com/azjezz/psl), [reactphp](https://reactphp.org) and any other async PHP library based on [Revolt](https://revolt.run). +This allows full compatibility with [amphp](https://amphp.org), [PSL](https://github.com/azjezz/psl), [reactphp](https://reactphp.org) and any other async PHP library based on [Revolt](https://revolt.run). Make sure to require [php-tokio](https://github.com/danog/php-tokio) as a dependency before proceeding. @@ -49,11 +49,11 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { } ``` -Here's the async PHP code we use to interact with the Rust class we just exposed. +Here's the async PHP code we use to interact with the Rust class we just exposed. The `Client::init` method needs to be called only once in order to initialize the Revolt event loop and link it to the Tokio event loop, as shown by the following code. -See [here »](https://amphp.org) for more info on async PHP using [amphp](https://amphp.org) + [revolt](https://revolt.run). +See [here »](https://amphp.org) for more info on async PHP using [amphp](https://amphp.org) + [revolt](https://revolt.run). ```php &'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 valid 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 an function returning a `ClassEntry`. The following example implements [`ArrayAccess`](https://www.php.net/manual/en/class.arrayaccess.php): -```rust,no_run + +````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..a7198b853c 100644 --- a/guide/src/macros/constant.md +++ b/guide/src/macros/constant.md @@ -1,21 +1,30 @@ -# `#[php_const]` +# `#[php_const]` Attribute Exports a Rust constant as a global PHP constant. The constant can be any type that implements `IntoConst`. +The `wrap_constant!()` macro can be used to simplify the registration of constants. +It sets the name and doc comments for the constant. + ## Examples ```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_const] const TEST_CONSTANT: i32 = 100; #[php_const] 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(("MANUAL_CONSTANT", ANOTHER_STRING_CONST, &[])) +} # fn main() {} ``` @@ -25,5 +34,5 @@ const ANOTHER_STRING_CONST: &'static str = "Hello world!"; `] (notice how [`Zval`] is consumed rather than borrowed in this +case). + +Unlike most other attributes, this does not need to be placed inside a +`#[php_module]` block. + +# Panics + +The function can panic when called under a few circumstances: + +* The function could not be found or was not callable. +* One of the parameters could not be converted into a [`Zval`]. +* The actual function call failed internally. +* The output [`Zval`] could not be parsed into the output type. + +The last point can be important when interacting with functions that return +unions, such as [`strpos`] which can return an integer or a boolean. In this +case, a [`Zval`] should be returned as parsing a boolean to an integer is +invalid, and vice versa. + +# Example + +This `extern` block imports the [`strpos`] function from PHP. Notice that +the string parameters can take either [`String`] or [`&str`], the optional +parameter `offset` is an [`Option`], and the return value is a [`Zval`] +as the return type is an integer-boolean union. + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::{ + prelude::*, + types::Zval, +}; + +#[php_extern] +extern "C" { + fn strpos(haystack: &str, needle: &str, offset: Option) -> Zval; +} + +#[php_function] +pub fn my_strpos() { + assert_eq!(unsafe { strpos("Hello", "e", None) }.long(), Some(1)); +} + +#[php_module] +pub fn module(module: ModuleBuilder) -> ModuleBuilder { + module.function(wrap_function!(my_strpos)) +} +# fn main() {} +``` + +[`strpos`]: https://www.php.net/manual/en/function.strpos.php +[`IntoZval`]: crate::convert::IntoZval +[`Zval`]: crate::types::Zval diff --git a/guide/src/macros/function.md b/guide/src/macros/function.md index d961f5f27b..86c7f8da0f 100644 --- a/guide/src/macros/function.md +++ b/guide/src/macros/function.md @@ -1,4 +1,4 @@ -# `#[php_function]` +# `#[php_function]` Attribute Used to annotate functions which should be exported to PHP. Note that this should not be used on class methods - see the `#[php_impl]` macro for that. @@ -16,7 +16,8 @@ default value. ```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_function] pub fn greet(name: String, age: Option) -> String { let mut greeting = format!("Hello, {}!", name); @@ -27,6 +28,11 @@ pub fn greet(name: String, age: Option) -> String { greeting } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.function(wrap_function!(greet)) +} # fn main() {} ``` @@ -37,12 +43,18 @@ default, it does not need to be a variant of `Option`: ```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_function(defaults(offset = 0))] pub fn rusty_strpos(haystack: &str, needle: &str, offset: i64) -> Option { let haystack: String = haystack.chars().skip(offset as usize).collect(); haystack.find(needle) } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.function(wrap_function!(rusty_strpos)) +} # fn main() {} ``` @@ -53,7 +65,8 @@ argument rather than an optional argument. ```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; -# use ext_php_rs::prelude::*; +use ext_php_rs::prelude::*; + /// `age` will be deemed required and nullable rather than optional. #[php_function] pub fn greet(name: String, age: Option, description: String) -> String { @@ -66,6 +79,11 @@ pub fn greet(name: String, age: Option, description: String) -> String { greeting += &format!(" {}.", description); greeting } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.function(wrap_function!(greet)) +} # fn main() {} ``` @@ -76,7 +94,8 @@ parameter: ```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; -# use ext_php_rs::prelude::*; +use ext_php_rs::prelude::*; + /// `age` will be deemed required and nullable rather than optional, /// while description will be optional. #[php_function(optional = "description")] @@ -93,6 +112,11 @@ pub fn greet(name: String, age: Option, description: Option) -> Str greeting } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.function(wrap_function!(greet)) +} # fn main() {} ``` @@ -105,14 +129,19 @@ the `...$args` syntax. ```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; -# use ext_php_rs::prelude::*; -# use ext_php_rs::types::Zval; +use ext_php_rs::{prelude::*, types::Zval}; + /// This can be called from PHP as `add(1, 2, 3, 4, 5)` #[php_function] pub fn add(number: u32, numbers:&[&Zval]) -> u32 { // numbers is a slice of 4 Zvals all of type long number } + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.function(wrap_function!(add)) +} # fn main() {} ``` diff --git a/guide/src/macros/impl.md b/guide/src/macros/impl.md index db12ecaf3d..c76c4205a1 100644 --- a/guide/src/macros/impl.md +++ b/guide/src/macros/impl.md @@ -1,9 +1,10 @@ -# `#[php_impl]` +# `#[php_impl]` Attribute 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 @@ -100,22 +100,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] @@ -137,18 +143,18 @@ impl Human { 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/index.md b/guide/src/macros/index.md index b97285ba4d..d06f8cea14 100644 --- a/guide/src/macros/index.md +++ b/guide/src/macros/index.md @@ -12,26 +12,12 @@ used from PHP without fiddling around with zvals. - [`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_extern`] - Attribute used to annotate `extern` blocks which are deemed as + PHP functions. [`php_module`]: ./module.md -[`php_startup`]: ./module_startup.md [`php_function`]: ./function.md [`php_class`]: ./classes.md [`php_impl`]: ./impl.md [`php_const`]: ./constant.md -[see here]: https://github.com/rust-lang/reference/issues/578 +[`php_extern`]: ./extern.md diff --git a/guide/src/macros/module.md b/guide/src/macros/module.md index 273fd43872..b0a26cb446 100644 --- a/guide/src/macros/module.md +++ b/guide/src/macros/module.md @@ -1,4 +1,4 @@ -# `#[php_module]` +# `#[php_module]` Attribute The module macro is used to annotate the `get_module` function, which is used by the PHP interpreter to retrieve information about your extension, including the @@ -6,38 +6,46 @@ 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 +Classes and constants are not registered with PHP in the `get_module` function. These are registered inside the extension startup function. ## Usage -```rust,ignore +```rust,no_run # #![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::*, + zend::ModuleEntry, + info_table_start, + info_table_row, + info_table_end +}; + +#[php_const] +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 +56,11 @@ 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) } +# 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..7586138d93 100644 --- a/guide/src/macros/zval_convert.md +++ b/guide/src/macros/zval_convert.md @@ -1,4 +1,4 @@ -# `ZvalConvert` +# `ZvalConvert` Derive Macro The `#[derive(ZvalConvert)]` macro derives the `FromZval` and `IntoZval` traits on a struct or enum. @@ -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,12 @@ 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 +131,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/migration-guides/v0.14.md b/guide/src/migration-guides/v0.14.md new file mode 100644 index 0000000000..1ff61e1e01 --- /dev/null +++ b/guide/src/migration-guides/v0.14.md @@ -0,0 +1,151 @@ +# Migrating to `v0.14` + +## 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. + +There is no real changes on existing macros, however you will now need to +register functions, classes, constants and startup function when declaring +the module. + +```rs +#[php_module(startup = "startup_function")] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module + .class::() + .function(wrap_function!(hello_world)) + .constant(wrap_constant!(SOME_CONSTANT)) +} +``` + +### 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] +#[derive(Debug)] +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 + } + + pub fn builder_pattern( + self_: &mut ZendClassObject, + ) -> &mut ZendClassObject { + dbg!(self_) + } +} + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module + .class::() +} +``` + +### Constants + +Mostly unchanged in terms of constant definition, however you now need to +register the constant with the module builder: + +```rs +use ext_php_rs::prelude::*; + +#[php_const] +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 + +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}"); +} +``` + +### 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 + +```rs +use ext_php_rs::prelude::*; + +fn startup_function(ty: i32, mod_num: i32) -> i32 { + 0 +} + +#[php_module(startup = "startup_function")] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module +} +``` diff --git a/guide/src/types/class_object.md b/guide/src/types/class_object.md index ed6125cf4a..b18fb3f018 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() {} ``` @@ -58,9 +61,10 @@ impl Example { Example { foo, bar } } } -# #[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..479a55e729 100644 --- a/guide/src/types/object.md +++ b/guide/src/types/object.md @@ -27,10 +27,11 @@ use ext_php_rs::{prelude::*, types::ZendObject}; pub fn take_obj(obj: &mut ZendObject) -> () { let _ = obj.try_call_method("hello", vec![&"arg1", &"arg2"]); } -# #[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() {} ``` @@ -47,10 +48,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 +70,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/args.rs b/src/args.rs index f0e30c1a4b..6c04724fc1 100644 --- a/src/args.rs +++ b/src/args.rs @@ -4,6 +4,7 @@ use std::{ffi::CString, ptr}; use crate::{ convert::{FromZvalMut, IntoZvalDyn}, + describe::{abi, Parameter}, error::{Error, Result}, ffi::{ _zend_expected_type, _zend_expected_type_Z_EXPECTED_ARRAY, @@ -24,7 +25,7 @@ pub struct Arg<'a> { _type: DataType, as_ref: bool, allow_null: bool, - variadic: bool, + pub(crate) variadic: bool, default_value: Option, zval: Option<&'a mut Zval>, variadic_zvals: Vec>, @@ -184,6 +185,17 @@ impl From> for _zend_expected_type { } } +impl From> for Parameter { + fn from(val: Arg<'_>) -> Self { + Parameter { + name: val.name.into(), + ty: Some(val._type).into(), + nullable: val.allow_null, + default: val.default_value.map(abi::RString::from).into(), + } + } +} + /// Internal argument information used by Zend. pub type ArgInfo = zend_internal_arg_info; @@ -238,9 +250,12 @@ impl<'a, 'b> ArgParser<'a, 'b> { /// should break execution after seeing an error type. pub fn parse(mut self) -> Result<()> { let max_num_args = self.args.len(); - let min_num_args = self.min_num_args.unwrap_or(max_num_args); + let mut min_num_args = self.min_num_args.unwrap_or(max_num_args); let num_args = self.arg_zvals.len(); let has_variadic = self.args.last().is_some_and(|arg| arg.variadic); + if has_variadic { + min_num_args = min_num_args.saturating_sub(1); + } if num_args < min_num_args || (!has_variadic && num_args > max_num_args) { // SAFETY: Exported C function is safe, return value is unused and parameters 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/builders/class.rs b/src/builders/class.rs index bc1f29f14a..c717b426d9 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -1,9 +1,10 @@ -use std::{ffi::CString, mem::MaybeUninit}; +use std::{ffi::CString, mem::MaybeUninit, rc::Rc}; use crate::{ builders::FunctionBuilder, class::{ConstructorMeta, ConstructorResult, RegisteredClass}, - convert::IntoZval, + convert::{IntoZval, IntoZvalDyn}, + describe::DocComments, error::{Error, Result}, exception::PhpException, ffi::{ @@ -16,16 +17,20 @@ use crate::{ zend_fastcall, }; +type ConstantEntry = (String, Box Result>, DocComments); + /// Builder for registering a class in PHP. pub struct ClassBuilder { - name: String, + pub(crate) name: String, ce: ClassEntry, extends: Option<&'static ClassEntry>, interfaces: Vec<&'static ClassEntry>, - methods: Vec, + pub(crate) methods: Vec<(FunctionBuilder<'static>, MethodFlags)>, object_override: Option *mut ZendObject>, - properties: Vec<(String, Zval, PropertyFlags)>, - constants: Vec<(String, Zval)>, + pub(crate) properties: Vec<(String, PropertyFlags, DocComments)>, + pub(crate) constants: Vec, + register: Option, + pub(crate) docs: DocComments, } impl ClassBuilder { @@ -47,6 +52,8 @@ impl ClassBuilder { object_override: None, properties: vec![], constants: vec![], + register: None, + docs: &[], } } @@ -82,11 +89,10 @@ impl ClassBuilder { /// /// # Parameters /// - /// * `func` - The function entry to add to the class. + /// * `func` - The function builder to add to the class. /// * `flags` - Flags relating to the function. See [`MethodFlags`]. - pub fn method(mut self, mut func: FunctionEntry, flags: MethodFlags) -> Self { - func.flags |= flags.bits(); - self.methods.push(func); + pub fn method(mut self, func: FunctionBuilder<'static>, flags: MethodFlags) -> Self { + self.methods.push((func, flags)); self } @@ -99,6 +105,7 @@ impl ClassBuilder { /// * `name` - The name of the property to add to the class. /// * `default` - The default value of the property. /// * `flags` - Flags relating to the property. See [`PropertyFlags`]. + /// * `docs` - Documentation comments for the property. /// /// # Panics /// @@ -107,15 +114,10 @@ impl ClassBuilder { pub fn property>( mut self, name: T, - default: impl IntoZval, flags: PropertyFlags, + docs: DocComments, ) -> Self { - let default = match default.into_zval(true) { - Ok(default) => default, - Err(_) => panic!("Invalid default value for property `{}`.", name.into()), - }; - - self.properties.push((name.into(), default, flags)); + self.properties.push((name.into(), flags, docs)); self } @@ -129,10 +131,38 @@ impl ClassBuilder { /// /// * `name` - The name of the constant to add to the class. /// * `value` - The value of the constant. - pub fn constant>(mut self, name: T, value: impl IntoZval) -> Result { - let value = value.into_zval(true)?; + /// * `docs` - Documentation comments for the constant. + pub fn constant>( + mut self, + name: T, + value: impl IntoZval + 'static, + docs: DocComments, + ) -> Result { + self.constants + .push((name.into(), Box::new(|| value.into_zval(true)), docs)); + Ok(self) + } - self.constants.push((name.into(), value)); + /// 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. + /// * `docs` - Documentation comments for the constant. + pub fn dyn_constant>( + mut self, + name: T, + value: &'static dyn IntoZvalDyn, + docs: DocComments, + ) -> Result { + let value = Rc::new(value); + self.constants + .push((name.into(), Box::new(move || value.as_zval(true)), docs)); Ok(self) } @@ -152,9 +182,8 @@ impl ClassBuilder { /// # Parameters /// /// * `T` - The type which will override the Zend object. Must implement - /// [`RegisteredClass`] - /// which can be derived using the [`php_class`](crate::php_class) attribute - /// macro. + /// [`RegisteredClass`] which can be derived using the + /// [`php_class`](crate::php_class) attribute macro. /// /// # Panics /// @@ -170,7 +199,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,25 +240,57 @@ 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") + func }, MethodFlags::Public, ) } - /// Builds the class, returning a reference to the class entry. + /// Function to register the class with PHP. This function is called after + /// the class is built. + /// + /// # Parameters + /// + /// * `register` - The function to call to register the class. + pub fn registration(mut self, register: fn(&'static mut ClassEntry)) -> Self { + self.register = Some(register); + self + } + + /// Sets the documentation for the class. + /// + /// # Parameters + /// + /// * `docs` - The documentation comments for the class. + pub fn docs(mut self, docs: DocComments) -> Self { + self.docs = docs; + self + } + + /// Builds and registers the class. /// /// # Errors /// /// Returns an [`Error`] variant if the class could not be registered. - pub fn build(mut self) -> Result<&'static mut ClassEntry> { + pub fn register(mut self) -> Result<()> { self.ce.name = ZendStr::new_interned(&self.name, true).into_raw(); - self.methods.push(FunctionEntry::end()); - let func = Box::into_raw(self.methods.into_boxed_slice()) as *const FunctionEntry; + let mut methods = self + .methods + .into_iter() + .map(|(method, flags)| { + method.build().map(|mut method| { + method.flags |= flags.bits(); + method + }) + }) + .collect::>>()?; + + methods.push(FunctionEntry::end()); + let func = Box::into_raw(methods.into_boxed_slice()) as *const FunctionEntry; self.ce.info.internal.builtin_functions = func; let class = unsafe { @@ -266,20 +327,20 @@ impl ClassBuilder { }; } - for (name, mut default, flags) in self.properties { + for (name, flags, _) in self.properties { unsafe { zend_declare_property( class, CString::new(name.as_str())?.as_ptr(), name.len() as _, - &mut default, + &mut Zval::new(), flags.bits() as _, ); } } - for (name, value) in self.constants { - let value = Box::into_raw(Box::new(value)); + for (name, value, _) in self.constants { + let value = Box::into_raw(Box::new(value()?)); unsafe { zend_declare_class_constant( class, @@ -294,6 +355,12 @@ impl ClassBuilder { class.__bindgen_anon_2.create_object = Some(object_override); } - Ok(class) + if let Some(register) = self.register { + register(class); + } else { + panic!("Class {} was not registered.", self.name); + } + + Ok(()) } } diff --git a/src/builders/function.rs b/src/builders/function.rs index 99f74f7e2d..6a4f402dd0 100644 --- a/src/builders/function.rs +++ b/src/builders/function.rs @@ -1,5 +1,6 @@ use crate::{ args::{Arg, ArgInfo}, + describe::DocComments, error::{Error, Result}, flags::{DataType, MethodFlags}, types::Zval, @@ -24,13 +25,14 @@ type FunctionPointerHandler = /// Builder for registering a function in PHP. #[derive(Debug)] pub struct FunctionBuilder<'a> { - name: String, + pub(crate) name: String, function: FunctionEntry, - args: Vec>, + pub(crate) args: Vec>, n_req: Option, - retval: Option, + pub(crate) retval: Option, ret_as_ref: bool, - ret_as_null: bool, + pub(crate) ret_as_null: bool, + pub(crate) docs: DocComments, } impl<'a> FunctionBuilder<'a> { @@ -65,6 +67,7 @@ impl<'a> FunctionBuilder<'a> { retval: None, ret_as_ref: false, ret_as_null: false, + docs: &[], } } @@ -93,6 +96,7 @@ impl<'a> FunctionBuilder<'a> { retval: None, ret_as_ref: false, ret_as_null: false, + docs: &[], } } @@ -123,11 +127,6 @@ impl<'a> FunctionBuilder<'a> { self } - pub fn variadic(mut self) -> Self { - self.function.flags |= MethodFlags::Variadic.bits(); - self - } - /// Sets the return value of the function. /// /// # Parameters @@ -142,15 +141,37 @@ impl<'a> FunctionBuilder<'a> { self } + /// Sets the documentation for the function. + /// This is used to generate the PHP stubs for the function. + /// + /// # Parameters + /// + /// * `docs` - The documentation for the function. + pub fn docs(mut self, docs: DocComments) -> Self { + self.docs = docs; + self + } + /// Builds the function converting it into a Zend function entry. /// /// Returns a result containing the function entry if successful. pub fn build(mut self) -> Result { let mut args = Vec::with_capacity(self.args.len() + 1); + let mut n_req = self.n_req.unwrap_or(self.args.len()); + let variadic = self.args.last().is_some_and(|arg| arg.variadic); + + if variadic { + self.function.flags |= MethodFlags::Variadic.bits(); + n_req = n_req.saturating_sub(1); + } // argument header, retval etc + // The first argument is used as `zend_internal_function_info` for the function. + // That struct shares the same memory as `zend_internal_arg_info` which is used + // for the arguments. args.push(ArgInfo { - name: self.n_req.unwrap_or(self.args.len()) as *const _, + // required_num_args + name: n_req as *const _, type_: match self.retval { Some(retval) => { ZendType::empty_from_type(retval, self.ret_as_ref, false, self.ret_as_null) diff --git a/src/builders/mod.rs b/src/builders/mod.rs index 1a72a43a0b..070d717314 100644 --- a/src/builders/mod.rs +++ b/src/builders/mod.rs @@ -9,6 +9,6 @@ mod sapi; pub use class::ClassBuilder; pub use function::FunctionBuilder; -pub use module::ModuleBuilder; +pub use module::{ModuleBuilder, ModuleStartup}; #[cfg(feature = "embed")] pub use sapi::SapiBuilder; diff --git a/src/builders/module.rs b/src/builders/module.rs index ee2b219fdd..f78630be09 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -1,17 +1,21 @@ +use std::{convert::TryFrom, ffi::CString, mem, ptr}; + +use super::{ClassBuilder, FunctionBuilder}; use crate::{ + class::RegisteredClass, + constant::IntoConst, + describe::DocComments, error::Result, ffi::{ext_php_rs_php_build_id, ZEND_MODULE_API_NO}, zend::{FunctionEntry, ModuleEntry}, PHP_DEBUG, PHP_ZTS, }; -use std::{ffi::CString, 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 /// pointer to a `ModuleEntry`. /// -/// ``` +/// ```rust,no_run /// use ext_php_rs::{ /// builders::ModuleBuilder, /// zend::ModuleEntry, @@ -27,62 +31,43 @@ use std::{ffi::CString, 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() +/// .try_into() +/// .unwrap(); +/// entry.into_raw() /// } /// ``` -#[derive(Debug, Clone)] -pub struct ModuleBuilder { - name: String, - version: String, - module: ModuleEntry, - functions: Vec, +#[derive(Debug, Default)] +pub struct ModuleBuilder<'a> { + pub(crate) name: String, + pub(crate) version: String, + pub(crate) functions: Vec>, + pub(crate) constants: Vec<(String, Box, DocComments)>, + pub(crate) classes: Vec ClassBuilder>, + startup_func: Option, + shutdown_func: Option, + request_startup_func: Option, + request_shutdown_func: Option, + post_deactivate_func: Option i32>, + info_func: Option, } -impl ModuleBuilder { +impl ModuleBuilder<'_> { /// Creates a new module builder with a given name and version. /// /// # Arguments /// /// * `name` - The name of the extension. /// * `version` - The current version of the extension. - pub fn new, U: Into>(name: T, version: U) -> Self { + pub fn new(name: impl Into, version: impl Into) -> Self { Self { name: name.into(), version: version.into(), - module: ModuleEntry { - size: mem::size_of::() as u16, - zend_api: ZEND_MODULE_API_NO, - zend_debug: u8::from(PHP_DEBUG), - zts: u8::from(PHP_ZTS), - ini_entry: ptr::null(), - deps: ptr::null(), - name: ptr::null(), - functions: ptr::null(), - module_startup_func: None, - module_shutdown_func: None, - request_startup_func: None, - request_shutdown_func: None, - info_func: None, - version: ptr::null(), - globals_size: 0, - #[cfg(not(php_zts))] - globals_ptr: ptr::null_mut(), - #[cfg(php_zts)] - globals_id_ptr: ptr::null_mut(), - globals_ctor: None, - globals_dtor: None, - post_deactivate_func: None, - module_started: 0, - type_: 0, - handle: ptr::null_mut(), - module_number: 0, - build_id: unsafe { ext_php_rs_php_build_id() }, - }, functions: vec![], + constants: vec![], + classes: vec![], + ..Default::default() } } @@ -92,7 +77,7 @@ impl ModuleBuilder { /// /// * `func` - The function to be called on startup. pub fn startup_function(mut self, func: StartupShutdownFunc) -> Self { - self.module.module_startup_func = Some(func); + self.startup_func = Some(func); self } @@ -102,7 +87,7 @@ impl ModuleBuilder { /// /// * `func` - The function to be called on shutdown. pub fn shutdown_function(mut self, func: StartupShutdownFunc) -> Self { - self.module.module_shutdown_func = Some(func); + self.shutdown_func = Some(func); self } @@ -112,7 +97,7 @@ impl ModuleBuilder { /// /// * `func` - The function to be called when startup is requested. pub fn request_startup_function(mut self, func: StartupShutdownFunc) -> Self { - self.module.request_startup_func = Some(func); + self.request_startup_func = Some(func); self } @@ -122,7 +107,7 @@ impl ModuleBuilder { /// /// * `func` - The function to be called when shutdown is requested. pub fn request_shutdown_function(mut self, func: StartupShutdownFunc) -> Self { - self.module.request_shutdown_func = Some(func); + self.request_shutdown_func = Some(func); self } @@ -135,8 +120,8 @@ impl ModuleBuilder { /// # Arguments /// /// * `func` - The function to be called when shutdown is requested. - pub fn post_deactivate_function(mut self, func: extern "C" fn() -> i32) -> Self { - self.module.post_deactivate_func = Some(func); + pub fn post_deactivate_function(mut self, func: unsafe extern "C" fn() -> i32) -> Self { + self.post_deactivate_func = Some(func); self } @@ -147,7 +132,7 @@ impl ModuleBuilder { /// * `func` - The function to be called to retrieve the information about /// the extension. pub fn info_function(mut self, func: InfoFunc) -> Self { - self.module.info_func = Some(func); + self.info_func = Some(func); self } @@ -156,27 +141,153 @@ impl ModuleBuilder { /// # Arguments /// /// * `func` - The function to be added to the extension. - pub fn function(mut self, func: FunctionEntry) -> Self { + pub fn function(mut self, func: FunctionBuilder<'static>) -> Self { self.functions.push(func); self } - /// Builds the extension and returns a `ModuleEntry`. + /// Adds a constant to the extension. + /// + /// # Arguments /// - /// Returns a result containing the module entry if successful. - pub fn build(mut self) -> Result { - 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(); + /// * `const` - Tuple containing the name, value and doc comments for the constant. This is + /// a tuple to support the [`wrap_constant`] macro. + /// + /// [`wrap_constant`]: crate::wrap_constant + pub fn constant( + mut self, + r#const: (&str, impl IntoConst + Send + 'static, DocComments), + ) -> Self { + let (name, val, docs) = r#const; + self.constants.push(( + name.into(), + Box::new(val) as Box, + docs, + )); + self + } + + /// Adds a class to the extension. + pub fn class(mut self) -> Self { + self.classes.push(|| { + let mut builder = ClassBuilder::new(T::CLASS_NAME); + for (method, flags) in T::method_builders() { + builder = builder.method(method, flags); + } + if let Some(extends) = T::EXTENDS { + builder = builder.extends(extends()); + } + for iface in T::IMPLEMENTS { + builder = builder.implements(iface()); + } + for (name, value, docs) in T::constants() { + builder = builder + .dyn_constant(*name, *value, docs) + .expect("Failed to register constant"); + } + for (name, prop_info) in T::get_properties() { + builder = builder.property(name, prop_info.flags, prop_info.docs); + } + if let Some(modifier) = T::BUILDER_MODIFIER { + builder = modifier(builder); + } + + builder + .object_override::() + .registration(|ce| { + T::get_metadata().set_ce(ce); + }) + .docs(T::DOC_COMMENTS) + }); + self + } +} + +/// Artifacts from the [`ModuleBuilder`] that should be revisited inside the +/// extension startup function. +pub struct ModuleStartup { + constants: Vec<(String, Box)>, + classes: Vec ClassBuilder>, +} + +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)?; + } - Ok(self.module) + self.classes.into_iter().map(|c| c()).for_each(|c| { + c.register().expect("Failed to build class"); + }); + Ok(()) } } /// A function to be called when the extension is starting up or shutting down. -pub type StartupShutdownFunc = extern "C" fn(_type: i32, _module_number: i32) -> i32; +pub type StartupShutdownFunc = unsafe extern "C" fn(_type: i32, _module_number: i32) -> i32; /// A function to be called when `phpinfo();` is called. -pub type InfoFunc = extern "C" fn(zend_module: *mut ModuleEntry); +pub type InfoFunc = unsafe extern "C" fn(zend_module: *mut ModuleEntry); + +/// Builds a [`ModuleEntry`] and [`ModuleStartup`] from a [`ModuleBuilder`]. +/// This is the entry point for the module to be registered with PHP. +impl TryFrom> for (ModuleEntry, ModuleStartup) { + type Error = crate::error::Error; + + fn try_from(builder: ModuleBuilder) -> Result { + let mut functions = builder + .functions + .into_iter() + .map(|f| f.build()) + .collect::>>()?; + functions.push(FunctionEntry::end()); + let functions = Box::into_raw(functions.into_boxed_slice()) as *const FunctionEntry; + + let name = CString::new(builder.name)?.into_raw(); + let version = CString::new(builder.version)?.into_raw(); + + let startup = ModuleStartup { + constants: builder + .constants + .into_iter() + .map(|(n, v, _)| (n, v)) + .collect(), + classes: builder.classes, + }; + + Ok(( + ModuleEntry { + size: mem::size_of::() as u16, + zend_api: ZEND_MODULE_API_NO, + zend_debug: u8::from(PHP_DEBUG), + zts: u8::from(PHP_ZTS), + ini_entry: ptr::null(), + deps: ptr::null(), + name, + functions, + module_startup_func: builder.startup_func, + module_shutdown_func: builder.shutdown_func, + request_startup_func: builder.request_startup_func, + request_shutdown_func: builder.request_shutdown_func, + info_func: builder.info_func, + version, + globals_size: 0, + #[cfg(not(php_zts))] + globals_ptr: ptr::null_mut(), + #[cfg(php_zts)] + globals_id_ptr: ptr::null_mut(), + globals_ctor: None, + globals_dtor: None, + post_deactivate_func: builder.post_deactivate_func, + module_started: 0, + type_: 0, + handle: ptr::null_mut(), + module_number: 0, + build_id: unsafe { ext_php_rs_php_build_id() }, + }, + startup, + )) + } +} diff --git a/src/class.rs b/src/class.rs index c24cdf912a..67ead7d80f 100644 --- a/src/class.rs +++ b/src/class.rs @@ -9,9 +9,12 @@ use std::{ use once_cell::sync::OnceCell; use crate::{ - builders::FunctionBuilder, + builders::{ClassBuilder, FunctionBuilder}, + convert::IntoZvalDyn, + describe::DocComments, exception::PhpException, - props::Property, + flags::{ClassFlags, MethodFlags}, + internal::property::PropertyInfo, zend::{ClassEntry, ExecuteData, ZendObjectHandlers}, }; @@ -21,8 +24,21 @@ 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>; + + /// 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(); + + /// Doc comments for the class. + const DOC_COMMENTS: DocComments = &[]; /// Returns a reference to the class metadata, which stores the class entry /// and handlers. @@ -37,12 +53,21 @@ pub trait RegisteredClass: Sized + 'static { /// /// The key should be the name of the property and the value should be a /// reference to the property with reference to `self`. The value is a - /// [`Property`]. + /// [`PropertyInfo`]. /// /// Instead of using this method directly, you should access the properties /// 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>>; + fn get_properties<'a>() -> HashMap<&'static str, PropertyInfo<'a, Self>>; + + /// Returns the method builders required to build the class. + 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, DocComments)]; } /// Stores metadata about a classes Rust constructor, including the function @@ -87,7 +112,7 @@ impl From for ConstructorResult { /// to PHP. Usually allocated statically. pub struct ClassMetadata { handlers: OnceCell, - properties: OnceCell>>, + properties: OnceCell>>, ce: AtomicPtr, // `AtomicPtr` is used here because it is `Send + Sync`. @@ -166,7 +191,7 @@ impl ClassMetadata { /// # Returns /// /// Immutable reference to the properties hashmap. - pub fn get_properties(&self) -> &HashMap<&'static str, Property<'static, T>> { + pub fn get_properties(&self) -> &HashMap<&'static str, PropertyInfo<'static, T>> { self.properties.get_or_init(T::get_properties) } } diff --git a/src/closure.rs b/src/closure.rs index d9884d133b..0bba5e74ce 100644 --- a/src/closure.rs +++ b/src/closure.rs @@ -7,11 +7,12 @@ use crate::{ builders::{ClassBuilder, FunctionBuilder}, class::{ClassMetadata, RegisteredClass}, convert::{FromZval, IntoZval}, + describe::DocComments, exception::PhpException, flags::{DataType, MethodFlags}, - props::Property, + internal::property::PropertyInfo, types::Zval, - zend::ExecuteData, + zend::{ClassEntry, ExecuteData}, zend_fastcall, }; @@ -32,7 +33,7 @@ static CLOSURE_META: ClassMetadata = ClassMetadata::new(); /// /// class RustClosure { /// public function __invoke(...$args): mixed { -/// // ... +/// // ... /// } /// } /// ``` @@ -122,20 +123,18 @@ impl Closure { panic!("Closure has already been built."); } - let ce = ClassBuilder::new("RustClosure") + ClassBuilder::new("RustClosure") .method( FunctionBuilder::new("__invoke", Self::invoke) .not_required() .arg(Arg::new("args", DataType::Mixed).is_variadic()) - .returns(DataType::Mixed, false, true) - .build() - .expect("Failed to build `RustClosure` PHP class."), + .returns(DataType::Mixed, false, true), MethodFlags::Public, ) .object_override::() - .build() + .registration(|ce| CLOSURE_META.set_ce(ce)) + .register() .expect("Failed to build `RustClosure` PHP class."); - CLOSURE_META.set_ce(ce); } zend_fastcall! { @@ -152,13 +151,33 @@ 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 } - fn get_properties<'a>() -> HashMap<&'static str, Property<'a, Self>> { + fn get_properties<'a>() -> HashMap<&'static str, PropertyInfo<'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, + DocComments, + )] { + unimplemented!() + } } class_derives!(Closure); 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/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/describe/abi.rs b/src/describe/abi.rs index a184cdba68..e0d9dde50b 100644 --- a/src/describe/abi.rs +++ b/src/describe/abi.rs @@ -85,9 +85,59 @@ impl Display for Str { } } +/// An ABI-stable String +#[repr(C)] +pub struct RString { + inner: Vec, +} + +impl RString { + /// Returns the string as a string slice. + pub fn as_str(&self) -> &str { + std::str::from_utf8(&self.inner).expect("RString value is not valid UTF-8") + } +} + +impl From<&str> for RString { + fn from(s: &str) -> Self { + Self { + inner: s.as_bytes().to_vec().into(), + } + } +} + +impl From for RString { + fn from(s: String) -> Self { + Self { + inner: s.into_bytes().into(), + } + } +} + +impl AsRef for RString { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Display for RString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.as_str().fmt(f) + } +} + /// An ABI-stable [`Option`][std::option::Option]. #[repr(C, u8)] pub enum Option { Some(T), None, } + +impl From> for Option { + fn from(opt: std::option::Option) -> Self { + match opt { + Some(val) => Self::Some(val), + None => Self::None, + } + } +} diff --git a/src/describe/mod.rs b/src/describe/mod.rs index bb3a1cec7b..99f0056944 100644 --- a/src/describe/mod.rs +++ b/src/describe/mod.rs @@ -1,14 +1,25 @@ //! Types used to describe downstream extensions. Used by the `cargo-php` //! CLI application to generate PHP stub files used by IDEs. +use bitflags::bitflags_match; +use std::vec::Vec as StdVec; + +use crate::{ + builders::{ClassBuilder, FunctionBuilder}, + constant::IntoConst, + flags::{DataType, MethodFlags, PropertyFlags}, + prelude::ModuleBuilder, +}; +use abi::*; pub mod abi; mod stub; -use crate::flags::DataType; -use abi::*; - pub use stub::ToStub; +/// A slice of strings containing documentation comments. +pub type DocComments = &'static [&'static str]; + +/// Representation of the extension used to generate PHP stubs. #[repr(C)] pub struct Description { /// Extension description. @@ -31,77 +42,275 @@ impl Description { } } +/// Represents a set of comments on an export. +#[repr(C)] +pub struct DocBlock(pub Vec); + +impl From<&'static [&'static str]> for DocBlock { + fn from(val: &'static [&'static str]) -> Self { + Self( + val.iter() + .map(|s| (*s).into()) + .collect::>() + .into(), + ) + } +} + /// Represents an extension containing a set of exports. #[repr(C)] pub struct Module { - pub name: Str, + /// Name of the extension. + pub name: RString, + /// Functions exported by the extension. pub functions: Vec, + /// Classes exported by the extension. pub classes: Vec, + /// Constants exported by the extension. pub constants: Vec, } -/// Represents a set of comments on an export. -#[repr(C)] -pub struct DocBlock(pub Vec); +/// Builds a [`Module`] from a [`ModuleBuilder`]. +/// This is used to generate the PHP stubs for the module. +impl From> for Module { + fn from(builder: ModuleBuilder) -> Self { + let functions = builder.functions; + Self { + name: builder.name.into(), + functions: functions + .into_iter() + .map(Function::from) + .collect::>() + .into(), + classes: builder + .classes + .into_iter() + .map(|c| c().into()) + .collect::>() + .into(), + constants: builder + .constants + .into_iter() + .map(Constant::from) + .collect::>() + .into(), + } + } +} /// Represents an exported function. #[repr(C)] pub struct Function { - pub name: Str, + /// Name of the function. + pub name: RString, + /// Documentation comments for the function. pub docs: DocBlock, + /// Return value of the function. pub ret: Option, + /// Parameters of the function. pub params: Vec, } +impl From> for Function { + fn from(val: FunctionBuilder<'_>) -> Self { + let ret_allow_null = val.ret_as_null; + Function { + name: val.name.into(), + docs: DocBlock( + val.docs + .iter() + .map(|d| (*d).into()) + .collect::>() + .into(), + ), + ret: val + .retval + .map(|r| Retval { + ty: r, + nullable: r != DataType::Mixed && ret_allow_null, + }) + .into(), + params: val + .args + .into_iter() + .map(Parameter::from) + .collect::>() + .into(), + } + } +} + /// Represents a parameter attached to an exported function or method. #[repr(C)] pub struct Parameter { - pub name: Str, + /// Name of the parameter. + pub name: RString, + /// Type of the parameter. pub ty: Option, + /// Whether the parameter is nullable. pub nullable: bool, - pub default: Option, + /// Default value of the parameter. + pub default: Option, } /// Represents an exported class. #[repr(C)] pub struct Class { - pub name: Str, + /// Name of the class. + pub name: RString, + /// Documentation comments for the class. pub docs: DocBlock, - pub extends: Option, - pub implements: Vec, + /// Name of the class the exported class extends. (Not implemented #326) + pub extends: Option, + /// Names of the interfaces the exported class implements. (Not implemented #326) + pub implements: Vec, + /// Properties of the class. pub properties: Vec, + /// Methods of the class. pub methods: Vec, + /// Constants of the class. pub constants: Vec, } +impl From for Class { + fn from(val: ClassBuilder) -> Self { + Self { + name: val.name.into(), + docs: DocBlock( + val.docs + .iter() + .map(|doc| (*doc).into()) + .collect::>() + .into(), + ), + extends: abi::Option::None, // TODO: Implement extends #326 + implements: vec![].into(), // TODO: Implement implements #326 + properties: val + .properties + .into_iter() + .map(Property::from) + .collect::>() + .into(), + methods: val + .methods + .into_iter() + .map(Method::from) + .collect::>() + .into(), + constants: val + .constants + .into_iter() + .map(|(name, _, docs)| (name, docs)) + .map(Constant::from) + .collect::>() + .into(), + } + } +} + /// Represents a property attached to an exported class. #[repr(C)] pub struct Property { - pub name: Str, + /// Name of the property. + pub name: RString, + /// Documentation comments for the property. pub docs: DocBlock, + /// Type of the property (Not implemented #376) pub ty: Option, + /// Visibility of the property. pub vis: Visibility, + /// Whether the property is static. pub static_: bool, + /// Whether the property is nullable. (Not implemented #376) pub nullable: bool, - pub default: Option, + /// Default value of the property. (Not implemented #376) + pub default: Option, +} + +impl From<(String, PropertyFlags, DocComments)> for Property { + fn from(value: (String, PropertyFlags, DocComments)) -> Self { + let (name, flags, docs) = value; + let static_ = flags.contains(PropertyFlags::Static); + let vis = Visibility::from(flags); + // TODO: Implement ty #376 + let ty = abi::Option::None; + // TODO: Implement default #376 + let default = abi::Option::::None; + // TODO: Implement nullable #376 + let nullable = false; + let docs = docs.into(); + println!("Property: {:?}", name); + Self { + name: name.into(), + docs, + ty, + vis, + static_, + nullable, + default, + } + } } /// Represents a method attached to an exported class. #[repr(C)] pub struct Method { - pub name: Str, + /// Name of the method. + pub name: RString, + /// Documentation comments for the method. pub docs: DocBlock, + /// Type of the method. pub ty: MethodType, + /// Parameters of the method. pub params: Vec, + /// Return value of the method. pub retval: Option, + /// Whether the method is static. pub _static: bool, + /// Visibility of the method. pub visibility: Visibility, } +impl From<(FunctionBuilder<'_>, MethodFlags)> for Method { + fn from(val: (FunctionBuilder<'_>, MethodFlags)) -> Self { + let (builder, flags) = val; + let ret_allow_null = builder.ret_as_null; + Method { + name: builder.name.into(), + docs: DocBlock( + builder + .docs + .iter() + .map(|d| (*d).into()) + .collect::>() + .into(), + ), + retval: builder + .retval + .map(|r| Retval { + ty: r, + nullable: r != DataType::Mixed && ret_allow_null, + }) + .into(), + params: builder + .args + .into_iter() + .map(|a| a.into()) + .collect::>() + .into(), + ty: flags.into(), + _static: flags.contains(MethodFlags::Static), + visibility: flags.into(), + } + } +} + /// Represents a value returned from a function or method. #[repr(C)] pub struct Retval { + /// Type of the return value. pub ty: DataType, + /// Whether the return value is nullable. pub nullable: bool, } @@ -109,25 +318,88 @@ pub struct Retval { #[repr(C)] #[derive(Clone, Copy)] pub enum MethodType { + /// A member method. Member, + /// A static method. Static, + /// A constructor. Constructor, } +impl From for MethodType { + fn from(value: MethodFlags) -> Self { + match value { + MethodFlags::Static => Self::Static, + MethodFlags::IsConstructor => Self::Constructor, + _ => Self::Member, + } + } +} + /// Enumerator used to differentiate between different method and property /// visibilties. #[repr(C)] #[derive(Clone, Copy)] pub enum Visibility { + /// Private visibility. Private, + /// Protected visibility. Protected, + /// Public visibility. Public, } +impl From for Visibility { + fn from(value: PropertyFlags) -> Self { + bitflags_match!(value, { + PropertyFlags::Public => Visibility::Public, + PropertyFlags::Protected => Visibility::Protected, + PropertyFlags::Private => Visibility::Private, + _ => Visibility::Public, + }) + } +} + +impl From for Visibility { + fn from(value: MethodFlags) -> Self { + bitflags_match!(value, { + MethodFlags::Public => Self::Public, + MethodFlags::Protected => Self::Protected, + MethodFlags::Private => Self::Private, + _ => Self::Public, + }) + } +} + /// Represents an exported constant, stand alone or attached to a class. #[repr(C)] pub struct Constant { - pub name: Str, + /// Name of the constant. + pub name: RString, + /// Documentation comments for the constant. pub docs: DocBlock, - pub value: Option, + /// Value of the constant. + pub value: Option, +} + +impl From<(String, DocComments)> for Constant { + fn from(val: (String, DocComments)) -> Self { + let (name, docs) = val; + Constant { + name: name.into(), + value: abi::Option::None, + docs: docs.into(), + } + } +} + +impl From<(String, Box, DocComments)> for Constant { + fn from(val: (String, Box, DocComments)) -> Self { + let (name, _, docs) = val; + Constant { + name: name.into(), + value: abi::Option::None, + docs: docs.into(), + } + } } diff --git a/src/describe/stub.rs b/src/describe/stub.rs index 17cfbfde36..46daeb5596 100644 --- a/src/describe/stub.rs +++ b/src/describe/stub.rs @@ -42,7 +42,7 @@ impl ToStub for Module { fn fmt_stub(&self, buf: &mut String) -> FmtResult { writeln!(buf, ">() .join(", ") )?; diff --git a/src/exception.rs b/src/exception.rs index aee0d9099f..5465ecc10b 100644 --- a/src/exception.rs +++ b/src/exception.rs @@ -187,7 +187,7 @@ pub fn throw_with_code(ex: &ClassEntry, code: i32, message: &str) -> Result<()> /// use crate::ext_php_rs::convert::IntoZval; /// /// #[php_class] -/// #[extends(ext_php_rs::zend::ce::exception())] +/// #[extends(ext_php_rs::zend::ce::exception)] /// pub struct JsException { /// #[prop(flags = ext_php_rs::flags::PropertyFlags::Public)] /// message: String, diff --git a/src/flags.rs b/src/flags.rs index 259a73e35a..7eadd4bd7d 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -136,11 +136,17 @@ bitflags! { /// Flags for building properties. #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] pub struct PropertyFlags: u32 { + /// Visibility public const Public = ZEND_ACC_PUBLIC; + /// Visibility protected const Protected = ZEND_ACC_PROTECTED; + /// Visibility private const Private = ZEND_ACC_PRIVATE; + /// Property or method overrides private one const Changed = ZEND_ACC_CHANGED; + /// Static property const Static = ZEND_ACC_STATIC; + /// Promoted property const Promoted = ZEND_ACC_PROMOTED; } } diff --git a/src/internal.rs b/src/internal.rs deleted file mode 100644 index 44d9af34d8..0000000000 --- a/src/internal.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Internal, public functions that are called from downstream extensions. - -/// Called by startup functions registered with the [`#[php_startup]`] macro. -/// Initializes all classes that are defined by ext-php-rs (i.e. `Closure`). -/// -/// [`#[php_startup]`]: crate::php_startup -#[inline(always)] -pub fn ext_php_rs_startup() { - #[cfg(feature = "closure")] - crate::closure::Closure::build(); -} diff --git a/src/internal/class.rs b/src/internal/class.rs new file mode 100644 index 0000000000..d03540a1ae --- /dev/null +++ b/src/internal/class.rs @@ -0,0 +1,64 @@ +use std::{collections::HashMap, marker::PhantomData}; + +use crate::{ + builders::FunctionBuilder, + class::{ConstructorMeta, RegisteredClass}, + convert::{IntoZval, IntoZvalDyn}, + describe::DocComments, + flags::MethodFlags, + 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<(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, DocComments)]; +} + +// 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<(FunctionBuilder<'static>, MethodFlags)> { + Default::default() + } + + #[inline] + fn get_method_props<'a>(self) -> HashMap<&'static str, Property<'a, T>> { + Default::default() + } + + #[inline] + fn get_constructor(self) -> Option> { + Default::default() + } + + #[inline] + fn get_constants(self) -> &'static [(&'static str, &'static dyn IntoZvalDyn, DocComments)] { + &[] + } +} + +// 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/internal/function.rs b/src/internal/function.rs new file mode 100644 index 0000000000..4f953a206d --- /dev/null +++ b/src/internal/function.rs @@ -0,0 +1,8 @@ +use crate::builders::FunctionBuilder; + +/// Implemented on ZSTs that represent PHP functions. +pub trait PhpFunction { + /// Function used to 'build' the PHP function, returning a [`FunctionBuilder`] + /// used to build the function. + const FUNCTION_ENTRY: fn() -> FunctionBuilder<'static>; +} diff --git a/src/internal/mod.rs b/src/internal/mod.rs new file mode 100644 index 0000000000..fe297df41e --- /dev/null +++ b/src/internal/mod.rs @@ -0,0 +1,26 @@ +//! 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; +pub mod property; + +/// A mutex type that contains a [`ModuleStartup`] instance. +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. +/// Initializes all classes that are defined by ext-php-rs (i.e. `Closure`). +/// +/// [`#[php_startup]`]: crate::php_startup +#[inline(always)] +pub fn ext_php_rs_startup() { + #[cfg(feature = "closure")] + crate::closure::Closure::build(); +} diff --git a/src/internal/property.rs b/src/internal/property.rs new file mode 100644 index 0000000000..2bc90dc7a9 --- /dev/null +++ b/src/internal/property.rs @@ -0,0 +1,7 @@ +use crate::{describe::DocComments, flags::PropertyFlags, props::Property}; + +pub struct PropertyInfo<'a, T> { + pub prop: Property<'a, T>, + pub flags: PropertyFlags, + pub docs: DocComments, +} diff --git a/src/lib.rs b/src/lib.rs index 1f42a1d878..8af7fdd333 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,17 +43,13 @@ pub mod prelude { #[cfg_attr(docs, doc(cfg(feature = "closure")))] 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; - 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::ZvalConvert; + pub use crate::{ + php_class, php_const, php_extern, php_function, php_impl, php_module, wrap_constant, + wrap_function, zend_fastcall, ZvalConvert, + }; } /// `ext-php-rs` version. @@ -65,650 +61,7 @@ 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. -/// -/// This allows you to 'import' PHP functions into Rust so that they can be -/// called like regular Rust functions. Parameters can be any type that -/// implements [`IntoZval`], and the return type can be anything that implements -/// [`From`] (notice how [`Zval`] is consumed rather than borrowed in this -/// case). -/// -/// # Panics -/// -/// The function can panic when called under a few circumstances: -/// -/// * The function could not be found or was not callable. -/// * One of the parameters could not be converted into a [`Zval`]. -/// * The actual function call failed internally. -/// * The output [`Zval`] could not be parsed into the output type. -/// -/// The last point can be important when interacting with functions that return -/// unions, such as [`strpos`] which can return an integer or a boolean. In this -/// case, a [`Zval`] should be returned as parsing a boolean to an integer is -/// invalid, and vice versa. -/// -/// # Example -/// -/// This `extern` block imports the [`strpos`] function from PHP. Notice that -/// the string parameters can take either [`String`] or [`&str`], the optional -/// parameter `offset` is an [`Option`], and the return value is a [`Zval`] -/// as the return type is an integer-boolean union. -/// -/// ``` -/// # #![cfg_attr(windows, feature(abi_vectorcall))] -/// # use ext_php_rs::prelude::*; -/// # use ext_php_rs::types::Zval; -/// #[php_extern] -/// extern "C" { -/// fn strpos(haystack: &str, needle: &str, offset: Option) -> Zval; -/// } -/// -/// #[php_function] -/// pub fn my_strpos() { -/// assert_eq!(unsafe { strpos("Hello", "e", None) }.long(), Some(1)); -/// } -/// # #[php_module] -/// # pub fn module(module: ModuleBuilder) -> ModuleBuilder { -/// # module -/// # } -/// ``` -/// -/// [`strpos`]: https://www.php.net/manual/en/function.strpos.php -/// [`IntoZval`]: crate::convert::IntoZval -/// [`Zval`]: crate::types::Zval -pub use ext_php_rs_derive::php_extern; - -/// Attribute used to annotate a function as a PHP function. -/// -/// Only types that implement [`FromZval`] can be used as parameter and return -/// types. These include but are not limited to the following: -/// -/// - Most primitive integers ([`i8`], [`i16`], [`i32`], [`i64`], [`u8`], -/// [`u16`], [`u32`], [`u64`], -/// [`usize`], [`isize`]) -/// - Double-precision floating point numbers ([`f64`]) -/// - [`bool`] -/// - [`String`] -/// - [`Vec`] and [`HashMap`](std::collections::HashMap) where `T: -/// FromZval`. -/// - [`Binary`] for passing binary data as a string, where `T: Pack`. -/// - [`ZendCallable`] for receiving PHP callables, not applicable for return -/// values. -/// - [`Option`] where `T: FromZval`. When used as a parameter, the parameter -/// will be -/// deemed nullable, and will contain [`None`] when `null` is passed. When used -/// as a return type, if [`None`] is returned the [`Zval`] will be set to null. -/// Optional parameters *must* be of the type [`Option`]. -/// -/// Additionally, you are able to return a variant of [`Result`]. `T` must -/// implement [`IntoZval`] and `E` must implement `Into`. If an -/// error variant is returned, a PHP exception is thrown using the -/// [`PhpException`] struct contents. -/// -/// You are able to implement [`FromZval`] on your own custom types to have -/// arguments passed in seamlessly. Similarly, you can implement [`IntoZval`] on -/// values that you want to be able to be returned from PHP functions. -/// -/// Parameters may be deemed optional by passing the parameter name into the -/// attribute options. Note that all parameters that are optional (which -/// includes the given optional parameter as well as all parameters after) -/// *must* be of the type [`Option`], where `T` is a valid type. -/// -/// Generics are *not* supported. -/// -/// Behind the scenes, an `extern "C"` wrapper function is generated, which is -/// actually called by PHP. The first example function would be converted into a -/// function which looks like so: -/// -/// ```no_run -/// # #![cfg_attr(windows, feature(abi_vectorcall))] -/// # use ext_php_rs::{prelude::*, exception::PhpException, zend::ExecuteData, convert::{FromZvalMut, IntoZval}, types::Zval, args::{Arg, ArgParser}}; -/// pub fn hello(name: String) -> String { -/// format!("Hello, {}!", name) -/// } -/// -/// pub extern "C" fn _internal_php_hello(ex: &mut ExecuteData, retval: &mut Zval) { -/// let mut name = Arg::new("name", ::TYPE); -/// let parser = ex.parser() -/// .arg(&mut name) -/// .parse(); -/// -/// if parser.is_err() { -/// return; -/// } -/// -/// let result = hello(match name.val() { -/// Some(val) => val, -/// None => { -/// PhpException::default("Invalid value given for argument `name`.".into()) -/// .throw() -/// .expect("Failed to throw exception: Invalid value given for argument `name`."); -/// return; -/// } -/// }); -/// -/// match result.set_zval(retval, false) { -/// Ok(_) => {}, -/// Err(e) => { -/// let e: PhpException = e.into(); -/// e.throw().expect("Failed to throw exception: Failed to set return value."); -/// } -/// }; -/// } -/// ``` -/// -/// This allows the original function to continue being used while also being -/// exported as a PHP function. -/// -/// # Examples -/// -/// Creating a simple function which will return a string. The function still -/// must be declared in the PHP module to be able to call. -/// -/// ``` -/// # #![cfg_attr(windows, feature(abi_vectorcall))] -/// # use ext_php_rs::prelude::*; -/// #[php_function] -/// pub fn hello(name: String) -> String { -/// format!("Hello, {}!", name) -/// } -/// # #[php_module] -/// # pub fn module(module: ModuleBuilder) -> ModuleBuilder { -/// # module -/// # } -/// ``` -/// -/// Parameters can also be deemed optional by passing the parameter name in the -/// attribute options. This function takes one required parameter (`name`) and -/// two optional parameters (`description` and `age`). -/// -/// ``` -/// # #![cfg_attr(windows, feature(abi_vectorcall))] -/// # use ext_php_rs::prelude::*; -/// #[php_function(optional = "description")] -/// pub fn hello(name: String, description: Option, age: Option) -> String { -/// let mut response = format!("Hello, {}!", name); -/// -/// if let Some(description) = description { -/// response.push_str(format!(" {}.", description).as_ref()); -/// } -/// -/// if let Some(age) = age { -/// response.push_str(format!(" I am {} year(s) old.", age).as_ref()); -/// } -/// -/// response -/// } -/// # #[php_module] -/// # pub fn module(module: ModuleBuilder) -> ModuleBuilder { -/// # module -/// # } -/// ``` -/// -/// Defaults can also be given in a similar fashion. For example, the above -/// function could have default values for `description` and `age` by changing -/// the attribute to the following: -/// -/// ``` -/// # #![cfg_attr(windows, feature(abi_vectorcall))] -/// # use ext_php_rs::prelude::*; -/// #[php_function(optional = "description", defaults(description = "David", age = 10))] -/// pub fn hello(name: String, description: String, age: i32) -> String { -/// format!("Hello, {}! {}. I am {} year(s) old.", name, description, age) -/// } -/// # #[php_module] -/// # pub fn module(module: ModuleBuilder) -> ModuleBuilder { -/// # module -/// # } -/// ``` -/// -/// [`Result`]: std::result::Result -/// [`FunctionBuilder`]: crate::php::function::FunctionBuilder -/// [`FromZval`]: crate::convert::FromZval -/// [`IntoZval`]: crate::convert::IntoZval -/// [`Zval`]: crate::types::Zval. -/// [`Binary`]: crate::binary::Binary -/// [`ZendCallable`]: crate::types::ZendCallable -/// [`PhpException`]: crate::exception::PhpException -pub use ext_php_rs_derive::php_function; - -/// Annotates a structs `impl` block, declaring that all methods and constants -/// declared inside the `impl` block will be declared as PHP methods and -/// constants. -/// -/// If you do not want to export a method to PHP, declare it in another `impl` -/// block that is not tagged with this macro. -/// -/// The declared methods and functions are kept intact so they can continue to -/// 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. -/// -/// The only contrary to this is setting the visibility, optional argument and -/// default arguments for methods. These are done through separate macros: -/// -/// - `#[defaults(key = value, ...)]` for setting defaults of method variables, -/// similar to the -/// function macro. Arguments with defaults need to be optional. -/// - `#[optional(key)]` for setting `key` as an optional argument (and -/// therefore the rest of the -/// arguments). -/// - `#[public]`, `#[protected]` and `#[private]` for setting the visibility of -/// the method, -/// defaulting to public. The Rust visibility has no effect on the PHP -/// visibility. -/// -/// Methods can take a immutable or a mutable reference to `self`, but cannot -/// consume `self`. They can also take no reference to `self` which indicates a -/// static method. -/// -/// ## Constructors -/// -/// You may add *one* constructor to the impl block. This method must be called -/// `__construct` or be tagged with the `#[constructor]` attribute, and it will -/// not be exported to PHP like a regular method. -/// -/// The constructor method must not take a reference to `self` and must return -/// `Self` or [`Result`][`Result`], where `E: Into`. -/// -/// # Example -/// -/// ```no_run -/// # #![cfg_attr(windows, feature(abi_vectorcall))] -/// # use ext_php_rs::prelude::*; -/// #[php_class] -/// #[derive(Debug)] -/// pub struct Human { -/// name: String, -/// age: i32, -/// } -/// -/// #[php_impl] -/// impl Human { -/// // Class constant - `Human::AGE_LIMIT` -/// const AGE_LIMIT: i32 = 100; -/// -/// #[optional(age)] -/// #[defaults(age = 0)] -/// pub fn __construct(name: String, age: i32) -> Self { -/// Self { name, age } -/// } -/// -/// pub fn get_name(&self) -> String { -/// self.name.clone() -/// } -/// -/// pub fn get_age(&self) -> i32 { -/// self.age -/// } -/// -/// // Static method - `Human::get_age_limit()` -/// pub fn get_age_limit() -> i32 { -/// Self::AGE_LIMIT -/// } -/// } -/// -/// #[php_module] -/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { -/// module -/// } -/// ``` -pub use ext_php_rs_derive::php_impl; - -/// Annotates a function that will be used by PHP to retrieve information about -/// the module. -/// -/// In the process, the function is wrapped by an `extern "C"` function which is -/// called from PHP, which then calls the given function. -/// -/// As well as wrapping the function, the `ModuleBuilder` is initialized and -/// functions which have already been declared with the [`macro@php_function`] -/// attribute will be registered with the module, so ideally you won't have to -/// do anything inside the function. -/// -/// The attribute must be called on a function *last*, i.e. the last proc-macro -/// to be compiled, as the attribute relies on all other PHP attributes being -/// compiled before the module. If another PHP attribute is compiled after the -/// module attribute, an error will be thrown. -/// -/// Note that if the function is not called `get_module`, it will be renamed. -/// -/// If you have defined classes using the [`macro@php_class`] macro and you have -/// not defined a startup function, it will be automatically declared and -/// registered. -/// -/// # Example -/// -/// The `get_module` function is required in every PHP extension. This is a bare -/// minimum example, since the function is declared above the module it will -/// automatically be registered when the module attribute is called. -/// -/// ``` -/// # #![cfg_attr(windows, feature(abi_vectorcall))] -/// # use ext_php_rs::prelude::*; -/// #[php_function] -/// pub fn hello(name: String) -> String { -/// format!("Hello, {}!", name) -/// } -/// -/// #[php_module] -/// pub fn module(module: ModuleBuilder) -> ModuleBuilder { -/// module -/// } -/// ``` -pub use ext_php_rs_derive::php_module; - -/// Annotates a struct that will be exported to PHP as a class. -/// -/// By default, the class cannot be constructed from PHP. You must add a -/// constructor method in the [`macro@php_impl`] impl block to be able to -/// construct the object from PHP. -/// -/// This attribute takes a set of optional arguments: -/// -/// * `name` - The name of the exported class, if it is different from the Rust -/// struct name. This can be useful for namespaced classes, as you cannot -/// place backslashes in Rust struct names. -/// -/// Any struct that uses this attribute can also provide an optional set of -/// extra attributes, used to modify the class. These attributes must be used -/// **underneath** this attribute, as they are not valid Rust attributes, and -/// instead are parsed by this attribute: -/// -/// * `#[extends(ce)]` - Sets the parent class of this new class. Can only be -/// used once, and `ce` may be any valid expression. -/// * `#[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. -/// -/// # Example -/// -/// Export a simple class called `Example`, with 3 Rust fields. -/// -/// ``` -/// # #![cfg_attr(windows, feature(abi_vectorcall))] -/// # use ext_php_rs::prelude::*; -/// #[php_class] -/// pub struct Example { -/// x: i32, -/// y: String, -/// z: bool -/// } -/// -/// #[php_module] -/// pub fn module(module: ModuleBuilder) -> ModuleBuilder { -/// module -/// } -/// ``` -/// -/// Create a custom exception `RedisException` inside the namespace -/// `Redis\Exception`: -/// -/// ``` -/// # #![cfg_attr(windows, feature(abi_vectorcall))] -/// # use ext_php_rs::prelude::*; -/// use ext_php_rs::exception::PhpException; -/// use ext_php_rs::zend::ce; -/// -/// #[php_class(name = "Redis\\Exception\\RedisException")] -/// #[extends(ce::exception())] -/// pub struct Example; -/// -/// #[php_function] -/// pub fn throw_exception() -> Result { -/// Err(PhpException::from_class::("Bad things happen".into())) -/// } -/// -/// #[php_module] -/// pub fn module(module: ModuleBuilder) -> ModuleBuilder { -/// 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. -/// -/// # Structs -/// -/// When the macro is used on a struct, the [`FromZendObject`] and -/// [`IntoZendObject`] traits are also implemented, and will attempt to retrieve -/// values for the struct fields from the objects properties. This can be useful -/// when you expect some arbitrary object (of which the type does not matter), -/// but you care about the value of the properties. -/// -/// All properties must implement [`FromZval`] and [`IntoZval`] themselves. -/// Generics are supported, however, a [`FromZval`] and [`IntoZval`] bound will -/// be added. If one property cannot be retrieved from the object, the whole -/// conversion will fail. -/// -/// ## Examples -/// -/// Basic example with some primitive PHP type. -/// -/// ``` -/// # #![cfg_attr(windows, feature(abi_vectorcall))] -/// # use ext_php_rs::prelude::*; -/// #[derive(Debug, ZvalConvert)] -/// pub struct ExampleStruct<'a> { -/// a: i32, -/// b: String, -/// c: &'a str -/// } -/// -/// #[php_function] -/// pub fn take_object(obj: ExampleStruct) { -/// dbg!(obj); -/// } -/// -/// #[php_function] -/// pub fn give_object() -> ExampleStruct<'static> { -/// ExampleStruct { -/// a: 5, -/// b: "Hello, world!".into(), -/// c: "Static string", -/// } -/// } -/// ``` -/// -/// Can be used in PHP: -/// -/// ```php -/// $obj = (object) [ -/// 'a' => 5, -/// 'b' => 'Hello, world!', -/// 'c' => 'asdf', -/// ]; -/// take_object($obj); -/// var_dump(give_object()); -/// ``` -/// -/// Another example involving generics: -/// -/// ``` -/// # #![cfg_attr(windows, feature(abi_vectorcall))] -/// # use ext_php_rs::prelude::*; -/// #[derive(Debug, ZvalConvert)] -/// pub struct CompareVals> { -/// a: T, -/// b: T -/// } -/// -/// #[php_function] -/// pub fn take_object(obj: CompareVals) { -/// dbg!(obj); -/// } -/// ``` -/// -/// # Enums -/// -/// When the macro is used on an enum, the [`FromZval`] and [`IntoZval`] -/// implementations will treat the enum as a tagged union with a mixed datatype. -/// This allows you to accept two different types in a parameter, for example, a -/// string and an integer. -/// -/// The enum variants must not have named fields (i.e. not in the form of a -/// struct), and must have exactly one field, the type to extract from the -/// [`Zval`]. Optionally, the enum may have a single default, empty variant, -/// which is used when the [`Zval`] did not contain any data to fill -/// the other variants. This empty variant is equivalent to `null` in PHP. -/// -/// The ordering of the enum variants is important, as the [`Zval`] contents is -/// matched in order of the variants. For example, [`Zval::string`] will attempt -/// to read a string from the [`Zval`], and if the [`Zval`] contains a long, the -/// long will be converted to a string. If a string variant was placed above an -/// integer variant in the enum, the integer would be converted into a -/// string and passed as the string variant. -/// -/// ## Examples -/// -/// Basic example showing the importance of variant ordering and default field: -/// -/// ``` -/// # #![cfg_attr(windows, feature(abi_vectorcall))] -/// # use ext_php_rs::prelude::*; -/// #[derive(Debug, ZvalConvert)] -/// pub enum UnionExample<'a> { -/// Long(u64), // Long -/// ProperStr(&'a str), // Actual string - not a converted value -/// ParsedStr(String), // Potentially parsed string, i.e. a double -/// None // Zval did not contain anything that could be parsed above -/// } -/// -/// #[php_function] -/// pub fn test_union(val: UnionExample) { -/// dbg!(val); -/// } -/// -/// #[php_function] -/// pub fn give_union() -> UnionExample<'static> { -/// UnionExample::Long(5) -/// } -/// ``` -/// -/// Use in PHP: -/// -/// ```php -/// test_union(5); // UnionExample::Long(5) -/// test_union("Hello, world!"); // UnionExample::ProperStr("Hello, world!") -/// test_union(5.66666); // UnionExample::ParsedStr("5.6666") -/// test_union(null); // UnionExample::None -/// var_dump(give_union()); // int(5) -/// ``` -/// -/// [`FromZval`]: crate::convert::FromZval -/// [`IntoZval`]: crate::convert::IntoZval -/// [`FromZendObject`]: crate::convert::FromZendObject -/// [`IntoZendObject`]: crate::convert::IntoZendObject -/// [`Zval`]: crate::types::Zval. -/// [`Zval::string`]: crate::types::Zval.::string -pub use ext_php_rs_derive::ZvalConvert; - -/// Defines an `extern` function with the Zend fastcall convention based on -/// operating system. -/// -/// On Windows, Zend fastcall functions use the vector calling convention, while -/// on all other operating systems no fastcall convention is used (just the -/// regular C calling convention). -/// -/// This macro wraps a function and applies the correct calling convention. -/// -/// ## Examples -/// -/// ``` -/// # #![cfg_attr(windows, feature(abi_vectorcall))] -/// use ext_php_rs::zend_fastcall; -/// -/// zend_fastcall! { -/// pub extern fn test_hello_world(a: i32, b: i32) -> i32 { -/// a + b -/// } -/// } -/// ``` -/// -/// On Windows, this function will have the signature `pub extern "vectorcall" -/// fn(i32, i32) -> i32`, while on macOS/Linux the function will have the -/// signature `pub extern "C" fn(i32, i32) -> i32`. -/// -/// ## Support -/// -/// 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::{ + php_class, php_const, php_extern, php_function, php_impl, php_module, wrap_constant, + wrap_function, zend_fastcall, ZvalConvert, +}; diff --git a/src/macros.rs b/src/macros.rs index 49641e6da5..ae295a7275 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::{ClassFlags, MethodFlags}, internal::property::PropertyInfo, describe::DocComments}; /// use ext_php_rs::class_derives; /// /// struct Test { @@ -211,17 +211,33 @@ 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] = &[]; +/// const FLAGS: ClassFlags = ClassFlags::empty(); +/// const DOC_COMMENTS: DocComments = &[]; /// /// fn get_metadata() -> &'static ext_php_rs::class::ClassMetadata { /// todo!() /// } /// /// fn get_properties<'a>( -/// ) -> std::collections::HashMap<&'static str, ext_php_rs::props::Property<'a, Self>> +/// ) -> std::collections::HashMap<&'static str, PropertyInfo<'a, Self>> /// { /// todo!() /// } +/// +/// fn method_builders() -> Vec<(FunctionBuilder<'static>, MethodFlags)> { +/// todo!() +/// } +/// +/// fn constructor() -> Option> { +/// todo!() +/// } +/// +/// fn constants() -> &'static [(&'static str, &'static dyn IntoZvalDyn, DocComments)] { +/// todo!() +/// } /// } /// /// class_derives!(Test); @@ -301,6 +317,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( @@ -329,6 +346,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 0570ad4d2c..7d4205985a 100644 --- a/src/types/array.rs +++ b/src/types/array.rs @@ -863,6 +863,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); @@ -928,6 +929,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()?; @@ -992,6 +994,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 c9ac51ed62..c03ac321f3 100644 --- a/src/types/class_object.rs +++ b/src/types/class_object.rs @@ -272,6 +272,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(); @@ -282,6 +283,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 0d45fd8479..1dc5f9c343 100644 --- a/src/types/object.rs +++ b/src/types/object.rs @@ -353,6 +353,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<()> { @@ -367,6 +368,7 @@ impl IntoZval for ZBox { impl IntoZval for &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 6de991d8c7..d7ef148532 100644 --- a/src/types/zval.rs +++ b/src/types/zval.rs @@ -696,6 +696,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; diff --git a/src/zend/handlers.rs b/src/zend/handlers.rs index d8f8ac148e..fe31a438a0 100644 --- a/src/zend/handlers.rs +++ b/src/zend/handlers.rs @@ -94,8 +94,8 @@ impl ZendObjectHandlers { rv_mut.u1.type_info = ZvalTypeFlags::Null.bits(); Ok(match prop { - Some(prop) => { - prop.get(self_, rv_mut)?; + Some(prop_info) => { + prop_info.prop.get(self_, rv_mut)?; rv } None => zend_std_read_property(object, member, type_, cache_slot, rv), @@ -138,8 +138,8 @@ impl ZendObjectHandlers { let value_mut = value.as_mut().ok_or("Invalid return zval given")?; Ok(match prop { - Some(prop) => { - prop.set(self_, value_mut)?; + Some(prop_info) => { + prop_info.prop.set(self_, value_mut)?; value } None => zend_std_write_property(object, member, value, cache_slot), @@ -172,7 +172,7 @@ impl ZendObjectHandlers { for (name, val) in struct_props { let mut zv = Zval::new(); - if val.get(self_, &mut zv).is_err() { + if val.prop.get(self_, &mut zv).is_err() { continue; } props.insert(name, zv).map_err(|e| { @@ -225,7 +225,7 @@ impl ZendObjectHandlers { 0 => { if let Some(val) = prop { let mut zv = Zval::new(); - val.get(self_, &mut zv)?; + val.prop.get(self_, &mut zv)?; if !zv.is_null() { return Ok(1); } @@ -236,7 +236,7 @@ impl ZendObjectHandlers { 1 => { if let Some(val) = prop { let mut zv = Zval::new(); - val.get(self_, &mut zv)?; + val.prop.get(self_, &mut zv)?; cfg_if::cfg_if! { if #[cfg(php84)] { diff --git a/tests/module.rs b/tests/module.rs index 6407f998e6..afbb9269e4 100644 --- a/tests/module.rs +++ b/tests/module.rs @@ -38,5 +38,5 @@ pub fn hello_world(name: String) -> String { #[php_module] pub fn module(module: ModuleBuilder) -> ModuleBuilder { - module + module.function(wrap_function!(hello_world)) } diff --git a/tests/sapi.rs b/tests/sapi.rs index 98a53e9530..def0bc7578 100644 --- a/tests/sapi.rs +++ b/tests/sapi.rs @@ -94,5 +94,5 @@ pub fn hello_world(name: String) -> String { #[php_module] pub fn module(module: ModuleBuilder) -> ModuleBuilder { - module + module.function(wrap_function!(hello_world)) } diff --git a/tests/src/integration/variadic_args.php b/tests/src/integration/variadic_args.php new file mode 100644 index 0000000000..28dc2104aa --- /dev/null +++ b/tests/src/integration/variadic_args.php @@ -0,0 +1,36 @@ + Zval { } } +// Rust type &[&Zval] must be converted because to Vec because of +// lifetime hell. +#[php_function] +pub fn test_variadic_args(params: &[&Zval]) -> Vec { + params.iter().map(|x| x.shallow_clone()).collect() +} + +#[php_function] +pub fn test_variadic_add_required(number: u32, numbers: &[&Zval]) -> u32 { + number + + numbers + .iter() + .map(|x| x.long().unwrap() as u32) + .sum::() +} + #[php_class] pub struct TestClass { string: String, @@ -206,8 +222,35 @@ pub fn test_class(string: String, number: i32) -> TestClass { } #[php_module] -pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +pub fn build_module(module: ModuleBuilder) -> ModuleBuilder { module + .class::() + .function(wrap_function!(test_str)) + .function(wrap_function!(test_string)) + .function(wrap_function!(test_bool)) + .function(wrap_function!(test_number_signed)) + .function(wrap_function!(test_number_unsigned)) + .function(wrap_function!(test_number_float)) + .function(wrap_function!(test_array)) + .function(wrap_function!(test_array_assoc)) + .function(wrap_function!(test_binary)) + .function(wrap_function!(test_nullable)) + .function(wrap_function!(test_object)) + .function(wrap_function!(test_globals_http_get)) + .function(wrap_function!(test_globals_http_post)) + .function(wrap_function!(test_globals_http_cookie)) + .function(wrap_function!(test_globals_http_server)) + .function(wrap_function!(test_globals_http_request)) + .function(wrap_function!(test_globals_http_files)) + .function(wrap_function!(test_closure)) + .function(wrap_function!(test_closure_once)) + .function(wrap_function!(test_callable)) + .function(wrap_function!(iter_next)) + .function(wrap_function!(iter_back)) + .function(wrap_function!(iter_next_back)) + .function(wrap_function!(test_class)) + .function(wrap_function!(test_variadic_args)) + .function(wrap_function!(test_variadic_add_required)) } #[cfg(test)] @@ -279,4 +322,5 @@ mod integration { mod object; mod string; mod types; + mod variadic_args; }