From ec721012d60cb3bee095845f37a9ce9b97924cd3 Mon Sep 17 00:00:00 2001 From: Joe Hoyle Date: Tue, 5 Dec 2023 10:38:37 +0100 Subject: [PATCH] Add support for Enums This adds an EnumBuilder to create PHP enums, and a `#[php_enum]` macro which you can attach to Rust enums. Only symbolic enums are supported from Rust. PHP doesn't support enums that hold data. They do support scalar backend-enums, however Rust does not support that. So, the `#[php_enum]` can only be used for non-backed enums. You can implement backed enums via the EnumBuilder in cses where you'd want to implement your own IntoZval / FromZval for your name and do something custom with the backed value. We could potentially add attributes to the enum for their string / int scale values in PHP though. I've added cfg php8.1 to this, as enums are only supported in PHP >= 8.1. I think it's better to conintue supporting PHP 8.0 for the time being, and just have the enums only be available in >= php 8.1. --- allowed_bindings.rs | 3 + crates/macros/src/enum_.rs | 215 ++++++++++++++++++++++++++ crates/macros/src/lib.rs | 19 ++- crates/macros/src/module.rs | 41 +++++ crates/macros/src/startup_function.rs | 44 ++++++ src/builders/enum_.rs | 95 ++++++++++++ src/builders/mod.rs | 3 + src/lib.rs | 2 + src/wrapper.h | 1 + 9 files changed, 422 insertions(+), 1 deletion(-) create mode 100644 crates/macros/src/enum_.rs create mode 100644 src/builders/enum_.rs diff --git a/allowed_bindings.rs b/allowed_bindings.rs index 9d09fb00bc..782ab4addf 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -289,6 +289,9 @@ bind! { php_unregister_url_stream_wrapper, php_unregister_url_stream_wrapper_volatile, php_register_url_stream_wrapper_volatile, + zend_register_internal_enum, + zend_enum_add_case, + zend_enum_new, php_stream_wrapper, php_stream_stdio_ops, zend_atomic_bool_store, diff --git a/crates/macros/src/enum_.rs b/crates/macros/src/enum_.rs new file mode 100644 index 0000000000..49dc66475d --- /dev/null +++ b/crates/macros/src/enum_.rs @@ -0,0 +1,215 @@ +use crate::STATE; +use anyhow::{anyhow, bail, Context, Result}; +use darling::{FromMeta, ToTokens}; +use proc_macro2::{Ident, TokenStream}; +use quote::{quote, format_ident}; +use syn::parse::ParseStream; +use syn::{Attribute, AttributeArgs, Expr, LitStr, Token, ItemEnum}; + +#[derive(Debug, Default)] +pub struct Enum { + pub enum_name: String, + pub struct_path: String, + pub docs: Vec, + pub methods: Vec, + pub modifier: Option, + pub cases: Vec, + pub flags: Option, +} + +#[derive(Debug)] +pub enum ParsedAttribute { + Comment(String), +} + +#[derive(Default, Debug, FromMeta)] +#[darling(default)] +pub struct AttrArgs { + name: Option, + modifier: Option, + flags: Option, +} + +pub fn parser(args: AttributeArgs, mut input: ItemEnum) -> Result { + let args = AttrArgs::from_list(&args) + .map_err(|e| anyhow!("Unable to parse attribute arguments: {:?}", e))?; + + let mut cases = vec![]; + 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::Comment(comment) => { + comments.push(comment); + } + attr => bail!("Attribute `{:?}` is not valid for enums.", attr), + }, + None => unused.push(attr), + } + } + unused + }; + + for variant in input.variants.iter_mut() { + let mut attrs = vec![]; + attrs.append(&mut variant.attrs); + cases.push(variant.ident.to_string()); + } + + let ItemEnum { ident, .. } = &input; + let enum_name = args.name.unwrap_or_else(|| input.ident.to_string()); + let struct_path = input.ident.to_string(); + let flags = args.flags.map(|flags| flags.to_token_stream().to_string()); + let enum_ = Enum { + enum_name, + struct_path, + docs: comments, + modifier: args.modifier, + flags, + cases, + ..Default::default() + }; + + let mut state = STATE.lock(); + + if state.startup_function.is_some() { + // bail!("The `#[php_startup]` macro must be called after all the enums have been defined."); + } + + let cases = enum_.cases.clone(); + let cases = cases.iter().map(|case| { + let name = case; + let ident = format_ident!("{}", name); + quote! { #name => Ok(Self::#ident), } + }); + + + state.enums.insert(input.ident.to_string(), enum_); + + let name = stringify!(#ident); + Ok(quote! { + #input + + impl ext_php_rs::convert::FromZendObject<'_> for #ident { + fn from_zend_object(object: &ext_php_rs::types::ZendObject) -> Result { + let name = &object + .get_properties()? + .get("name") + .ok_or(ext_php_rs::error::Error::InvalidProperty)? + .indirect() + .ok_or(ext_php_rs::error::Error::InvalidProperty)? + .string() + .ok_or(ext_php_rs::error::Error::InvalidProperty)?; + + match name.as_str() { + #(#cases)* + _ => Err(ext_php_rs::error::Error::InvalidProperty), + } + } + } + + impl ext_php_rs::convert::FromZval<'_> for #ident { + const TYPE: ext_php_rs::flags::DataType = ext_php_rs::flags::DataType::Object(Some(#name)); + fn from_zval(zval: &ext_php_rs::types::Zval) -> Option { + zval.object() + .and_then(|o| Self::from_zend_object(o).ok()) + } + } + + impl ext_php_rs::convert::IntoZendObject for #ident { + fn into_zend_object(self) -> ext_php_rs::error::Result> { + let mut obj = ext_php_rs::types::ZendObject::new(#ident::get_metadata().ce()); + let name = ext_php_rs::types::ZendStr::new("name", false); + let mut zval = ext_php_rs::types::Zval::new(); + zval.set_zend_string(name); + obj.properties_table[0] = zval; + Ok(obj) + } + } + + impl ext_php_rs::convert::IntoZval for #ident { + const TYPE: ext_php_rs::flags::DataType = ext_php_rs::flags::DataType::Object(Some(#name)); + + fn set_zval(self, zv: &mut ext_php_rs::types::Zval, _persistent: bool) -> ext_php_rs::error::Result<()> { + let obj = self.into_zend_object()?; + zv.set_object(obj.into_raw()); + Ok(()) + } + } + }) +} + +#[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, + }, +} + +#[derive(Debug, Default)] +pub struct PropertyAttr { + pub rename: Option, + pub flags: Option, +} + +impl syn::parse::Parse for PropertyAttr { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut this = Self::default(); + while !input.is_empty() { + let field = input.parse::()?.to_string(); + input.parse::()?; + + match field.as_str() { + "rename" => { + this.rename.replace(input.parse::()?.value()); + } + "flags" => { + this.flags.replace(input.parse::()?); + } + _ => return Err(input.error("invalid attribute field")), + } + + let _ = input.parse::(); + } + + Ok(this) + } +} + +pub fn parse_attribute(attr: &Attribute) -> Result> { + let name = attr.path.to_token_stream().to_string(); + + Ok(match name.as_ref() { + "doc" => { + struct DocComment(pub String); + + impl syn::parse::Parse for DocComment { + fn parse(input: ParseStream) -> syn::Result { + input.parse::()?; + let comment: LitStr = input.parse()?; + Ok(Self(comment.value())) + } + } + + let comment: DocComment = + syn::parse2(attr.tokens.clone()).with_context(|| "Failed to parse doc comment")?; + Some(ParsedAttribute::Comment(comment.0)) + } + _ => None, + }) +} diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 2e6deaf8ab..c2b7bd8e5a 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -10,6 +10,8 @@ mod module; mod startup_function; mod syn_ext; mod zval; +#[cfg(php81)] +mod enum_; use std::{ collections::HashMap, @@ -21,7 +23,7 @@ use proc_macro::TokenStream; use proc_macro2::Span; use syn::{ parse_macro_input, AttributeArgs, DeriveInput, ItemConst, ItemFn, ItemForeignMod, ItemImpl, - ItemStruct, + ItemStruct, ItemEnum, }; extern crate proc_macro; @@ -31,6 +33,8 @@ struct State { functions: Vec, classes: HashMap, constants: Vec, + #[cfg(php81)] + enums: HashMap, startup_function: Option, built_module: bool, } @@ -63,6 +67,19 @@ pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream { .into() } +#[cfg(php81)] +#[proc_macro_attribute] +pub fn php_enum(args: TokenStream, input: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as AttributeArgs); + let input = parse_macro_input!(input as ItemEnum); + + match enum_::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_function(args: TokenStream, input: TokenStream) -> TokenStream { let args = parse_macro_input!(args as AttributeArgs); diff --git a/crates/macros/src/module.rs b/crates/macros/src/module.rs index f350fd472b..c68e405e93 100644 --- a/crates/macros/src/module.rs +++ b/crates/macros/src/module.rs @@ -11,6 +11,9 @@ use crate::{ startup_function, State, STATE, }; +#[cfg(php81)] +use crate::enum_::Enum; + pub fn parser(input: ItemFn) -> Result { let ItemFn { sig, block, .. } = input; let Signature { output, inputs, .. } = sig; @@ -58,10 +61,21 @@ pub fn parser(input: ItemFn) -> Result { .values() .map(generate_registered_class_impl) .collect::>>()?; + + let registered_enums_impls: Vec = vec![]; + + #[cfg(php81)] + let registered_enums_impls = state + .enums + .values() + .map(generate_registered_enum_impl) + .collect::>>()?; + let describe_fn = generate_stubs(&state); let result = quote! { #(#registered_classes_impls)* + #(#registered_enums_impls)* #startup_fn @@ -94,6 +108,33 @@ pub fn parser(input: ItemFn) -> Result { Ok(result) } +/// Generates an implementation for `RegisteredClass` on the given enum. +#[cfg(php81)] +pub fn generate_registered_enum_impl(enum_: &Enum) -> Result { + let self_ty = Ident::new(&enum_.struct_path, Span::call_site()); + let enum_name = &enum_.enum_name; + let meta = Ident::new(&format!("_{}_META", &enum_.struct_path), Span::call_site()); + + 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 = #enum_name; + const CONSTRUCTOR: ::std::option::Option< + ::ext_php_rs::class::ConstructorMeta + > = None; + + 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>> { + ::std::collections::HashMap::new() + } + } + }) +} + /// 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()); diff --git a/crates/macros/src/startup_function.rs b/crates/macros/src/startup_function.rs index 695753d6d9..4eb4904d72 100644 --- a/crates/macros/src/startup_function.rs +++ b/crates/macros/src/startup_function.rs @@ -7,6 +7,8 @@ use quote::quote; use syn::{AttributeArgs, Expr, ItemFn, Signature}; use crate::{class::Class, constant::Constant, STATE}; +#[cfg(php81)] +use crate::enum_::Enum; #[derive(Default, Debug, FromMeta)] #[darling(default)] @@ -30,6 +32,9 @@ pub fn parser(args: Option, input: ItemFn) -> Result state.startup_function = Some(ident.to_string()); let classes = build_classes(&state.classes)?; + let enums: Vec = vec![]; + #[cfg(php81)] + let enums = build_enums(&state.enums)?; let constants = build_constants(&state.constants); let (before, after) = if args.before { (Some(quote! { internal(ty, module_number); }), None) @@ -52,6 +57,7 @@ pub fn parser(args: Option, input: ItemFn) -> Result #before #(#classes)* #(#constants)* + #(#enums)* #after 0 @@ -184,6 +190,44 @@ fn build_classes(classes: &HashMap) -> Result> { .collect::>>() } +/// Returns a vector of `EnumBuilder`s for each enum. +#[cfg(php81)] +fn build_enums(enums: &HashMap) -> Result> { + enums + .iter() + .map(|(name, enum_)| { + dbg!(enum_); + let Enum { enum_name, .. } = &enum_; + let ident = Ident::new(name, Span::call_site()); + let meta = Ident::new(&format!("_{name}_META"), Span::call_site()); + let methods = enum_.methods.iter().map(|method| { + let builder = method.get_builder(&ident); + let flags = method.get_flags(); + quote! { .method(#builder.unwrap(), #flags) } + }); + let cases = enum_.cases.iter().map(|case| { + let name = &case; + quote! { .case(::ext_php_rs::builders::EnumBuilderCase { + name: #name.to_string(), + value: None, + }) } + }); + + Ok(quote! {{ + let builder: ::ext_php_rs::builders::EnumBuilder = ::ext_php_rs::builders::EnumBuilder::new(#enum_name, ::ext_php_rs::flags::DataType::Undef ) + #(#methods)* + #(#cases)* + ; + + let enum_ = builder.build() + .expect(concat!("Unable to build enum `", #enum_name, "`")); + + #meta.set_ce(enum_); + }}) + }) + .collect::>>() +} + fn build_constants(constants: &[Constant]) -> Vec { constants .iter() diff --git a/src/builders/enum_.rs b/src/builders/enum_.rs new file mode 100644 index 0000000000..99039f2d6a --- /dev/null +++ b/src/builders/enum_.rs @@ -0,0 +1,95 @@ +use std::{ffi::CString, ptr}; + +use crate::{ + convert::IntoZval, + error::{Error, Result}, + ffi::{zend_enum_add_case, zend_register_internal_enum}, + flags::{DataType, MethodFlags}, + types::ZendStr, + zend::{ClassEntry, FunctionEntry}, +}; + +/// Builder for registering an enum in PHP. +pub struct EnumBuilder { + name: String, + methods: Vec, + type_: DataType, + cases: Vec>, +} + +impl EnumBuilder { + /// Creates a new enum builder, used to build enums + /// to be exported to PHP. + /// + /// # Parameters + /// + /// * `name` - The name of the class. + pub fn new>(name: N, _type: DataType) -> Self { + Self { + name: name.into(), + methods: vec![], + type_: _type, + cases: vec![], + } + } + + /// Adds a method to the class. + /// + /// # Parameters + /// + /// * `func` - The function entry 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); + self + } + + /// Add a new case to the enum. + pub fn case(mut self, case: EnumBuilderCase) -> Self { + self.cases.push(case); + self + } + + /// Builds the enum, returning a reference to the class entry. + /// + /// # Errors + /// + /// Returns an [`Error`] variant if the class could not be registered. + pub fn build(mut self) -> Result<&'static mut ClassEntry> { + self.methods.push(FunctionEntry::end()); + let func = Box::into_raw(self.methods.into_boxed_slice()) as *const FunctionEntry; + + let class = unsafe { + zend_register_internal_enum( + CString::new(self.name.as_str())?.as_ptr(), + self.type_.as_u32() as _, + func, + ) + .as_mut() + .ok_or(Error::InvalidPointer)? + }; + + for case in self.cases { + let name = ZendStr::new(&case.name, true); + let value = match case.value { + Some(value) => { + let zval = value.into_zval(true)?; + let mut zv = core::mem::ManuallyDrop::new(zval); + core::ptr::addr_of_mut!(zv).cast() + } + None => ptr::null_mut(), + }; + unsafe { + zend_enum_add_case(class, name.into_raw(), value); + } + } + + Ok(class) + } +} + +pub struct EnumBuilderCase { + pub name: String, + pub value: Option, +} diff --git a/src/builders/mod.rs b/src/builders/mod.rs index 1a72a43a0b..e6819cbe9f 100644 --- a/src/builders/mod.rs +++ b/src/builders/mod.rs @@ -4,10 +4,13 @@ mod class; mod function; mod module; +mod enum_; #[cfg(feature = "embed")] mod sapi; pub use class::ClassBuilder; +pub use enum_::EnumBuilder; +pub use enum_::EnumBuilderCase; pub use function::FunctionBuilder; pub use module::ModuleBuilder; #[cfg(feature = "embed")] diff --git a/src/lib.rs b/src/lib.rs index ee1d68fc80..d2b7f756b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,8 @@ pub mod prelude { pub use crate::exception::{PhpException, PhpResult}; pub use crate::php_class; pub use crate::php_const; + #[cfg(php81)] + pub use crate::php_enum; pub use crate::php_extern; pub use crate::php_function; pub use crate::php_impl; diff --git a/src/wrapper.h b/src/wrapper.h index 24326a556a..e7cc6a1c67 100644 --- a/src/wrapper.h +++ b/src/wrapper.h @@ -22,6 +22,7 @@ #include "zend_exceptions.h" #include "zend_inheritance.h" #include "zend_interfaces.h" +#include "Zend/zend_enum.h" #include "php_variables.h" #include "zend_ini.h" #include "main/SAPI.h"