Skip to content

Commit

Permalink
Add support for Enums
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
joehoyle committed Dec 5, 2023
1 parent a002e98 commit ec72101
Show file tree
Hide file tree
Showing 9 changed files with 422 additions and 1 deletion.
3 changes: 3 additions & 0 deletions allowed_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
215 changes: 215 additions & 0 deletions crates/macros/src/enum_.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub methods: Vec<crate::method::Method>,
pub modifier: Option<String>,
pub cases: Vec<String>,
pub flags: Option<String>,
}

#[derive(Debug)]
pub enum ParsedAttribute {
Comment(String),
}

#[derive(Default, Debug, FromMeta)]
#[darling(default)]
pub struct AttrArgs {
name: Option<String>,
modifier: Option<String>,
flags: Option<Expr>,
}

pub fn parser(args: AttributeArgs, mut input: ItemEnum) -> Result<TokenStream> {
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<Self, ext_php_rs::error::Error> {
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<Self> {
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<ext_php_rs::boxed::ZBox<ext_php_rs::types::ZendObject>> {
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<String>,
#[allow(dead_code)]
pub flags: Option<String>,
}

#[derive(Debug)]
pub enum PropertyType {
Field {
field_name: String,
},
Method {
getter: Option<String>,
setter: Option<String>,
},
}

#[derive(Debug, Default)]
pub struct PropertyAttr {
pub rename: Option<String>,
pub flags: Option<Expr>,
}

impl syn::parse::Parse for PropertyAttr {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut this = Self::default();
while !input.is_empty() {
let field = input.parse::<Ident>()?.to_string();
input.parse::<Token![=]>()?;

match field.as_str() {
"rename" => {
this.rename.replace(input.parse::<LitStr>()?.value());
}
"flags" => {
this.flags.replace(input.parse::<Expr>()?);
}
_ => return Err(input.error("invalid attribute field")),
}

let _ = input.parse::<Token![,]>();
}

Ok(this)
}
}

pub fn parse_attribute(attr: &Attribute) -> Result<Option<ParsedAttribute>> {
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<Self> {
input.parse::<Token![=]>()?;
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,
})
}
19 changes: 18 additions & 1 deletion crates/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ mod module;
mod startup_function;
mod syn_ext;
mod zval;
#[cfg(php81)]
mod enum_;

use std::{
collections::HashMap,
Expand All @@ -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;
Expand All @@ -31,6 +33,8 @@ struct State {
functions: Vec<function::Function>,
classes: HashMap<String, class::Class>,
constants: Vec<Constant>,
#[cfg(php81)]
enums: HashMap<String, enum_::Enum>,
startup_function: Option<String>,
built_module: bool,
}
Expand Down Expand Up @@ -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);
Expand Down
41 changes: 41 additions & 0 deletions crates/macros/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ use crate::{
startup_function, State, STATE,
};

#[cfg(php81)]
use crate::enum_::Enum;

pub fn parser(input: ItemFn) -> Result<TokenStream> {
let ItemFn { sig, block, .. } = input;
let Signature { output, inputs, .. } = sig;
Expand Down Expand Up @@ -58,10 +61,21 @@ pub fn parser(input: ItemFn) -> Result<TokenStream> {
.values()
.map(generate_registered_class_impl)
.collect::<Result<Vec<_>>>()?;

let registered_enums_impls: Vec<TokenStream> = vec![];

#[cfg(php81)]
let registered_enums_impls = state
.enums
.values()
.map(generate_registered_enum_impl)
.collect::<Result<Vec<_>>>()?;

let describe_fn = generate_stubs(&state);

let result = quote! {
#(#registered_classes_impls)*
#(#registered_enums_impls)*

#startup_fn

Expand Down Expand Up @@ -94,6 +108,33 @@ pub fn parser(input: ItemFn) -> Result<TokenStream> {
Ok(result)
}

/// Generates an implementation for `RegisteredClass` on the given enum.
#[cfg(php81)]
pub fn generate_registered_enum_impl(enum_: &Enum) -> Result<TokenStream> {
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<Self>
> = None;

fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata<Self> {
&#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<TokenStream> {
let self_ty = Ident::new(&class.struct_path, Span::call_site());
Expand Down
Loading

0 comments on commit ec72101

Please sign in to comment.