From 79021d2dafafa3c0c964979c63c6cf6b8d488d0a Mon Sep 17 00:00:00 2001 From: Xenira <1288524+Xenira@users.noreply.github.com> Date: Sun, 6 Jul 2025 13:11:39 +0200 Subject: [PATCH 01/10] feat(enum): add basic enum support Fixes: #178 Refs: #302 --- Cargo.toml | 2 + allowed_bindings.rs | 4 + build.rs | 13 +- crates/macros/Cargo.toml | 3 +- crates/macros/src/enum_.rs | 278 +++++++++++++++++++++++++++ crates/macros/src/lib.rs | 56 +++++- docsrs_bindings.rs | 23 +++ guide/src/macros/enum.md | 40 ++++ src/builders/enum_builder.rs | 89 +++++++++ src/builders/mod.rs | 2 + src/builders/module.rs | 38 ++++ src/enum_.rs | 55 ++++++ src/flags.rs | 23 ++- src/lib.rs | 6 + src/wrapper.h | 3 + tests/Cargo.toml | 6 +- tests/src/integration/enum_/enum.php | 11 ++ tests/src/integration/enum_/mod.rs | 38 ++++ tests/src/integration/mod.rs | 11 +- tests/src/lib.rs | 4 + tools/update_lib_docs.sh | 1 + 21 files changed, 688 insertions(+), 18 deletions(-) create mode 100644 crates/macros/src/enum_.rs create mode 100644 guide/src/macros/enum.md create mode 100644 src/builders/enum_builder.rs create mode 100644 src/enum_.rs create mode 100644 tests/src/integration/enum_/enum.php create mode 100644 tests/src/integration/enum_/mod.rs diff --git a/Cargo.toml b/Cargo.toml index bb8656886b..f480a6f2d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,9 +38,11 @@ native-tls = "0.2" zip = "4.0" [features] +default = ["enum"] closure = [] embed = [] anyhow = ["dep:anyhow"] +enum = [] [workspace] members = [ diff --git a/allowed_bindings.rs b/allowed_bindings.rs index ecf2800f49..b7a48f8c68 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -87,6 +87,8 @@ bind! { zend_declare_class_constant, zend_declare_property, zend_do_implement_interface, + zend_enum_add_case, + zend_enum_new, zend_execute_data, zend_function_entry, zend_hash_clean, @@ -114,6 +116,7 @@ bind! { zend_register_bool_constant, zend_register_double_constant, zend_register_ini_entries, + zend_register_internal_enum, zend_ini_entry_def, zend_register_internal_class_ex, zend_register_long_constant, @@ -191,6 +194,7 @@ bind! { ZEND_ACC_DEPRECATED, ZEND_ACC_DONE_PASS_TWO, ZEND_ACC_EARLY_BINDING, + ZEND_ACC_ENUM, ZEND_ACC_FAKE_CLOSURE, ZEND_ACC_FINAL, ZEND_ACC_GENERATOR, diff --git a/build.rs b/build.rs index e144eda655..9df8dae7d0 100644 --- a/build.rs +++ b/build.rs @@ -261,7 +261,7 @@ fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result Self { - ApiVersion::Php80 + pub fn min() -> Self { + [ + ApiVersion::Php80, + #[cfg(feature = "enum")] + ApiVersion::Php81, + ] + .into_iter() + .max() + .unwrap_or(Self::max()) } /// Returns the maximum API version supported by ext-php-rs. diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index 276830c3c0..0ad8803691 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -6,7 +6,7 @@ homepage = "https://github.com/davidcole1340/ext-php-rs" license = "MIT OR Apache-2.0" version = "0.11.2" authors = ["David Cole "] -edition = "2018" +edition = "2021" [lib] proc-macro = true @@ -19,6 +19,7 @@ proc-macro2 = "1.0.26" lazy_static = "1.4.0" anyhow = "1.0" convert_case = "0.8.0" +itertools = "0.14.0" [lints.rust] missing_docs = "warn" diff --git a/crates/macros/src/enum_.rs b/crates/macros/src/enum_.rs new file mode 100644 index 0000000000..343f6ec6d6 --- /dev/null +++ b/crates/macros/src/enum_.rs @@ -0,0 +1,278 @@ +use std::convert::TryFrom; + +use darling::FromAttributes; +use itertools::Itertools; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{Expr, Fields, Ident, ItemEnum, Lit}; + +use crate::{ + helpers::get_docs, + parsing::{PhpRename, RenameRule, Visibility}, + prelude::*, +}; + +#[derive(FromAttributes, Default, Debug)] +#[darling(default, attributes(php), forward_attrs(doc))] +struct PhpEnumAttribute { + #[darling(flatten)] + rename: PhpRename, + #[darling(default)] + allow_discriminants: bool, + rename_cases: Option, + vis: Option, + attrs: Vec, +} + +#[derive(FromAttributes, Default, Debug)] +#[darling(default, attributes(php), forward_attrs(doc))] +struct PhpEnumVariantAttribute { + #[darling(flatten)] + rename: PhpRename, + discriminant: Option, + // TODO: Implement doc support for enum variants + #[allow(dead_code)] + attrs: Vec, +} + +pub fn parser(mut input: ItemEnum) -> Result { + let php_attr = PhpEnumAttribute::from_attributes(&input.attrs)?; + input.attrs.retain(|attr| !attr.path().is_ident("php")); + + let docs = get_docs(&php_attr.attrs)?; + let mut cases = vec![]; + let mut discriminant_type = DiscriminantType::None; + + for variant in &mut input.variants { + if variant.fields != Fields::Unit { + bail!("Enum cases must be unit variants, found: {:?}", variant); + } + if !php_attr.allow_discriminants && variant.discriminant.is_some() { + bail!(variant => "Native discriminants are currently not exported to PHP. To set a discriminant, use the `#[php(allow_discriminants)]` attribute on the enum. To export discriminants, set the #[php(discriminant = ...)] attribute on the enum case."); + } + + let variant_attr = PhpEnumVariantAttribute::from_attributes(&variant.attrs)?; + variant.attrs.retain(|attr| !attr.path().is_ident("php")); + let docs = get_docs(&variant_attr.attrs)?; + let discriminant = variant_attr + .discriminant + .as_ref() + .map(TryInto::try_into) + .transpose()?; + + if let Some(d) = &discriminant { + match d { + Discriminant::String(_) => { + if discriminant_type == DiscriminantType::Integer { + bail!(variant => "Mixed discriminants are not allowed in enums, found string and integer discriminants"); + } + + discriminant_type = DiscriminantType::String; + } + Discriminant::Integer(_) => { + if discriminant_type == DiscriminantType::String { + bail!(variant => "Mixed discriminants are not allowed in enums, found string and integer discriminants"); + } + + discriminant_type = DiscriminantType::Integer; + } + } + } else if discriminant_type != DiscriminantType::None { + bail!(variant => "Discriminant must be specified for all enum cases, found: {:?}", variant); + } + + cases.push(EnumCase { + ident: variant.ident.clone(), + name: variant_attr.rename.rename( + variant.ident.to_string(), + php_attr.rename_cases.unwrap_or(RenameRule::Pascal), + ), + attrs: variant_attr, + discriminant, + docs, + }); + + if !cases + .iter() + .filter_map(|case| case.discriminant.as_ref()) + .all_unique() + { + bail!(variant => "Enum cases must have unique discriminants, found duplicates in: {:?}", cases); + } + } + + let enum_props = Enum { + ident: &input.ident, + attrs: php_attr, + docs, + cases, + flags: None, // TODO: Implement flags support + }; + + Ok(quote! { + #[allow(dead_code)] + #input + + #enum_props + }) +} + +#[derive(Debug)] +pub struct Enum<'a> { + ident: &'a Ident, + attrs: PhpEnumAttribute, + docs: Vec, + cases: Vec, + // TODO: Implement flags support + #[allow(dead_code)] + flags: Option, +} + +impl ToTokens for Enum<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let ident = &self.ident; + let enum_name = self + .attrs + .rename + .rename(ident.to_string(), RenameRule::Pascal); + let flags = quote! { ::ext_php_rs::flags::ClassFlags::Enum }; + let docs = &self.docs; + let cases = &self.cases; + + let class = quote! { + impl ::ext_php_rs::class::RegisteredClass for #ident { + const CLASS_NAME: &'static str = #enum_name; + const BUILDER_MODIFIER: ::std::option::Option< + fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder + > = None; + const EXTENDS: ::std::option::Option< + ::ext_php_rs::class::ClassEntryInfo + > = None; + const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[]; + const FLAGS: ::ext_php_rs::flags::ClassFlags = #flags; + const DOC_COMMENTS: &'static [&'static str] = &[ + #(#docs,)* + ]; + + fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { + static METADATA: ::ext_php_rs::class::ClassMetadata<#ident> = + ::ext_php_rs::class::ClassMetadata::new(); + &METADATA + } + + #[inline] + fn get_properties<'a>() -> ::std::collections::HashMap< + &'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self> + > { + ::std::collections::HashMap::new() + } + + #[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> { + None + } + + #[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() + } + } + }; + let enum_impl = quote! { + impl ::ext_php_rs::enum_::PhpEnum for #ident { + const CASES: &'static [::ext_php_rs::enum_::EnumCase] = &[ + #(#cases,)* + ]; + } + }; + + tokens.extend(quote! { + #class + #enum_impl + }); + } +} + +#[derive(Debug)] +struct EnumCase { + #[allow(dead_code)] + ident: Ident, + name: String, + #[allow(dead_code)] + attrs: PhpEnumVariantAttribute, + discriminant: Option, + docs: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum Discriminant { + String(String), + Integer(i64), +} + +impl TryFrom<&Expr> for Discriminant { + type Error = syn::Error; + + fn try_from(expr: &Expr) -> Result { + match expr { + Expr::Lit(expr) => match &expr.lit { + Lit::Str(s) => Ok(Discriminant::String(s.value())), + Lit::Int(i) => i.base10_parse::().map(Discriminant::Integer).map_err( + |_| err!(expr => "Invalid integer literal for enum case: {:?}", expr.lit), + ), + _ => bail!(expr => "Unsupported discriminant type: {:?}", expr.lit), + }, + _ => { + bail!(expr => "Unsupported discriminant type, expected a literal of type string or i64, found: {:?}", expr); + } + } + } +} + +impl ToTokens for Discriminant { + fn to_tokens(&self, tokens: &mut TokenStream) { + tokens.extend(match self { + Discriminant::String(s) => { + quote! { ::ext_php_rs::enum_::Discriminant::String(#s.to_string()) } + } + Discriminant::Integer(i) => { + quote! { ::ext_php_rs::enum_::Discriminant::Int(#i) } + } + }); + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum DiscriminantType { + None, + String, + Integer, +} + +impl ToTokens for EnumCase { + fn to_tokens(&self, tokens: &mut TokenStream) { + let ident = &self.name; + let discriminant = self + .discriminant + .as_ref() + .map_or_else(|| quote! { None }, |v| quote! { Some(#v) }); + let docs = &self.docs; + + tokens.extend(quote! { + ::ext_php_rs::enum_::EnumCase { + name: #ident, + discriminant: #discriminant, + docs: &[#(#docs,)*], + } + }); + } +} diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index d058018916..f4ad112c19 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -1,6 +1,7 @@ //! Macros for the `php-ext` crate. mod class; mod constant; +mod enum_; mod extern_; mod fastcall; mod function; @@ -13,7 +14,7 @@ mod zval; use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; -use syn::{DeriveInput, ItemConst, ItemFn, ItemForeignMod, ItemImpl, ItemStruct}; +use syn::{DeriveInput, ItemConst, ItemEnum, ItemFn, ItemForeignMod, ItemImpl, ItemStruct}; extern crate proc_macro; @@ -215,6 +216,59 @@ fn php_class_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 { class::parser(input).unwrap_or_else(|e| e.to_compile_error()) } +// BEGIN DOCS FROM enum.md +/// # `#[php_enum]` Attribute +/// +/// Enums can be exported to PHP as enums with the `#[php_enum]` attribute +/// macro. This attribute derives the `RegisteredClass` and `PhpEnum` traits on +/// your enum. To register the enum use the `r#enum::()` method on the +/// `ModuleBuilder` in the `#[php_module]` macro. +/// +/// ## Options +/// +/// tbd +/// +/// ## Restrictions +/// +/// tbd +/// +/// ## Example +/// +/// This example creates a PHP enum `Suit`. +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// +/// #[php_enum] +/// pub enum Suit { +/// Hearts, +/// Diamonds, +/// Clubs, +/// Spades, +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module.r#enum::() +/// } +/// # fn main() {} +/// ``` +/// +/// TODO: Add backed enums example +// END DOCS FROM enum.md +#[proc_macro_attribute] +pub fn php_enum(args: TokenStream, input: TokenStream) -> TokenStream { + php_enum_internal(args.into(), input.into()).into() +} + +fn php_enum_internal(_args: TokenStream2, input: TokenStream2) -> TokenStream2 { + let input = parse_macro_input2!(input as ItemEnum); + + enum_::parser(input).unwrap_or_else(|e| e.to_compile_error()) +} + // BEGIN DOCS FROM function.md /// # `#[php_function]` Attribute /// diff --git a/docsrs_bindings.rs b/docsrs_bindings.rs index 900390cd7e..ef5e3b2073 100644 --- a/docsrs_bindings.rs +++ b/docsrs_bindings.rs @@ -146,6 +146,7 @@ pub const ZEND_ACC_PROMOTED: u32 = 256; pub const ZEND_ACC_INTERFACE: u32 = 1; pub const ZEND_ACC_TRAIT: u32 = 2; pub const ZEND_ACC_ANON_CLASS: u32 = 4; +pub const ZEND_ACC_ENUM: u32 = 268435456; pub const ZEND_ACC_LINKED: u32 = 8; pub const ZEND_ACC_IMPLICIT_ABSTRACT_CLASS: u32 = 16; pub const ZEND_ACC_USE_GUARDS: u32 = 2048; @@ -2700,6 +2701,28 @@ pub struct php_file_globals { extern "C" { pub static mut file_globals: php_file_globals; } +extern "C" { + pub fn zend_enum_new( + result: *mut zval, + ce: *mut zend_class_entry, + case_name: *mut zend_string, + backing_value_zv: *mut zval, + ) -> *mut zend_object; +} +extern "C" { + pub fn zend_register_internal_enum( + name: *const ::std::os::raw::c_char, + type_: u8, + functions: *const zend_function_entry, + ) -> *mut zend_class_entry; +} +extern "C" { + pub fn zend_enum_add_case( + ce: *mut zend_class_entry, + case_name: *mut zend_string, + value: *mut zval, + ); +} extern "C" { pub static mut zend_ce_throwable: *mut zend_class_entry; } diff --git a/guide/src/macros/enum.md b/guide/src/macros/enum.md new file mode 100644 index 0000000000..3c22a86179 --- /dev/null +++ b/guide/src/macros/enum.md @@ -0,0 +1,40 @@ +# `#[php_enum]` Attribute + +Enums can be exported to PHP as enums with the `#[php_enum]` attribute macro. +This attribute derives the `RegisteredClass` and `PhpEnum` traits on your enum. +To register the enum use the `r#enum::()` method on the `ModuleBuilder` +in the `#[php_module]` macro. + +## Options + +tbd + +## Restrictions + +tbd + +## Example + +This example creates a PHP enum `Suit`. + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::prelude::*; + +#[php_enum] +pub enum Suit { + Hearts, + Diamonds, + Clubs, + Spades, +} + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.r#enum::() +} +# fn main() {} +``` + +TODO: Add backed enums example diff --git a/src/builders/enum_builder.rs b/src/builders/enum_builder.rs new file mode 100644 index 0000000000..ad9b506725 --- /dev/null +++ b/src/builders/enum_builder.rs @@ -0,0 +1,89 @@ +use std::{ffi::CString, ptr}; + +use crate::{ + builders::FunctionBuilder, + enum_::EnumCase, + error::Result, + ffi::{zend_enum_add_case, zend_register_internal_enum}, + flags::{DataType, MethodFlags}, + types::ZendStr, + zend::FunctionEntry, +}; + +#[must_use] +pub struct EnumBuilder { + pub(crate) name: String, + pub(crate) methods: Vec<(FunctionBuilder<'static>, MethodFlags)>, + pub(crate) cases: Vec<&'static EnumCase>, + pub(crate) datatype: DataType, +} + +impl EnumBuilder { + pub fn new>(name: T) -> Self { + Self { + name: name.into(), + methods: Vec::default(), + cases: Vec::default(), + datatype: DataType::Undef, + } + } + + pub fn case(mut self, case: &'static EnumCase) -> Self { + let data_type = case.data_type(); + assert!( + data_type == self.datatype || self.cases.is_empty(), + "Cannot add case with data type {:?} to enum with data type {:?}", + data_type, + self.datatype + ); + + self.datatype = data_type; + self.cases.push(case); + + self + } + + pub fn add_method(mut self, method: FunctionBuilder<'static>, flags: MethodFlags) -> Self { + self.methods.push((method, flags)); + self + } + + pub fn register(self) -> Result<()> { + 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 class = unsafe { + zend_register_internal_enum( + CString::new(self.name)?.as_ptr(), + self.datatype.as_u32().try_into()?, + methods.into_boxed_slice().as_ptr(), + ) + }; + + for case in self.cases { + let name = ZendStr::new(case.name, true); + let value = match &case.discriminant { + Some(value) => { + let value = value.into_zval(false)?; + let mut zv = core::mem::ManuallyDrop::new(value); + (&raw mut zv).cast() + } + None => ptr::null_mut(), + }; + unsafe { + zend_enum_add_case(class, name.into_raw(), value); + } + } + + Ok(()) + } +} diff --git a/src/builders/mod.rs b/src/builders/mod.rs index bb984e86bd..e4880dbebe 100644 --- a/src/builders/mod.rs +++ b/src/builders/mod.rs @@ -2,6 +2,8 @@ //! Generally zero-cost abstractions. mod class; +#[cfg(feature = "enum")] +mod enum_builder; mod function; #[cfg(all(php82, feature = "embed"))] mod ini; diff --git a/src/builders/module.rs b/src/builders/module.rs index 4e5b2d8ee9..8196af255c 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -1,6 +1,8 @@ use std::{convert::TryFrom, ffi::CString, mem, ptr}; use super::{ClassBuilder, FunctionBuilder}; +#[cfg(feature = "enum")] +use crate::{builders::enum_builder::EnumBuilder, enum_::PhpEnum}; use crate::{ class::RegisteredClass, constant::IntoConst, @@ -46,6 +48,8 @@ pub struct ModuleBuilder<'a> { pub(crate) functions: Vec>, pub(crate) constants: Vec<(String, Box, DocComments)>, pub(crate) classes: Vec ClassBuilder>, + #[cfg(feature = "enum")] + pub(crate) enums: Vec EnumBuilder>, startup_func: Option, shutdown_func: Option, request_startup_func: Option, @@ -206,6 +210,27 @@ impl ModuleBuilder<'_> { }); self } + + /// Adds an enum to the extension. + #[cfg(feature = "enum")] + pub fn r#enum(mut self) -> Self + where + T: RegisteredClass + PhpEnum, + { + self.enums.push(|| { + let mut builder = EnumBuilder::new(T::CLASS_NAME); + for case in T::CASES { + builder = builder.case(case); + } + for (method, flags) in T::method_builders() { + builder = builder.add_method(method, flags); + } + + builder + }); + + self + } } /// Artifacts from the [`ModuleBuilder`] that should be revisited inside the @@ -213,6 +238,8 @@ impl ModuleBuilder<'_> { pub struct ModuleStartup { constants: Vec<(String, Box)>, classes: Vec ClassBuilder>, + #[cfg(feature = "enum")] + enums: Vec EnumBuilder>, } impl ModuleStartup { @@ -234,6 +261,15 @@ impl ModuleStartup { self.classes.into_iter().map(|c| c()).for_each(|c| { c.register().expect("Failed to build class"); }); + + #[cfg(feature = "enum")] + self.enums + .into_iter() + .map(|builder| builder()) + .for_each(|e| { + e.register().expect("Failed to build enum"); + }); + Ok(()) } } @@ -268,6 +304,8 @@ impl TryFrom> for (ModuleEntry, ModuleStartup) { .map(|(n, v, _)| (n, v)) .collect(), classes: builder.classes, + #[cfg(feature = "enum")] + enums: builder.enums, }; Ok(( diff --git a/src/enum_.rs b/src/enum_.rs new file mode 100644 index 0000000000..760054616a --- /dev/null +++ b/src/enum_.rs @@ -0,0 +1,55 @@ +//! This module defines the `PhpEnum` trait and related types for Rust enums that are exported to PHP. +use crate::{ + convert::IntoZval, describe::DocComments, error::Result, flags::DataType, types::Zval, +}; + +/// Implemented on Rust enums which are exported to PHP. +pub trait PhpEnum { + /// The cases of the enum. + const CASES: &'static [EnumCase]; +} + +/// Represents a case in a PHP enum. +pub struct EnumCase { + /// The identifier of the enum case, e.g. `Bar` in `enum Foo { Bar }`. + pub name: &'static str, + /// The value of the enum case, which can be an integer or a string. + pub discriminant: Option, + /// The documentation comments for the enum case. + pub docs: DocComments, +} + +impl EnumCase { + /// Gets the PHP data type of the enum case's discriminant. + #[must_use] + pub fn data_type(&self) -> DataType { + match self.discriminant { + Some(Discriminant::Int(_)) => DataType::Long, + Some(Discriminant::String(_)) => DataType::String, + None => DataType::Undef, + } + } +} + +/// Represents the discriminant of an enum case in PHP, which can be either an integer or a string. +pub enum Discriminant { + /// An integer discriminant. + Int(i64), + /// A string discriminant. + String(&'static str), +} + +impl Discriminant { + /// Converts the discriminant to a PHP value. + /// + /// # Errors + /// + /// Returns an error if the conversion fails. See [`String`] and [`i64`] [`IntoZval`] implementations + /// for more details on potential errors. + pub fn into_zval(&self, persistent: bool) -> Result { + match self { + Discriminant::Int(i) => i.into_zval(persistent), + Discriminant::String(s) => s.into_zval(persistent), + } + } +} diff --git a/src/flags.rs b/src/flags.rs index f74d2bed6b..139722c491 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -14,16 +14,16 @@ use crate::ffi::{ PHP_INI_PERDIR, PHP_INI_SYSTEM, PHP_INI_USER, ZEND_ACC_ABSTRACT, ZEND_ACC_ANON_CLASS, ZEND_ACC_CALL_VIA_TRAMPOLINE, ZEND_ACC_CHANGED, ZEND_ACC_CLOSURE, ZEND_ACC_CONSTANTS_UPDATED, ZEND_ACC_CTOR, ZEND_ACC_DEPRECATED, ZEND_ACC_DONE_PASS_TWO, ZEND_ACC_EARLY_BINDING, - ZEND_ACC_FAKE_CLOSURE, ZEND_ACC_FINAL, ZEND_ACC_GENERATOR, ZEND_ACC_HAS_FINALLY_BLOCK, - ZEND_ACC_HAS_RETURN_TYPE, ZEND_ACC_HAS_TYPE_HINTS, ZEND_ACC_HEAP_RT_CACHE, ZEND_ACC_IMMUTABLE, - ZEND_ACC_IMPLICIT_ABSTRACT_CLASS, ZEND_ACC_INTERFACE, ZEND_ACC_LINKED, ZEND_ACC_NEARLY_LINKED, - ZEND_ACC_NEVER_CACHE, ZEND_ACC_NO_DYNAMIC_PROPERTIES, ZEND_ACC_PRELOADED, ZEND_ACC_PRIVATE, - ZEND_ACC_PROMOTED, ZEND_ACC_PROTECTED, ZEND_ACC_PUBLIC, ZEND_ACC_RESOLVED_INTERFACES, - ZEND_ACC_RESOLVED_PARENT, ZEND_ACC_RETURN_REFERENCE, ZEND_ACC_STATIC, ZEND_ACC_STRICT_TYPES, - ZEND_ACC_TOP_LEVEL, ZEND_ACC_TRAIT, ZEND_ACC_TRAIT_CLONE, ZEND_ACC_UNRESOLVED_VARIANCE, - ZEND_ACC_USES_THIS, ZEND_ACC_USE_GUARDS, ZEND_ACC_VARIADIC, ZEND_EVAL_CODE, - ZEND_HAS_STATIC_IN_METHODS, ZEND_INTERNAL_FUNCTION, ZEND_USER_FUNCTION, Z_TYPE_FLAGS_SHIFT, - _IS_BOOL, + ZEND_ACC_ENUM, ZEND_ACC_FAKE_CLOSURE, ZEND_ACC_FINAL, ZEND_ACC_GENERATOR, + ZEND_ACC_HAS_FINALLY_BLOCK, ZEND_ACC_HAS_RETURN_TYPE, ZEND_ACC_HAS_TYPE_HINTS, + ZEND_ACC_HEAP_RT_CACHE, ZEND_ACC_IMMUTABLE, ZEND_ACC_IMPLICIT_ABSTRACT_CLASS, + ZEND_ACC_INTERFACE, ZEND_ACC_LINKED, ZEND_ACC_NEARLY_LINKED, ZEND_ACC_NEVER_CACHE, + ZEND_ACC_NO_DYNAMIC_PROPERTIES, ZEND_ACC_PRELOADED, ZEND_ACC_PRIVATE, ZEND_ACC_PROMOTED, + ZEND_ACC_PROTECTED, ZEND_ACC_PUBLIC, ZEND_ACC_RESOLVED_INTERFACES, ZEND_ACC_RESOLVED_PARENT, + ZEND_ACC_RETURN_REFERENCE, ZEND_ACC_STATIC, ZEND_ACC_STRICT_TYPES, ZEND_ACC_TOP_LEVEL, + ZEND_ACC_TRAIT, ZEND_ACC_TRAIT_CLONE, ZEND_ACC_UNRESOLVED_VARIANCE, ZEND_ACC_USES_THIS, + ZEND_ACC_USE_GUARDS, ZEND_ACC_VARIADIC, ZEND_EVAL_CODE, ZEND_HAS_STATIC_IN_METHODS, + ZEND_INTERNAL_FUNCTION, ZEND_USER_FUNCTION, Z_TYPE_FLAGS_SHIFT, _IS_BOOL, }; use std::{convert::TryFrom, fmt::Display}; @@ -113,6 +113,9 @@ bitflags! { const Trait = ZEND_ACC_TRAIT; /// Anonymous class const AnonymousClass = ZEND_ACC_ANON_CLASS; + /// Class is an Enum + #[cfg(php81)] + const Enum = ZEND_ACC_ENUM; /// Class linked with parent, interfaces and traits const Linked = ZEND_ACC_LINKED; /// Class is abstract, since it is set by any abstract method diff --git a/src/lib.rs b/src/lib.rs index 47e33405af..4b51f64137 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,8 @@ pub mod constant; pub mod describe; #[cfg(feature = "embed")] pub mod embed; +#[cfg(feature = "enum")] +pub mod enum_; #[doc(hidden)] pub mod internal; pub mod props; @@ -47,6 +49,8 @@ pub mod prelude { #[cfg_attr(docs, doc(cfg(feature = "closure")))] pub use crate::closure::Closure; pub use crate::exception::{PhpException, PhpResult}; + #[cfg(feature = "enum")] + pub use crate::php_enum; pub use crate::php_print; pub use crate::php_println; pub use crate::types::ZendCallable; @@ -65,6 +69,8 @@ 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); +#[cfg(feature = "enum")] +pub use ext_php_rs_derive::php_enum; 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/wrapper.h b/src/wrapper.h index 24326a556a..5ae9098c89 100644 --- a/src/wrapper.h +++ b/src/wrapper.h @@ -19,6 +19,9 @@ #include "ext/standard/info.h" #include "ext/standard/php_var.h" #include "ext/standard/file.h" +#ifdef EXT_PHP_RS_PHP_81 +#include "zend_enum.h" +#endif #include "zend_exceptions.h" #include "zend_inheritance.h" #include "zend_interfaces.h" diff --git a/tests/Cargo.toml b/tests/Cargo.toml index a63fe5a250..9f79f427ed 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -6,7 +6,11 @@ publish = false license = "MIT OR Apache-2.0" [dependencies] -ext-php-rs = { path = "../", features = ["closure"] } +cfg-if = "1.0.1" +ext-php-rs = { path = "../", default-features = false, features = ["closure"] } + +[features] +enum = ["ext-php-rs/enum"] [lib] crate-type = ["cdylib"] diff --git a/tests/src/integration/enum_/enum.php b/tests/src/integration/enum_/enum.php new file mode 100644 index 0000000000..ae12df726c --- /dev/null +++ b/tests/src/integration/enum_/enum.php @@ -0,0 +1,11 @@ +value === 1); diff --git a/tests/src/integration/enum_/mod.rs b/tests/src/integration/enum_/mod.rs new file mode 100644 index 0000000000..ea384bd4a0 --- /dev/null +++ b/tests/src/integration/enum_/mod.rs @@ -0,0 +1,38 @@ +use ext_php_rs::{php_enum, prelude::ModuleBuilder}; + +#[php_enum] +#[php(allow_discriminants)] +pub enum TestEnum { + // #[php(discriminant = 2)] + Variant1, + // #[php(discriminant = 1)] + Variant2 = 1, +} + +#[php_enum] +pub enum IntBackedEnum { + #[php(discriminant = 1)] + Variant1, + #[php(discriminant = 2)] + Variant2, +} + +// #[php_enum] +// pub enum StringBackedEnum { +// #[php(discriminant = "foo")] +// Variant1, +// #[php(discriminant = "bar")] +// Variant2, +// } + +pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { + builder.r#enum::().r#enum::() +} + +#[cfg(test)] +mod tests { + #[test] + fn enum_works() { + assert!(crate::integration::test::run_php("enum_/enum.php")); + } +} diff --git a/tests/src/integration/mod.rs b/tests/src/integration/mod.rs index 647bbfc5c9..04bdc282e2 100644 --- a/tests/src/integration/mod.rs +++ b/tests/src/integration/mod.rs @@ -5,6 +5,8 @@ pub mod callable; pub mod class; pub mod closure; pub mod defaults; +#[cfg(feature = "enum")] +pub mod enum_; pub mod exception; pub mod globals; pub mod iterator; @@ -28,8 +30,13 @@ mod test { fn setup() { BUILD.call_once(|| { - assert!(Command::new("cargo") - .arg("build") + let mut command = Command::new("cargo"); + command.arg("build"); + #[cfg(feature = "enum")] + { + command.arg("--features=enum"); + } + assert!(command .output() .expect("failed to build extension") .status diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 0ea8185921..ced895ded5 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -18,6 +18,10 @@ pub fn build_module(module: ModuleBuilder) -> ModuleBuilder { module = integration::class::build_module(module); module = integration::closure::build_module(module); module = integration::defaults::build_module(module); + #[cfg(feature = "enum")] + { + module = integration::enum_::build_module(module); + } module = integration::exception::build_module(module); module = integration::globals::build_module(module); module = integration::iterator::build_module(module); diff --git a/tools/update_lib_docs.sh b/tools/update_lib_docs.sh index d45d0e6181..b8e794dacb 100755 --- a/tools/update_lib_docs.sh +++ b/tools/update_lib_docs.sh @@ -50,6 +50,7 @@ update_docs "function" update_docs "impl" update_docs "module" update_docs "zval_convert" +update_docs "enum" # Format to remove trailing whitespace rustup run nightly rustfmt crates/macros/src/lib.rs From a4c1d2aeb5086cb7ab22649eb49ddf282edc32f9 Mon Sep 17 00:00:00 2001 From: Xenira <1288524+Xenira@users.noreply.github.com> Date: Tue, 8 Jul 2025 19:48:25 +0200 Subject: [PATCH 02/10] feat(enum): add support for string backed enums Refs: #178 --- crates/macros/src/enum_.rs | 36 +++++++++++++--------------- src/builders/enum_builder.rs | 4 ++-- src/enum_.rs | 24 +++++++++---------- tests/src/integration/enum_/enum.php | 12 ++++++++-- tests/src/integration/enum_/mod.rs | 21 +++++++++------- 5 files changed, 52 insertions(+), 45 deletions(-) diff --git a/crates/macros/src/enum_.rs b/crates/macros/src/enum_.rs index 343f6ec6d6..d7e8603bfd 100644 --- a/crates/macros/src/enum_.rs +++ b/crates/macros/src/enum_.rs @@ -1,10 +1,10 @@ use std::convert::TryFrom; -use darling::FromAttributes; +use darling::{util::Flag, FromAttributes}; use itertools::Itertools; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; -use syn::{Expr, Fields, Ident, ItemEnum, Lit}; +use syn::{Fields, Ident, ItemEnum, Lit}; use crate::{ helpers::get_docs, @@ -18,7 +18,7 @@ struct PhpEnumAttribute { #[darling(flatten)] rename: PhpRename, #[darling(default)] - allow_discriminants: bool, + allow_native_discriminants: Flag, rename_cases: Option, vis: Option, attrs: Vec, @@ -29,7 +29,7 @@ struct PhpEnumAttribute { struct PhpEnumVariantAttribute { #[darling(flatten)] rename: PhpRename, - discriminant: Option, + discriminant: Option, // TODO: Implement doc support for enum variants #[allow(dead_code)] attrs: Vec, @@ -47,8 +47,8 @@ pub fn parser(mut input: ItemEnum) -> Result { if variant.fields != Fields::Unit { bail!("Enum cases must be unit variants, found: {:?}", variant); } - if !php_attr.allow_discriminants && variant.discriminant.is_some() { - bail!(variant => "Native discriminants are currently not exported to PHP. To set a discriminant, use the `#[php(allow_discriminants)]` attribute on the enum. To export discriminants, set the #[php(discriminant = ...)] attribute on the enum case."); + if !php_attr.allow_native_discriminants.is_present() && variant.discriminant.is_some() { + bail!(variant => "Native discriminants are currently not exported to PHP. To set a discriminant, use the `#[php(allow_native_discriminants)]` attribute on the enum. To export discriminants, set the #[php(discriminant = ...)] attribute on the enum case."); } let variant_attr = PhpEnumVariantAttribute::from_attributes(&variant.attrs)?; @@ -219,21 +219,17 @@ enum Discriminant { Integer(i64), } -impl TryFrom<&Expr> for Discriminant { +impl TryFrom<&Lit> for Discriminant { type Error = syn::Error; - fn try_from(expr: &Expr) -> Result { - match expr { - Expr::Lit(expr) => match &expr.lit { - Lit::Str(s) => Ok(Discriminant::String(s.value())), - Lit::Int(i) => i.base10_parse::().map(Discriminant::Integer).map_err( - |_| err!(expr => "Invalid integer literal for enum case: {:?}", expr.lit), - ), - _ => bail!(expr => "Unsupported discriminant type: {:?}", expr.lit), - }, - _ => { - bail!(expr => "Unsupported discriminant type, expected a literal of type string or i64, found: {:?}", expr); - } + fn try_from(lit: &Lit) -> Result { + match lit { + Lit::Str(s) => Ok(Discriminant::String(s.value())), + Lit::Int(i) => i + .base10_parse::() + .map(Discriminant::Integer) + .map_err(|_| err!(lit => "Invalid integer literal for enum case: {:?}", lit)), + _ => bail!(lit => "Unsupported discriminant type: {:?}", lit), } } } @@ -242,7 +238,7 @@ impl ToTokens for Discriminant { fn to_tokens(&self, tokens: &mut TokenStream) { tokens.extend(match self { Discriminant::String(s) => { - quote! { ::ext_php_rs::enum_::Discriminant::String(#s.to_string()) } + quote! { ::ext_php_rs::enum_::Discriminant::String(#s) } } Discriminant::Integer(i) => { quote! { ::ext_php_rs::enum_::Discriminant::Int(#i) } diff --git a/src/builders/enum_builder.rs b/src/builders/enum_builder.rs index ad9b506725..77bfb98e27 100644 --- a/src/builders/enum_builder.rs +++ b/src/builders/enum_builder.rs @@ -6,7 +6,7 @@ use crate::{ error::Result, ffi::{zend_enum_add_case, zend_register_internal_enum}, flags::{DataType, MethodFlags}, - types::ZendStr, + types::{ZendStr, Zval}, zend::FunctionEntry, }; @@ -73,7 +73,7 @@ impl EnumBuilder { let name = ZendStr::new(case.name, true); let value = match &case.discriminant { Some(value) => { - let value = value.into_zval(false)?; + let value: Zval = value.try_into()?; let mut zv = core::mem::ManuallyDrop::new(value); (&raw mut zv).cast() } diff --git a/src/enum_.rs b/src/enum_.rs index 760054616a..7efa602239 100644 --- a/src/enum_.rs +++ b/src/enum_.rs @@ -1,6 +1,10 @@ //! This module defines the `PhpEnum` trait and related types for Rust enums that are exported to PHP. use crate::{ - convert::IntoZval, describe::DocComments, error::Result, flags::DataType, types::Zval, + convert::IntoZval, + describe::DocComments, + error::{Error, Result}, + flags::DataType, + types::Zval, }; /// Implemented on Rust enums which are exported to PHP. @@ -39,17 +43,13 @@ pub enum Discriminant { String(&'static str), } -impl Discriminant { - /// Converts the discriminant to a PHP value. - /// - /// # Errors - /// - /// Returns an error if the conversion fails. See [`String`] and [`i64`] [`IntoZval`] implementations - /// for more details on potential errors. - pub fn into_zval(&self, persistent: bool) -> Result { - match self { - Discriminant::Int(i) => i.into_zval(persistent), - Discriminant::String(s) => s.into_zval(persistent), +impl TryFrom<&Discriminant> for Zval { + type Error = Error; + + fn try_from(value: &Discriminant) -> Result { + match value { + Discriminant::Int(i) => i.into_zval(false), + Discriminant::String(s) => s.into_zval(true), } } } diff --git a/tests/src/integration/enum_/enum.php b/tests/src/integration/enum_/enum.php index ae12df726c..11d15e9cb7 100644 --- a/tests/src/integration/enum_/enum.php +++ b/tests/src/integration/enum_/enum.php @@ -6,6 +6,14 @@ var_dump($enum_variant); assert($enum_variant === TestEnum::Variant1); assert($enum_variant !== TestEnum::Variant2); +assert(TestEnum::cases() === [TestEnum::Variant1, TestEnum::Variant2]); -$backed = IntBackedEnum::Variant1; -assert($backed->value === 1); +assert(IntBackedEnum::Variant1->value === 1); +assert(IntBackedEnum::from(2) === IntBackedEnum::Variant2); +assert(IntBackedEnum::tryFrom(1) === IntBackedEnum::Variant1); +assert(IntBackedEnum::tryFrom(3) === null); + +assert(StringBackedEnum::Variant1->value === 'foo'); +assert(StringBackedEnum::from('bar') === StringBackedEnum::Variant2); +assert(StringBackedEnum::tryFrom('foo') === StringBackedEnum::Variant1); +assert(StringBackedEnum::tryFrom('baz') === null); diff --git a/tests/src/integration/enum_/mod.rs b/tests/src/integration/enum_/mod.rs index ea384bd4a0..1921d8e29c 100644 --- a/tests/src/integration/enum_/mod.rs +++ b/tests/src/integration/enum_/mod.rs @@ -1,7 +1,7 @@ use ext_php_rs::{php_enum, prelude::ModuleBuilder}; #[php_enum] -#[php(allow_discriminants)] +#[php(allow_native_discriminants)] pub enum TestEnum { // #[php(discriminant = 2)] Variant1, @@ -17,16 +17,19 @@ pub enum IntBackedEnum { Variant2, } -// #[php_enum] -// pub enum StringBackedEnum { -// #[php(discriminant = "foo")] -// Variant1, -// #[php(discriminant = "bar")] -// Variant2, -// } +#[php_enum] +pub enum StringBackedEnum { + #[php(discriminant = "foo")] + Variant1, + #[php(discriminant = "bar")] + Variant2, +} pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { - builder.r#enum::().r#enum::() + builder + .r#enum::() + .r#enum::() + .r#enum::() } #[cfg(test)] From 66105adf9c50eed6e71a694d481c0dffcc6560ce Mon Sep 17 00:00:00 2001 From: Xenira <1288524+Xenira@users.noreply.github.com> Date: Tue, 8 Jul 2025 19:59:14 +0200 Subject: [PATCH 03/10] ci(enum): exclude `enum` feature on PHP 8.0 Refs: #178 --- .github/workflows/build.yml | 5 +++-- crates/cli/Cargo.toml | 6 +++++- src/builders/enum_builder.rs | 2 +- src/flags.rs | 22 ++++++++++++---------- tests/Cargo.toml | 1 + tests/src/integration/mod.rs | 2 +- 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3a45a3b5cb..b74b981723 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,6 +80,7 @@ jobs: env: DOCS_RS: "" run: cargo clean && cargo build + build: name: Build and Test runs-on: ${{ matrix.os }} @@ -161,12 +162,12 @@ jobs: - name: Build env: EXT_PHP_RS_TEST: "" - run: cargo build --release --features closure,anyhow --all + run: cargo build --release --features closure,anyhow --workspace ${{ matrix.php == '8.0' && '--no-default-features' || '' }} # Test - name: Test inline examples # Macos fails on unstable rust. We skip the inline examples test for now. if: "!(contains(matrix.os, 'macos') && matrix.rust == 'nightly')" - run: cargo test --release --all --features closure,anyhow --no-fail-fast + run: cargo test --release --workspace --features closure,anyhow --no-fail-fast ${{ matrix.php == '8.0' && '--no-default-features' || '' }} build-zts: name: Build with ZTS runs-on: ubuntu-latest diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index d02265e68f..49408ee2b2 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -11,7 +11,7 @@ edition = "2018" categories = ["api-bindings", "command-line-interface"] [dependencies] -ext-php-rs = { version = "0.14", path = "../../" } +ext-php-rs = { version = "0.14", default-features = false, path = "../../" } clap = { version = "4.0", features = ["derive"] } anyhow = "1" @@ -22,3 +22,7 @@ semver = "1.0" [lints.rust] missing_docs = "warn" + +[features] +default = ["enum"] +enum = ["ext-php-rs/enum"] diff --git a/src/builders/enum_builder.rs b/src/builders/enum_builder.rs index 77bfb98e27..759167fe5f 100644 --- a/src/builders/enum_builder.rs +++ b/src/builders/enum_builder.rs @@ -70,7 +70,7 @@ impl EnumBuilder { }; for case in self.cases { - let name = ZendStr::new(case.name, true); + let name = ZendStr::new_interned(case.name, true); let value = match &case.discriminant { Some(value) => { let value: Zval = value.try_into()?; diff --git a/src/flags.rs b/src/flags.rs index 139722c491..179bfc2f8d 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -2,6 +2,8 @@ use bitflags::bitflags; +#[cfg(php81)] +use crate::ffi::ZEND_ACC_ENUM; #[cfg(not(php82))] use crate::ffi::ZEND_ACC_REUSE_GET_ITERATOR; use crate::ffi::{ @@ -14,16 +16,16 @@ use crate::ffi::{ PHP_INI_PERDIR, PHP_INI_SYSTEM, PHP_INI_USER, ZEND_ACC_ABSTRACT, ZEND_ACC_ANON_CLASS, ZEND_ACC_CALL_VIA_TRAMPOLINE, ZEND_ACC_CHANGED, ZEND_ACC_CLOSURE, ZEND_ACC_CONSTANTS_UPDATED, ZEND_ACC_CTOR, ZEND_ACC_DEPRECATED, ZEND_ACC_DONE_PASS_TWO, ZEND_ACC_EARLY_BINDING, - ZEND_ACC_ENUM, ZEND_ACC_FAKE_CLOSURE, ZEND_ACC_FINAL, ZEND_ACC_GENERATOR, - ZEND_ACC_HAS_FINALLY_BLOCK, ZEND_ACC_HAS_RETURN_TYPE, ZEND_ACC_HAS_TYPE_HINTS, - ZEND_ACC_HEAP_RT_CACHE, ZEND_ACC_IMMUTABLE, ZEND_ACC_IMPLICIT_ABSTRACT_CLASS, - ZEND_ACC_INTERFACE, ZEND_ACC_LINKED, ZEND_ACC_NEARLY_LINKED, ZEND_ACC_NEVER_CACHE, - ZEND_ACC_NO_DYNAMIC_PROPERTIES, ZEND_ACC_PRELOADED, ZEND_ACC_PRIVATE, ZEND_ACC_PROMOTED, - ZEND_ACC_PROTECTED, ZEND_ACC_PUBLIC, ZEND_ACC_RESOLVED_INTERFACES, ZEND_ACC_RESOLVED_PARENT, - ZEND_ACC_RETURN_REFERENCE, ZEND_ACC_STATIC, ZEND_ACC_STRICT_TYPES, ZEND_ACC_TOP_LEVEL, - ZEND_ACC_TRAIT, ZEND_ACC_TRAIT_CLONE, ZEND_ACC_UNRESOLVED_VARIANCE, ZEND_ACC_USES_THIS, - ZEND_ACC_USE_GUARDS, ZEND_ACC_VARIADIC, ZEND_EVAL_CODE, ZEND_HAS_STATIC_IN_METHODS, - ZEND_INTERNAL_FUNCTION, ZEND_USER_FUNCTION, Z_TYPE_FLAGS_SHIFT, _IS_BOOL, + ZEND_ACC_FAKE_CLOSURE, ZEND_ACC_FINAL, ZEND_ACC_GENERATOR, ZEND_ACC_HAS_FINALLY_BLOCK, + ZEND_ACC_HAS_RETURN_TYPE, ZEND_ACC_HAS_TYPE_HINTS, ZEND_ACC_HEAP_RT_CACHE, ZEND_ACC_IMMUTABLE, + ZEND_ACC_IMPLICIT_ABSTRACT_CLASS, ZEND_ACC_INTERFACE, ZEND_ACC_LINKED, ZEND_ACC_NEARLY_LINKED, + ZEND_ACC_NEVER_CACHE, ZEND_ACC_NO_DYNAMIC_PROPERTIES, ZEND_ACC_PRELOADED, ZEND_ACC_PRIVATE, + ZEND_ACC_PROMOTED, ZEND_ACC_PROTECTED, ZEND_ACC_PUBLIC, ZEND_ACC_RESOLVED_INTERFACES, + ZEND_ACC_RESOLVED_PARENT, ZEND_ACC_RETURN_REFERENCE, ZEND_ACC_STATIC, ZEND_ACC_STRICT_TYPES, + ZEND_ACC_TOP_LEVEL, ZEND_ACC_TRAIT, ZEND_ACC_TRAIT_CLONE, ZEND_ACC_UNRESOLVED_VARIANCE, + ZEND_ACC_USES_THIS, ZEND_ACC_USE_GUARDS, ZEND_ACC_VARIADIC, ZEND_EVAL_CODE, + ZEND_HAS_STATIC_IN_METHODS, ZEND_INTERNAL_FUNCTION, ZEND_USER_FUNCTION, Z_TYPE_FLAGS_SHIFT, + _IS_BOOL, }; use std::{convert::TryFrom, fmt::Display}; diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 9f79f427ed..845c01acab 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -10,6 +10,7 @@ cfg-if = "1.0.1" ext-php-rs = { path = "../", default-features = false, features = ["closure"] } [features] +default = ["enum"] enum = ["ext-php-rs/enum"] [lib] diff --git a/tests/src/integration/mod.rs b/tests/src/integration/mod.rs index 04bdc282e2..cc18e5108d 100644 --- a/tests/src/integration/mod.rs +++ b/tests/src/integration/mod.rs @@ -31,7 +31,7 @@ mod test { fn setup() { BUILD.call_once(|| { let mut command = Command::new("cargo"); - command.arg("build"); + command.arg("build").arg("--no-default-features"); #[cfg(feature = "enum")] { command.arg("--features=enum"); From d91db429edb1216be39cf3f490e29db3d372cf68 Mon Sep 17 00:00:00 2001 From: Xenira <1288524+Xenira@users.noreply.github.com> Date: Tue, 8 Jul 2025 23:48:20 +0200 Subject: [PATCH 04/10] feat(enum): implement `IntoZval` and `FromZval` for enums Refs: #178 --- allowed_bindings.rs | 1 + crates/macros/src/enum_.rs | 101 +++++++++++++++++++++------ docsrs_bindings.rs | 6 ++ src/builders/enum_builder.rs | 21 +++++- src/builders/module.rs | 8 ++- src/enum_.rs | 98 ++++++++++++++++++++++++-- tests/src/integration/enum_/enum.php | 2 + tests/src/integration/enum_/mod.rs | 11 ++- 8 files changed, 217 insertions(+), 31 deletions(-) diff --git a/allowed_bindings.rs b/allowed_bindings.rs index b7a48f8c68..81a2a78c65 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -88,6 +88,7 @@ bind! { zend_declare_property, zend_do_implement_interface, zend_enum_add_case, + zend_enum_get_case, zend_enum_new, zend_execute_data, zend_function_entry, diff --git a/crates/macros/src/enum_.rs b/crates/macros/src/enum_.rs index d7e8603bfd..c2b0f29788 100644 --- a/crates/macros/src/enum_.rs +++ b/crates/macros/src/enum_.rs @@ -101,13 +101,13 @@ pub fn parser(mut input: ItemEnum) -> Result { } } - let enum_props = Enum { - ident: &input.ident, - attrs: php_attr, + let enum_props = Enum::new( + &input.ident, + &php_attr, docs, cases, - flags: None, // TODO: Implement flags support - }; + None, // TODO: Implement flags support + ); Ok(quote! { #[allow(dead_code)] @@ -120,28 +120,45 @@ pub fn parser(mut input: ItemEnum) -> Result { #[derive(Debug)] pub struct Enum<'a> { ident: &'a Ident, - attrs: PhpEnumAttribute, + name: String, docs: Vec, cases: Vec, - // TODO: Implement flags support - #[allow(dead_code)] flags: Option, } -impl ToTokens for Enum<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { +impl<'a> Enum<'a> { + fn new( + ident: &'a Ident, + attrs: &PhpEnumAttribute, + docs: Vec, + cases: Vec, + flags: Option, + ) -> Self { + let name = attrs.rename.rename(ident.to_string(), RenameRule::Pascal); + + Self { + ident, + name, + docs, + cases, + flags, + } + } + + fn registered_class(&self) -> TokenStream { let ident = &self.ident; - let enum_name = self - .attrs - .rename - .rename(ident.to_string(), RenameRule::Pascal); - let flags = quote! { ::ext_php_rs::flags::ClassFlags::Enum }; + let name = &self.name; + let flags = self + .flags + .as_ref() + .map(|f| quote! { | #f }) + .unwrap_or_default(); + let flags = quote! { ::ext_php_rs::flags::ClassFlags::Enum #flags }; let docs = &self.docs; - let cases = &self.cases; - let class = quote! { + quote! { impl ::ext_php_rs::class::RegisteredClass for #ident { - const CLASS_NAME: &'static str = #enum_name; + const CLASS_NAME: &'static str = #name; const BUILDER_MODIFIER: ::std::option::Option< fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder > = None; @@ -186,14 +203,54 @@ impl ToTokens for Enum<'_> { ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_constants() } } - }; - let enum_impl = quote! { - impl ::ext_php_rs::enum_::PhpEnum for #ident { + } + } + + fn registered_enum(&self) -> TokenStream { + let ident = &self.ident; + let cases = &self.cases; + let case_from_names = self.cases.iter().map(|case| { + let ident = &case.ident; + let name = &case.name; + quote! { + #name => Ok(Self::#ident) + } + }); + let case_to_names = self.cases.iter().map(|case| { + let ident = &case.ident; + let name = &case.name; + quote! { + Self::#ident => #name + } + }); + + quote! { + impl ::ext_php_rs::enum_::RegisteredEnum for #ident { const CASES: &'static [::ext_php_rs::enum_::EnumCase] = &[ #(#cases,)* ]; + + fn from_name(name: &str) -> ::ext_php_rs::error::Result { + match name { + #(#case_from_names,)* + _ => Err(::ext_php_rs::error::Error::InvalidProperty), + } + } + + fn to_name(&self) -> &'static str { + match self { + #(#case_to_names,)* + } + } } - }; + } + } +} + +impl ToTokens for Enum<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let class = self.registered_class(); + let enum_impl = self.registered_enum(); tokens.extend(quote! { #class diff --git a/docsrs_bindings.rs b/docsrs_bindings.rs index ef5e3b2073..e23a8302d5 100644 --- a/docsrs_bindings.rs +++ b/docsrs_bindings.rs @@ -2723,6 +2723,12 @@ extern "C" { value: *mut zval, ); } +extern "C" { + pub fn zend_enum_get_case( + ce: *mut zend_class_entry, + name: *mut zend_string, + ) -> *mut zend_object; +} extern "C" { pub static mut zend_ce_throwable: *mut zend_class_entry; } diff --git a/src/builders/enum_builder.rs b/src/builders/enum_builder.rs index 759167fe5f..c77a60f058 100644 --- a/src/builders/enum_builder.rs +++ b/src/builders/enum_builder.rs @@ -7,7 +7,7 @@ use crate::{ ffi::{zend_enum_add_case, zend_register_internal_enum}, flags::{DataType, MethodFlags}, types::{ZendStr, Zval}, - zend::FunctionEntry, + zend::{ClassEntry, FunctionEntry}, }; #[must_use] @@ -16,6 +16,7 @@ pub struct EnumBuilder { pub(crate) methods: Vec<(FunctionBuilder<'static>, MethodFlags)>, pub(crate) cases: Vec<&'static EnumCase>, pub(crate) datatype: DataType, + register: Option, } impl EnumBuilder { @@ -25,6 +26,7 @@ impl EnumBuilder { methods: Vec::default(), cases: Vec::default(), datatype: DataType::Undef, + register: None, } } @@ -48,6 +50,17 @@ impl EnumBuilder { self } + /// 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 + } + pub fn register(self) -> Result<()> { let mut methods = self .methods @@ -84,6 +97,12 @@ impl EnumBuilder { } } + if let Some(register) = self.register { + register(unsafe { &mut *class }); + } else { + panic!("Enum was not registered with a registration function",); + } + Ok(()) } } diff --git a/src/builders/module.rs b/src/builders/module.rs index 8196af255c..4e8c06a069 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -2,7 +2,7 @@ use std::{convert::TryFrom, ffi::CString, mem, ptr}; use super::{ClassBuilder, FunctionBuilder}; #[cfg(feature = "enum")] -use crate::{builders::enum_builder::EnumBuilder, enum_::PhpEnum}; +use crate::{builders::enum_builder::EnumBuilder, enum_::RegisteredEnum}; use crate::{ class::RegisteredClass, constant::IntoConst, @@ -215,7 +215,7 @@ impl ModuleBuilder<'_> { #[cfg(feature = "enum")] pub fn r#enum(mut self) -> Self where - T: RegisteredClass + PhpEnum, + T: RegisteredClass + RegisteredEnum, { self.enums.push(|| { let mut builder = EnumBuilder::new(T::CLASS_NAME); @@ -226,7 +226,9 @@ impl ModuleBuilder<'_> { builder = builder.add_method(method, flags); } - builder + builder.registration(|ce| { + T::get_metadata().set_ce(ce); + }) }); self diff --git a/src/enum_.rs b/src/enum_.rs index 7efa602239..8c72ec71db 100644 --- a/src/enum_.rs +++ b/src/enum_.rs @@ -1,17 +1,107 @@ //! This module defines the `PhpEnum` trait and related types for Rust enums that are exported to PHP. +use std::ptr; + use crate::{ - convert::IntoZval, + boxed::ZBox, + class::RegisteredClass, + convert::{FromZendObject, FromZval, IntoZendObject, IntoZval}, describe::DocComments, error::{Error, Result}, - flags::DataType, - types::Zval, + ffi::zend_enum_get_case, + flags::{ClassFlags, DataType}, + types::{ZendObject, ZendStr, Zval}, }; /// Implemented on Rust enums which are exported to PHP. -pub trait PhpEnum { +pub trait RegisteredEnum { /// The cases of the enum. const CASES: &'static [EnumCase]; + + /// # Errors + /// + /// - [`Error::InvalidProperty`] if the enum does not have a case with the given name, an error is returned. + fn from_name(name: &str) -> Result + where + Self: Sized; + + /// Returns the variant name of the enum as it is registered in PHP. + fn to_name(&self) -> &'static str; +} + +impl FromZendObject<'_> for T +where + T: RegisteredEnum, +{ + fn from_zend_object(obj: &ZendObject) -> Result { + if !ClassFlags::from_bits_truncate(unsafe { (*obj.ce).ce_flags }).contains(ClassFlags::Enum) + { + return Err(Error::InvalidProperty); + } + + let name = obj + .get_properties()? + .get("name") + .and_then(Zval::indirect) + .and_then(Zval::str) + .ok_or(Error::InvalidProperty)?; + + T::from_name(name) + } +} + +impl FromZval<'_> for T +where + T: RegisteredEnum + RegisteredClass, +{ + const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME)); + + fn from_zval(zval: &Zval) -> Option { + zval.object() + .and_then(|obj| Self::from_zend_object(obj).ok()) + } +} + +impl IntoZendObject for T +where + T: RegisteredEnum + RegisteredClass, +{ + fn into_zend_object(self) -> Result> { + let mut name = ZendStr::new(T::to_name(&self), false); + let variant = unsafe { + zend_enum_get_case( + ptr::from_ref(T::get_metadata().ce()).cast_mut(), + &raw mut *name, + ) + }; + + Ok(unsafe { ZBox::from_raw(variant) }) + } +} + +impl IntoZval for T +where + T: RegisteredEnum + RegisteredClass, +{ + const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME)); + const NULLABLE: bool = false; + + fn set_zval(self, zv: &mut Zval, _persistent: bool) -> Result<()> { + let obj = self.into_zend_object()?; + zv.set_object(obj.into_raw()); + Ok(()) + } } +// impl<'a, T> IntoZval for T +// where +// T: RegisteredEnum + RegisteredClass + IntoZendObject +// { +// const TYPE: DataType = DataType::Object(Some(T::CLASS_NAME)); +// const NULLABLE: bool = false; +// +// fn set_zval(self, zv: &mut Zval, persistent: bool) -> Result<()> { +// let obj = self.into_zend_object()?; +// } +// } /// Represents a case in a PHP enum. pub struct EnumCase { diff --git a/tests/src/integration/enum_/enum.php b/tests/src/integration/enum_/enum.php index 11d15e9cb7..3aaeb3d7e1 100644 --- a/tests/src/integration/enum_/enum.php +++ b/tests/src/integration/enum_/enum.php @@ -17,3 +17,5 @@ assert(StringBackedEnum::from('bar') === StringBackedEnum::Variant2); assert(StringBackedEnum::tryFrom('foo') === StringBackedEnum::Variant1); assert(StringBackedEnum::tryFrom('baz') === null); + +assert(test_enum(TestEnum::Variant2) === TestEnum::Variant1); diff --git a/tests/src/integration/enum_/mod.rs b/tests/src/integration/enum_/mod.rs index 1921d8e29c..7132fe33e7 100644 --- a/tests/src/integration/enum_/mod.rs +++ b/tests/src/integration/enum_/mod.rs @@ -1,4 +1,4 @@ -use ext_php_rs::{php_enum, prelude::ModuleBuilder}; +use ext_php_rs::{php_enum, php_function, prelude::ModuleBuilder, wrap_function}; #[php_enum] #[php(allow_native_discriminants)] @@ -25,11 +25,20 @@ pub enum StringBackedEnum { Variant2, } +#[php_function] +pub fn test_enum(a: TestEnum) -> TestEnum { + match a { + TestEnum::Variant1 => TestEnum::Variant2, + TestEnum::Variant2 => TestEnum::Variant1, + } +} + pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { builder .r#enum::() .r#enum::() .r#enum::() + .function(wrap_function!(test_enum)) } #[cfg(test)] From a1fe00d1e7d04a49cc0ffdc3d4d5e8a530d0afbc Mon Sep 17 00:00:00 2001 From: Xenira <1288524+Xenira@users.noreply.github.com> Date: Wed, 9 Jul 2025 19:47:07 +0200 Subject: [PATCH 05/10] feat(enum): implement `TryFrom` and `Into` for backed enum Refs: #178 --- crates/macros/src/enum_.rs | 81 ++++++++++++++++++++++++++++ tests/src/integration/enum_/enum.php | 3 +- tests/src/integration/enum_/mod.rs | 9 ++-- 3 files changed, 87 insertions(+), 6 deletions(-) diff --git a/crates/macros/src/enum_.rs b/crates/macros/src/enum_.rs index c2b0f29788..b0b79c093b 100644 --- a/crates/macros/src/enum_.rs +++ b/crates/macros/src/enum_.rs @@ -107,6 +107,7 @@ pub fn parser(mut input: ItemEnum) -> Result { docs, cases, None, // TODO: Implement flags support + discriminant_type, ); Ok(quote! { @@ -121,6 +122,7 @@ pub fn parser(mut input: ItemEnum) -> Result { pub struct Enum<'a> { ident: &'a Ident, name: String, + discriminant_type: DiscriminantType, docs: Vec, cases: Vec, flags: Option, @@ -133,12 +135,14 @@ impl<'a> Enum<'a> { docs: Vec, cases: Vec, flags: Option, + discriminant_type: DiscriminantType, ) -> Self { let name = attrs.rename.rename(ident.to_string(), RenameRule::Pascal); Self { ident, name, + discriminant_type, docs, cases, flags, @@ -245,16 +249,93 @@ impl<'a> Enum<'a> { } } } + + pub fn impl_try_from(&self) -> TokenStream { + if self.discriminant_type == DiscriminantType::None { + return quote! {}; + } + let discriminant_type = match self.discriminant_type { + DiscriminantType::Integer => quote! { i64 }, + DiscriminantType::String => quote! { &str }, + DiscriminantType::None => unreachable!("Discriminant type should not be None here"), + }; + let ident = &self.ident; + let cases = self.cases.iter().map(|case| { + let ident = &case.ident; + match case + .discriminant + .as_ref() + .expect("Discriminant should be set") + { + Discriminant::String(s) => quote! { #s => Ok(Self::#ident) }, + Discriminant::Integer(i) => quote! { #i => Ok(Self::#ident) }, + } + }); + + quote! { + impl TryFrom<#discriminant_type> for #ident { + type Error = ::ext_php_rs::error::Error; + + fn try_from(value: #discriminant_type) -> ::ext_php_rs::error::Result { + match value { + #( + #cases, + )* + _ => Err(::ext_php_rs::error::Error::InvalidProperty), + } + } + } + } + } + + pub fn impl_into(&self) -> TokenStream { + if self.discriminant_type == DiscriminantType::None { + return quote! {}; + } + let discriminant_type = match self.discriminant_type { + DiscriminantType::Integer => quote! { i64 }, + DiscriminantType::String => quote! { &'static str }, + DiscriminantType::None => unreachable!("Discriminant type should not be None here"), + }; + let ident = &self.ident; + let cases = self.cases.iter().map(|case| { + let ident = &case.ident; + match case + .discriminant + .as_ref() + .expect("Discriminant should be set") + { + Discriminant::String(s) => quote! { Self::#ident => #s }, + Discriminant::Integer(i) => quote! { Self::#ident => #i }, + } + }); + + quote! { + impl Into<#discriminant_type> for #ident { + fn into(self) -> #discriminant_type { + match self { + #( + #cases, + )* + } + } + } + } + } } impl ToTokens for Enum<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { let class = self.registered_class(); let enum_impl = self.registered_enum(); + let impl_try_from = self.impl_try_from(); + let impl_into = self.impl_into(); tokens.extend(quote! { #class #enum_impl + #impl_try_from + #impl_into }); } } diff --git a/tests/src/integration/enum_/enum.php b/tests/src/integration/enum_/enum.php index 3aaeb3d7e1..047b9d1a0c 100644 --- a/tests/src/integration/enum_/enum.php +++ b/tests/src/integration/enum_/enum.php @@ -3,7 +3,6 @@ declare(strict_types=1); $enum_variant = TestEnum::Variant1; -var_dump($enum_variant); assert($enum_variant === TestEnum::Variant1); assert($enum_variant !== TestEnum::Variant2); assert(TestEnum::cases() === [TestEnum::Variant1, TestEnum::Variant2]); @@ -18,4 +17,4 @@ assert(StringBackedEnum::tryFrom('foo') === StringBackedEnum::Variant1); assert(StringBackedEnum::tryFrom('baz') === null); -assert(test_enum(TestEnum::Variant2) === TestEnum::Variant1); +assert(test_enum(TestEnum::Variant1) === StringBackedEnum::Variant2); diff --git a/tests/src/integration/enum_/mod.rs b/tests/src/integration/enum_/mod.rs index 7132fe33e7..63904209e6 100644 --- a/tests/src/integration/enum_/mod.rs +++ b/tests/src/integration/enum_/mod.rs @@ -1,4 +1,4 @@ -use ext_php_rs::{php_enum, php_function, prelude::ModuleBuilder, wrap_function}; +use ext_php_rs::{error::Result, php_enum, php_function, prelude::ModuleBuilder, wrap_function}; #[php_enum] #[php(allow_native_discriminants)] @@ -26,10 +26,11 @@ pub enum StringBackedEnum { } #[php_function] -pub fn test_enum(a: TestEnum) -> TestEnum { +pub fn test_enum(a: TestEnum) -> Result { + let str: &str = StringBackedEnum::Variant2.into(); match a { - TestEnum::Variant1 => TestEnum::Variant2, - TestEnum::Variant2 => TestEnum::Variant1, + TestEnum::Variant1 => str.try_into(), + TestEnum::Variant2 => Ok(StringBackedEnum::Variant1), } } From 70e91f32d0c07c8424c974e14f6a4a48bd2cb582 Mon Sep 17 00:00:00 2001 From: Xenira <1288524+Xenira@users.noreply.github.com> Date: Wed, 9 Jul 2025 20:29:02 +0200 Subject: [PATCH 06/10] feat(enum): add stub generation Refs: #178 --- src/builders/enum_builder.rs | 30 +++++++++- src/builders/mod.rs | 2 + src/builders/module.rs | 10 ++-- src/describe/mod.rs | 94 ++++++++++++++++++++++++++++++ src/describe/stub.rs | 56 ++++++++++++++++-- tests/src/integration/enum_/mod.rs | 8 ++- 6 files changed, 189 insertions(+), 11 deletions(-) diff --git a/src/builders/enum_builder.rs b/src/builders/enum_builder.rs index c77a60f058..1d1ea998ac 100644 --- a/src/builders/enum_builder.rs +++ b/src/builders/enum_builder.rs @@ -2,6 +2,7 @@ use std::{ffi::CString, ptr}; use crate::{ builders::FunctionBuilder, + describe::DocComments, enum_::EnumCase, error::Result, ffi::{zend_enum_add_case, zend_register_internal_enum}, @@ -10,6 +11,7 @@ use crate::{ zend::{ClassEntry, FunctionEntry}, }; +/// A builder for PHP enums. #[must_use] pub struct EnumBuilder { pub(crate) name: String, @@ -17,9 +19,11 @@ pub struct EnumBuilder { pub(crate) cases: Vec<&'static EnumCase>, pub(crate) datatype: DataType, register: Option, + pub(crate) docs: DocComments, } impl EnumBuilder { + /// Creates a new enum builder with the given name. pub fn new>(name: T) -> Self { Self { name: name.into(), @@ -27,9 +31,15 @@ impl EnumBuilder { cases: Vec::default(), datatype: DataType::Undef, register: None, + docs: DocComments::default(), } } + /// Adds an enum case to the enum. + /// + /// # Panics + /// + /// If the case's data type does not match the enum's data type pub fn case(mut self, case: &'static EnumCase) -> Self { let data_type = case.data_type(); assert!( @@ -45,7 +55,8 @@ impl EnumBuilder { self } - pub fn add_method(mut self, method: FunctionBuilder<'static>, flags: MethodFlags) -> Self { + /// Adds a method to the enum. + pub fn method(mut self, method: FunctionBuilder<'static>, flags: MethodFlags) -> Self { self.methods.push((method, flags)); self } @@ -61,6 +72,23 @@ impl EnumBuilder { self } + /// Add documentation comments to the enum. + pub fn docs(mut self, docs: DocComments) -> Self { + self.docs = docs; + self + } + + /// Registers the enum with PHP. + /// + /// # Panics + /// + /// If the registration function was not set prior to calling this + /// method. + /// + /// # Errors + /// + /// If the enum could not be registered, e.g. due to an invalid name or + /// data type. pub fn register(self) -> Result<()> { let mut methods = self .methods diff --git a/src/builders/mod.rs b/src/builders/mod.rs index e4880dbebe..c439c81682 100644 --- a/src/builders/mod.rs +++ b/src/builders/mod.rs @@ -12,6 +12,8 @@ mod module; mod sapi; pub use class::ClassBuilder; +#[cfg(feature = "enum")] +pub use enum_builder::EnumBuilder; pub use function::FunctionBuilder; #[cfg(all(php82, feature = "embed"))] pub use ini::IniBuilder; diff --git a/src/builders/module.rs b/src/builders/module.rs index 4e8c06a069..635276e9b9 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -223,12 +223,14 @@ impl ModuleBuilder<'_> { builder = builder.case(case); } for (method, flags) in T::method_builders() { - builder = builder.add_method(method, flags); + builder = builder.method(method, flags); } - builder.registration(|ce| { - T::get_metadata().set_ce(ce); - }) + builder + .registration(|ce| { + T::get_metadata().set_ce(ce); + }) + .docs(T::DOC_COMMENTS) }); self diff --git a/src/describe/mod.rs b/src/describe/mod.rs index 8abf2592e1..1c98d92ad2 100644 --- a/src/describe/mod.rs +++ b/src/describe/mod.rs @@ -2,6 +2,8 @@ //! CLI application to generate PHP stub files used by IDEs. use std::vec::Vec as StdVec; +#[cfg(feature = "enum")] +use crate::builders::EnumBuilder; use crate::{ builders::{ClassBuilder, FunctionBuilder}, constant::IntoConst, @@ -67,6 +69,9 @@ pub struct Module { pub functions: Vec, /// Classes exported by the extension. pub classes: Vec, + #[cfg(feature = "enum")] + /// Enums exported by the extension. + pub enums: Vec, /// Constants exported by the extension. pub constants: Vec, } @@ -95,6 +100,13 @@ impl From> for Module { .map(Constant::from) .collect::>() .into(), + #[cfg(feature = "enum")] + enums: builder + .enums + .into_iter() + .map(|e| e().into()) + .collect::>() + .into(), } } } @@ -216,6 +228,86 @@ impl From for Class { } } +#[cfg(feature = "enum")] +/// Represents an exported enum. +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct Enum { + /// Name of the enum. + pub name: RString, + /// Documentation comments for the enum. + pub docs: DocBlock, + /// Cases of the enum. + pub cases: Vec, + /// Backing type of the enum. + pub backing_type: Option, +} + +#[cfg(feature = "enum")] +impl From for Enum { + fn from(val: EnumBuilder) -> Self { + Self { + name: val.name.into(), + docs: DocBlock( + val.docs + .iter() + .map(|d| (*d).into()) + .collect::>() + .into(), + ), + cases: val + .cases + .into_iter() + .map(EnumCase::from) + .collect::>() + .into(), + backing_type: match val.datatype { + DataType::Long => Some("int".into()), + DataType::String => Some("string".into()), + _ => None, + } + .into(), + } + } +} + +#[cfg(feature = "enum")] +/// Represents a case in an exported enum. +#[repr(C)] +#[derive(Debug, PartialEq)] +pub struct EnumCase { + /// Name of the enum case. + pub name: RString, + /// Documentation comments for the enum case. + pub docs: DocBlock, + /// Value of the enum case. + pub value: Option, +} + +#[cfg(feature = "enum")] +impl From<&'static crate::enum_::EnumCase> for EnumCase { + fn from(val: &'static crate::enum_::EnumCase) -> Self { + Self { + name: val.name.into(), + docs: DocBlock( + val.docs + .iter() + .map(|d| (*d).into()) + .collect::>() + .into(), + ), + value: val + .discriminant + .as_ref() + .map(|v| match v { + crate::enum_::Discriminant::Int(i) => i.to_string().into(), + crate::enum_::Discriminant::String(s) => format!("'{s}'").into(), + }) + .into(), + } + } +} + /// Represents a property attached to an exported class. #[repr(C)] #[derive(Debug, PartialEq)] @@ -437,6 +529,8 @@ mod tests { functions: vec![].into(), classes: vec![].into(), constants: vec![].into(), + #[cfg(feature = "enum")] + enums: vec![].into(), }; let description = Description::new(module); diff --git a/src/describe/stub.rs b/src/describe/stub.rs index 58c7428743..50affc3463 100644 --- a/src/describe/stub.rs +++ b/src/describe/stub.rs @@ -1,15 +1,22 @@ //! Traits and implementations to convert describe units into PHP stub code. -use crate::flags::DataType; -use std::{cmp::Ordering, collections::HashMap}; +use std::{ + cmp::Ordering, + collections::HashMap, + fmt::{Error as FmtError, Result as FmtResult, Write}, + option::Option as StdOption, + vec::Vec as StdVec, +}; use super::{ abi::{Option, RString}, Class, Constant, DocBlock, Function, Method, MethodType, Module, Parameter, Property, Visibility, }; -use std::fmt::{Error as FmtError, Result as FmtResult, Write}; -use std::{option::Option as StdOption, vec::Vec as StdVec}; + +#[cfg(feature = "enum")] +use crate::describe::{Enum, EnumCase}; +use crate::flags::DataType; /// Implemented on types which can be converted into PHP stubs. pub trait ToStub { @@ -78,6 +85,12 @@ impl ToStub for Module { insert(ns, class.to_stub()?); } + #[cfg(feature = "enum")] + for r#enum in &*self.enums { + let (ns, _) = split_namespace(r#enum.name.as_ref()); + insert(ns, r#enum.to_stub()?); + } + let mut entries: StdVec<_> = entries.iter().collect(); entries.sort_by(|(l, _), (r, _)| match (l, r) { (None, _) => Ordering::Greater, @@ -241,6 +254,41 @@ impl ToStub for Class { } } +#[cfg(feature = "enum")] +impl ToStub for Enum { + fn fmt_stub(&self, buf: &mut String) -> FmtResult { + self.docs.fmt_stub(buf)?; + + let (_, name) = split_namespace(self.name.as_ref()); + write!(buf, "enum {name}")?; + + if let Option::Some(backing_type) = &self.backing_type { + write!(buf, ": {backing_type}")?; + } + + writeln!(buf, " {{")?; + + for case in self.cases.iter() { + case.fmt_stub(buf)?; + } + + writeln!(buf, "}}") + } +} + +#[cfg(feature = "enum")] +impl ToStub for EnumCase { + fn fmt_stub(&self, buf: &mut String) -> FmtResult { + self.docs.fmt_stub(buf)?; + + write!(buf, " case {}", self.name)?; + if let Option::Some(value) = &self.value { + write!(buf, " = {value}")?; + } + writeln!(buf, ";") + } +} + impl ToStub for Property { fn fmt_stub(&self, buf: &mut String) -> FmtResult { self.docs.fmt_stub(buf)?; diff --git a/tests/src/integration/enum_/mod.rs b/tests/src/integration/enum_/mod.rs index 63904209e6..361c372cee 100644 --- a/tests/src/integration/enum_/mod.rs +++ b/tests/src/integration/enum_/mod.rs @@ -2,10 +2,14 @@ use ext_php_rs::{error::Result, php_enum, php_function, prelude::ModuleBuilder, #[php_enum] #[php(allow_native_discriminants)] +/// An example enum that demonstrates how to use PHP enums with Rust. +/// This enum has two variants, `Variant1` and `Variant2`. pub enum TestEnum { - // #[php(discriminant = 2)] + /// Represents the first variant of the enum. + /// This variant has a discriminant of 0. + /// But PHP does not know about it. Variant1, - // #[php(discriminant = 1)] + /// Represents the second variant of the enum. Variant2 = 1, } From 596ba24a78b01e8f5b8332519a348cf24fac4775 Mon Sep 17 00:00:00 2001 From: Xenira <1288524+Xenira@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:08:07 +0200 Subject: [PATCH 07/10] docs(enum): extend enum documentation Refs: #178 --- .lefthook.yml | 10 ++-- crates/macros/src/enum_.rs | 4 +- crates/macros/src/lib.rs | 87 +++++++++++++++++++++++++++--- guide/src/macros/enum.md | 76 +++++++++++++++++++++++--- guide/src/macros/php.md | 38 ++++++------- src/builders/module.rs | 2 +- tests/src/integration/enum_/mod.rs | 6 +-- 7 files changed, 178 insertions(+), 45 deletions(-) diff --git a/.lefthook.yml b/.lefthook.yml index 236e7d3da4..8d518ea070 100644 --- a/.lefthook.yml +++ b/.lefthook.yml @@ -17,8 +17,8 @@ pre-commit: The `docsrs_bindings.rs` file seems to be out of date. Please check the updated bindings in `docsrs_bindings.rs` and commit the changes. - name: "macro docs" - run: tools/update_lib_docs.sh && git diff --exit-code crates/macros/src/lib.rs - glob: "guide/src/macros/*.md" - fail_text: | - The macro crates documentation seems to be out of date. - Please check the updated documentation in `crates/macros/src/lib.rs` and commit the changes. + run: tools/update_lib_docs.sh + glob: + - "guide/src/macros/*.md" + - "crates/macros/src/lib.rs" + stage_fixed: true diff --git a/crates/macros/src/enum_.rs b/crates/macros/src/enum_.rs index b0b79c093b..dda1363941 100644 --- a/crates/macros/src/enum_.rs +++ b/crates/macros/src/enum_.rs @@ -20,6 +20,7 @@ struct PhpEnumAttribute { #[darling(default)] allow_native_discriminants: Flag, rename_cases: Option, + // TODO: Implement visibility support vis: Option, attrs: Vec, } @@ -30,8 +31,6 @@ struct PhpEnumVariantAttribute { #[darling(flatten)] rename: PhpRename, discriminant: Option, - // TODO: Implement doc support for enum variants - #[allow(dead_code)] attrs: Vec, } @@ -342,7 +341,6 @@ impl ToTokens for Enum<'_> { #[derive(Debug)] struct EnumCase { - #[allow(dead_code)] ident: Ident, name: String, #[allow(dead_code)] diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index f4ad112c19..d604b87817 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -221,18 +221,28 @@ fn php_class_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 { /// /// Enums can be exported to PHP as enums with the `#[php_enum]` attribute /// macro. This attribute derives the `RegisteredClass` and `PhpEnum` traits on -/// your enum. To register the enum use the `r#enum::()` method on the -/// `ModuleBuilder` in the `#[php_module]` macro. +/// your enum. To register the enum use the `enumeration::()` method +/// on the `ModuleBuilder` in the `#[php_module]` macro. /// /// ## Options /// -/// tbd +/// The `#[php_enum]` attribute can be configured with the following options: +/// - `#[php(name = "EnumName")]` or `#[php(change_case = snake_case)]`: Sets +/// the name of the enum in PHP. The default is the `PascalCase` name of the +/// enum. +/// - `#[php(allow_native_discriminants)]`: Allows the use of native Rust +/// discriminants (e.g., `Hearts = 1`). /// -/// ## Restrictions -/// -/// tbd +/// The cases of the enum can be configured with the following options: +/// - `#[php(name = "CaseName")]` or `#[php(change_case = snake_case)]`: Sets +/// the name of the enum case in PHP. The default is the `PascalCase` name of +/// the case. +/// - `#[php(discriminant = "value")]` or `#[php(discriminant = 123)]`: Sets the +/// discriminant value for the enum case. This can be a string or an integer. +/// If not set, the case will be exported as a simple enum case without a +/// discriminant. /// -/// ## Example +/// ### Example /// /// This example creates a PHP enum `Suit`. /// @@ -251,11 +261,72 @@ fn php_class_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 { /// /// #[php_module] /// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { -/// module.r#enum::() +/// module.enumeration::() /// } /// # fn main() {} /// ``` /// +/// ## Backed Enums +/// Enums can also be backed by either `i64` or `&'static str`. Those values can +/// be set using the `#[php(discriminant = "value")]` or `#[php(discriminant = +/// 123)]` attributes on the enum variants. +/// +/// All variants must have a discriminant of the same type, either all `i64` or +/// all `&'static str`. +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// +/// #[php_enum] +/// pub enum Suit { +/// #[php(discriminant = "hearts")] +/// Hearts, +/// #[php(discriminant = "diamonds")] +/// Diamonds, +/// #[php(discriminant = "clubs")] +/// Clubs, +/// #[php(discriminant = "spades")] +/// Spades, +/// } +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module.enumeration::() +/// } +/// # fn main() {} +/// ``` +/// +/// ### 'Native' Discriminators +/// Native rust discriminants are currently not supported and will not be +/// exported to PHP. +/// +/// To avoid confusion a compiler error will be raised if you try to use a +/// native discriminant. You can ignore this error by adding the +/// `#[php(allow_native_discriminants)]` attribute to your enum. +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// +/// #[php_enum] +/// #[php(allow_native_discriminants)] +/// pub enum Suit { +/// Hearts = 1, +/// Diamonds = 2, +/// Clubs = 3, +/// Spades = 4, +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module.enumeration::() +/// } +/// # fn main() {} +/// ``` +/// +/// /// TODO: Add backed enums example // END DOCS FROM enum.md #[proc_macro_attribute] diff --git a/guide/src/macros/enum.md b/guide/src/macros/enum.md index 3c22a86179..34bdd29914 100644 --- a/guide/src/macros/enum.md +++ b/guide/src/macros/enum.md @@ -2,18 +2,23 @@ Enums can be exported to PHP as enums with the `#[php_enum]` attribute macro. This attribute derives the `RegisteredClass` and `PhpEnum` traits on your enum. -To register the enum use the `r#enum::()` method on the `ModuleBuilder` +To register the enum use the `enumeration::()` method on the `ModuleBuilder` in the `#[php_module]` macro. ## Options -tbd +The `#[php_enum]` attribute can be configured with the following options: +- `#[php(name = "EnumName")]` or `#[php(change_case = snake_case)]`: Sets the name of the enum in PHP. + The default is the `PascalCase` name of the enum. +- `#[php(allow_native_discriminants)]`: Allows the use of native Rust discriminants (e.g., `Hearts = 1`). -## Restrictions +The cases of the enum can be configured with the following options: +- `#[php(name = "CaseName")]` or `#[php(change_case = snake_case)]`: Sets the name of the enum case in PHP. + The default is the `PascalCase` name of the case. +- `#[php(discriminant = "value")]` or `#[php(discriminant = 123)]`: Sets the discriminant value for the enum case. + This can be a string or an integer. If not set, the case will be exported as a simple enum case without a discriminant. -tbd - -## Example +### Example This example creates a PHP enum `Suit`. @@ -32,9 +37,66 @@ pub enum Suit { #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { - module.r#enum::() + module.enumeration::() } # fn main() {} ``` +## Backed Enums +Enums can also be backed by either `i64` or `&'static str`. Those values can be set using the +`#[php(discriminant = "value")]` or `#[php(discriminant = 123)]` attributes on the enum variants. + +All variants must have a discriminant of the same type, either all `i64` or all `&'static str`. + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::prelude::*; + +#[php_enum] +pub enum Suit { + #[php(discriminant = "hearts")] + Hearts, + #[php(discriminant = "diamonds")] + Diamonds, + #[php(discriminant = "clubs")] + Clubs, + #[php(discriminant = "spades")] + Spades, +} +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.enumeration::() +} +# fn main() {} +``` + +### 'Native' Discriminators +Native rust discriminants are currently not supported and will not be exported to PHP. + +To avoid confusion a compiler error will be raised if you try to use a native discriminant. +You can ignore this error by adding the `#[php(allow_native_discriminants)]` attribute to your enum. + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::prelude::*; + +#[php_enum] +#[php(allow_native_discriminants)] +pub enum Suit { + Hearts = 1, + Diamonds = 2, + Clubs = 3, + Spades = 4, +} + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.enumeration::() +} +# fn main() {} +``` + + TODO: Add backed enums example diff --git a/guide/src/macros/php.md b/guide/src/macros/php.md index 0e52ff0117..63d5f107b3 100644 --- a/guide/src/macros/php.md +++ b/guide/src/macros/php.md @@ -33,24 +33,26 @@ fn hello_world(a: i32, b: i32) -> i32 { Which attributes are available depends on the element you are annotating: -| Attribute | `const` | `fn` | `struct` | `struct` field | `impl` | `impl` `const` | `impl` `fn` | -| -------------------- | ------- | ---- | -------- | -------------- | ------ | -------------- | ----------- | -| name | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | -| change_case | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | -| change_method_case | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | -| change_constant_case | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | -| flags | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | -| prop | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| extends | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | -| implements | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | -| modifier | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | -| defaults | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | -| optional | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | -| vis | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | -| getter | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | -| setter | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | -| constructor | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | -| abstract_method | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | +| Attribute | `const` | `fn` | `struct` | `struct` field | `impl` | `impl` `const` | `impl` `fn` | `enum` | `enum` case | +| -------------------------- | ------- | ---- | -------- | -------------- | ------ | -------------- | ----------- | ------ | ----------- | +| name | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | +| change_case | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | +| change_method_case | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| change_constant_case | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| flags | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| prop | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| extends | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| implements | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| modifier | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| defaults | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| optional | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| vis | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| getter | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| setter | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| constructor | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| abstract_method | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| allow_native_discriminants | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | +| discriminant | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ## `name` and `change_case` diff --git a/src/builders/module.rs b/src/builders/module.rs index 635276e9b9..2a37295099 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -213,7 +213,7 @@ impl ModuleBuilder<'_> { /// Adds an enum to the extension. #[cfg(feature = "enum")] - pub fn r#enum(mut self) -> Self + pub fn enumeration(mut self) -> Self where T: RegisteredClass + RegisteredEnum, { diff --git a/tests/src/integration/enum_/mod.rs b/tests/src/integration/enum_/mod.rs index 361c372cee..3988642268 100644 --- a/tests/src/integration/enum_/mod.rs +++ b/tests/src/integration/enum_/mod.rs @@ -40,9 +40,9 @@ pub fn test_enum(a: TestEnum) -> Result { pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { builder - .r#enum::() - .r#enum::() - .r#enum::() + .enumeration::() + .enumeration::() + .enumeration::() .function(wrap_function!(test_enum)) } From 5bcd7f0591183c43adbc51e8a444a75e9f09480e Mon Sep 17 00:00:00 2001 From: Xenira <1288524+Xenira@users.noreply.github.com> Date: Sat, 19 Jul 2025 23:05:54 +0200 Subject: [PATCH 08/10] refactor(enum): use `value` for discriminants to be less verbose Refs: #178 --- crates/macros/src/enum_.rs | 3 ++- crates/macros/src/lib.rs | 23 +++++++++++------------ guide/src/macros/enum.md | 14 +++++++------- tests/src/integration/enum_/mod.rs | 8 ++++---- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/crates/macros/src/enum_.rs b/crates/macros/src/enum_.rs index dda1363941..d10ab89c91 100644 --- a/crates/macros/src/enum_.rs +++ b/crates/macros/src/enum_.rs @@ -30,6 +30,7 @@ struct PhpEnumAttribute { struct PhpEnumVariantAttribute { #[darling(flatten)] rename: PhpRename, + #[darling(rename = "value")] discriminant: Option, attrs: Vec, } @@ -47,7 +48,7 @@ pub fn parser(mut input: ItemEnum) -> Result { bail!("Enum cases must be unit variants, found: {:?}", variant); } if !php_attr.allow_native_discriminants.is_present() && variant.discriminant.is_some() { - bail!(variant => "Native discriminants are currently not exported to PHP. To set a discriminant, use the `#[php(allow_native_discriminants)]` attribute on the enum. To export discriminants, set the #[php(discriminant = ...)] attribute on the enum case."); + bail!(variant => "Native discriminants are currently not exported to PHP. To set a discriminant, use the `#[php(allow_native_discriminants)]` attribute on the enum. To export discriminants, set the #[php(value = ...)] attribute on the enum case."); } let variant_attr = PhpEnumVariantAttribute::from_attributes(&variant.attrs)?; diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index d604b87817..dca9e07eb3 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -237,10 +237,9 @@ fn php_class_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 { /// - `#[php(name = "CaseName")]` or `#[php(change_case = snake_case)]`: Sets /// the name of the enum case in PHP. The default is the `PascalCase` name of /// the case. -/// - `#[php(discriminant = "value")]` or `#[php(discriminant = 123)]`: Sets the -/// discriminant value for the enum case. This can be a string or an integer. -/// If not set, the case will be exported as a simple enum case without a -/// discriminant. +/// - `#[php(value = "value")]` or `#[php(value = 123)]`: Sets the discriminant +/// value for the enum case. This can be a string or an integer. If not set, +/// the case will be exported as a simple enum case without a discriminant. /// /// ### Example /// @@ -268,11 +267,11 @@ fn php_class_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 { /// /// ## Backed Enums /// Enums can also be backed by either `i64` or `&'static str`. Those values can -/// be set using the `#[php(discriminant = "value")]` or `#[php(discriminant = -/// 123)]` attributes on the enum variants. +/// be set using the `#[php(value = "value")]` or `#[php(value = 123)]` +/// attributes on the enum variants. /// -/// All variants must have a discriminant of the same type, either all `i64` or -/// all `&'static str`. +/// All variants must have a value of the same type, either all `i64` or all +/// `&'static str`. /// /// ```rust,no_run,ignore /// # #![cfg_attr(windows, feature(abi_vectorcall))] @@ -281,13 +280,13 @@ fn php_class_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 { /// /// #[php_enum] /// pub enum Suit { -/// #[php(discriminant = "hearts")] +/// #[php(value = "hearts")] /// Hearts, -/// #[php(discriminant = "diamonds")] +/// #[php(value = "diamonds")] /// Diamonds, -/// #[php(discriminant = "clubs")] +/// #[php(value = "clubs")] /// Clubs, -/// #[php(discriminant = "spades")] +/// #[php(value = "spades")] /// Spades, /// } /// #[php_module] diff --git a/guide/src/macros/enum.md b/guide/src/macros/enum.md index 34bdd29914..0c29e8b943 100644 --- a/guide/src/macros/enum.md +++ b/guide/src/macros/enum.md @@ -15,7 +15,7 @@ The `#[php_enum]` attribute can be configured with the following options: The cases of the enum can be configured with the following options: - `#[php(name = "CaseName")]` or `#[php(change_case = snake_case)]`: Sets the name of the enum case in PHP. The default is the `PascalCase` name of the case. -- `#[php(discriminant = "value")]` or `#[php(discriminant = 123)]`: Sets the discriminant value for the enum case. +- `#[php(value = "value")]` or `#[php(value = 123)]`: Sets the discriminant value for the enum case. This can be a string or an integer. If not set, the case will be exported as a simple enum case without a discriminant. ### Example @@ -44,9 +44,9 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { ## Backed Enums Enums can also be backed by either `i64` or `&'static str`. Those values can be set using the -`#[php(discriminant = "value")]` or `#[php(discriminant = 123)]` attributes on the enum variants. +`#[php(value = "value")]` or `#[php(value = 123)]` attributes on the enum variants. -All variants must have a discriminant of the same type, either all `i64` or all `&'static str`. +All variants must have a value of the same type, either all `i64` or all `&'static str`. ```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] @@ -55,13 +55,13 @@ use ext_php_rs::prelude::*; #[php_enum] pub enum Suit { - #[php(discriminant = "hearts")] + #[php(value = "hearts")] Hearts, - #[php(discriminant = "diamonds")] + #[php(value = "diamonds")] Diamonds, - #[php(discriminant = "clubs")] + #[php(value = "clubs")] Clubs, - #[php(discriminant = "spades")] + #[php(value = "spades")] Spades, } #[php_module] diff --git a/tests/src/integration/enum_/mod.rs b/tests/src/integration/enum_/mod.rs index 3988642268..f1e6b95110 100644 --- a/tests/src/integration/enum_/mod.rs +++ b/tests/src/integration/enum_/mod.rs @@ -15,17 +15,17 @@ pub enum TestEnum { #[php_enum] pub enum IntBackedEnum { - #[php(discriminant = 1)] + #[php(value = 1)] Variant1, - #[php(discriminant = 2)] + #[php(value = 2)] Variant2, } #[php_enum] pub enum StringBackedEnum { - #[php(discriminant = "foo")] + #[php(value = "foo")] Variant1, - #[php(discriminant = "bar")] + #[php(value = "bar")] Variant2, } From 3eedcd2b36f4abcc4022f6471f46d84a90bdfd96 Mon Sep 17 00:00:00 2001 From: Xenira <1288524+Xenira@users.noreply.github.com> Date: Sun, 20 Jul 2025 07:12:35 +0200 Subject: [PATCH 09/10] test(enum): add enum testcases Refs: #178 --- .github/workflows/coverage.yml | 6 +- crates/macros/src/lib.rs | 1 + crates/macros/tests/expand/enum.expanded.rs | 294 ++++++++++++++++++++ crates/macros/tests/expand/enum.rs | 34 +++ src/builders/enum_builder.rs | 76 ++++- src/builders/module.rs | 2 + src/enum_.rs | 24 +- 7 files changed, 432 insertions(+), 5 deletions(-) create mode 100644 crates/macros/tests/expand/enum.expanded.rs create mode 100644 crates/macros/tests/expand/enum.rs diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 09d93bfde7..e6bc17ed6c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -2,10 +2,10 @@ name: coverage on: push: branches-ignore: - - 'release-plz-*' + - "release-plz-*" pull_request: branches-ignore: - - 'release-plz-*' + - "release-plz-*" env: # increment this manually to force cache eviction RUST_CACHE_PREFIX: "v0-rust" @@ -63,6 +63,6 @@ jobs: cargo tarpaulin --version - name: Run tests run: | - cargo tarpaulin --engine llvm --workspace --all-features --tests --exclude tests --exclude-files docsrs_bindings.rs --timeout 120 --out Xml + cargo tarpaulin --engine llvm --workspace --all-features --tests --exclude tests --exclude-files docsrs_bindings.rs --exclude-files "crates/macros/tests/expand/*.expanded.rs" --timeout 120 --out Xml - name: Upload coverage uses: coverallsapp/github-action@v2 diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index dca9e07eb3..8b7cdbee04 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -1258,6 +1258,7 @@ mod tests { &[ ("php_class", php_class_internal as AttributeFn), ("php_const", php_const_internal as AttributeFn), + ("php_enum", php_enum_internal as AttributeFn), ("php_extern", php_extern_internal as AttributeFn), ("php_function", php_function_internal as AttributeFn), ("php_impl", php_impl_internal as AttributeFn), diff --git a/crates/macros/tests/expand/enum.expanded.rs b/crates/macros/tests/expand/enum.expanded.rs new file mode 100644 index 0000000000..546ad77547 --- /dev/null +++ b/crates/macros/tests/expand/enum.expanded.rs @@ -0,0 +1,294 @@ +#[macro_use] +extern crate ext_php_rs_derive; +#[allow(dead_code)] +/// Doc comments for MyEnum. +/// This is a basic enum example. +enum MyEnum { + /// Variant1 of MyEnum. + /// This variant represents the first case. + Variant1, + Variant2, + /// Variant3 of MyEnum. + Variant3, +} +impl ::ext_php_rs::class::RegisteredClass for MyEnum { + const CLASS_NAME: &'static str = "MyEnum"; + const BUILDER_MODIFIER: ::std::option::Option< + fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder, + > = None; + const EXTENDS: ::std::option::Option<::ext_php_rs::class::ClassEntryInfo> = None; + const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[]; + const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::Enum; + const DOC_COMMENTS: &'static [&'static str] = &[ + " Doc comments for MyEnum.", + " This is a basic enum example.", + ]; + fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { + static METADATA: ::ext_php_rs::class::ClassMetadata = ::ext_php_rs::class::ClassMetadata::new(); + &METADATA + } + #[inline] + fn get_properties<'a>() -> ::std::collections::HashMap< + &'static str, + ::ext_php_rs::internal::property::PropertyInfo<'a, Self>, + > { + ::std::collections::HashMap::new() + } + #[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, + > { + None + } + #[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() + } +} +impl ::ext_php_rs::enum_::RegisteredEnum for MyEnum { + const CASES: &'static [::ext_php_rs::enum_::EnumCase] = &[ + ::ext_php_rs::enum_::EnumCase { + name: "Variant1", + discriminant: None, + docs: &[" Variant1 of MyEnum.", " This variant represents the first case."], + }, + ::ext_php_rs::enum_::EnumCase { + name: "Variant_2", + discriminant: None, + docs: &[], + }, + ::ext_php_rs::enum_::EnumCase { + name: "VARIANT_3", + discriminant: None, + docs: &[" Variant3 of MyEnum."], + }, + ]; + fn from_name(name: &str) -> ::ext_php_rs::error::Result { + match name { + "Variant1" => Ok(Self::Variant1), + "Variant_2" => Ok(Self::Variant2), + "VARIANT_3" => Ok(Self::Variant3), + _ => Err(::ext_php_rs::error::Error::InvalidProperty), + } + } + fn to_name(&self) -> &'static str { + match self { + Self::Variant1 => "Variant1", + Self::Variant2 => "Variant_2", + Self::Variant3 => "VARIANT_3", + } + } +} +#[allow(dead_code)] +enum MyEnumWithIntValues { + Variant1, + Variant2, +} +impl ::ext_php_rs::class::RegisteredClass for MyEnumWithIntValues { + const CLASS_NAME: &'static str = "MyIntValuesEnum"; + const BUILDER_MODIFIER: ::std::option::Option< + fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder, + > = None; + const EXTENDS: ::std::option::Option<::ext_php_rs::class::ClassEntryInfo> = None; + const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[]; + const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::Enum; + const DOC_COMMENTS: &'static [&'static str] = &[]; + fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { + static METADATA: ::ext_php_rs::class::ClassMetadata = ::ext_php_rs::class::ClassMetadata::new(); + &METADATA + } + #[inline] + fn get_properties<'a>() -> ::std::collections::HashMap< + &'static str, + ::ext_php_rs::internal::property::PropertyInfo<'a, Self>, + > { + ::std::collections::HashMap::new() + } + #[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, + > { + None + } + #[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() + } +} +impl ::ext_php_rs::enum_::RegisteredEnum for MyEnumWithIntValues { + const CASES: &'static [::ext_php_rs::enum_::EnumCase] = &[ + ::ext_php_rs::enum_::EnumCase { + name: "Variant1", + discriminant: Some(::ext_php_rs::enum_::Discriminant::Int(1i64)), + docs: &[], + }, + ::ext_php_rs::enum_::EnumCase { + name: "Variant2", + discriminant: Some(::ext_php_rs::enum_::Discriminant::Int(42i64)), + docs: &[], + }, + ]; + fn from_name(name: &str) -> ::ext_php_rs::error::Result { + match name { + "Variant1" => Ok(Self::Variant1), + "Variant2" => Ok(Self::Variant2), + _ => Err(::ext_php_rs::error::Error::InvalidProperty), + } + } + fn to_name(&self) -> &'static str { + match self { + Self::Variant1 => "Variant1", + Self::Variant2 => "Variant2", + } + } +} +impl TryFrom for MyEnumWithIntValues { + type Error = ::ext_php_rs::error::Error; + fn try_from(value: i64) -> ::ext_php_rs::error::Result { + match value { + 1i64 => Ok(Self::Variant1), + 42i64 => Ok(Self::Variant2), + _ => Err(::ext_php_rs::error::Error::InvalidProperty), + } + } +} +impl Into for MyEnumWithIntValues { + fn into(self) -> i64 { + match self { + Self::Variant1 => 1i64, + Self::Variant2 => 42i64, + } + } +} +#[allow(dead_code)] +enum MyEnumWithStringValues { + Variant1, + Variant2, +} +impl ::ext_php_rs::class::RegisteredClass for MyEnumWithStringValues { + const CLASS_NAME: &'static str = "MY_ENUM_WITH_STRING_VALUES"; + const BUILDER_MODIFIER: ::std::option::Option< + fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder, + > = None; + const EXTENDS: ::std::option::Option<::ext_php_rs::class::ClassEntryInfo> = None; + const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[]; + const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::Enum; + const DOC_COMMENTS: &'static [&'static str] = &[]; + fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { + static METADATA: ::ext_php_rs::class::ClassMetadata = ::ext_php_rs::class::ClassMetadata::new(); + &METADATA + } + #[inline] + fn get_properties<'a>() -> ::std::collections::HashMap< + &'static str, + ::ext_php_rs::internal::property::PropertyInfo<'a, Self>, + > { + ::std::collections::HashMap::new() + } + #[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, + > { + None + } + #[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() + } +} +impl ::ext_php_rs::enum_::RegisteredEnum for MyEnumWithStringValues { + const CASES: &'static [::ext_php_rs::enum_::EnumCase] = &[ + ::ext_php_rs::enum_::EnumCase { + name: "Variant1", + discriminant: Some(::ext_php_rs::enum_::Discriminant::String("foo")), + docs: &[], + }, + ::ext_php_rs::enum_::EnumCase { + name: "Variant2", + discriminant: Some(::ext_php_rs::enum_::Discriminant::String("bar")), + docs: &[], + }, + ]; + fn from_name(name: &str) -> ::ext_php_rs::error::Result { + match name { + "Variant1" => Ok(Self::Variant1), + "Variant2" => Ok(Self::Variant2), + _ => Err(::ext_php_rs::error::Error::InvalidProperty), + } + } + fn to_name(&self) -> &'static str { + match self { + Self::Variant1 => "Variant1", + Self::Variant2 => "Variant2", + } + } +} +impl TryFrom<&str> for MyEnumWithStringValues { + type Error = ::ext_php_rs::error::Error; + fn try_from(value: &str) -> ::ext_php_rs::error::Result { + match value { + "foo" => Ok(Self::Variant1), + "bar" => Ok(Self::Variant2), + _ => Err(::ext_php_rs::error::Error::InvalidProperty), + } + } +} +impl Into<&'static str> for MyEnumWithStringValues { + fn into(self) -> &'static str { + match self { + Self::Variant1 => "foo", + Self::Variant2 => "bar", + } + } +} diff --git a/crates/macros/tests/expand/enum.rs b/crates/macros/tests/expand/enum.rs new file mode 100644 index 0000000000..50c9b77a53 --- /dev/null +++ b/crates/macros/tests/expand/enum.rs @@ -0,0 +1,34 @@ +#[macro_use] +extern crate ext_php_rs_derive; + +/// Doc comments for MyEnum. +/// This is a basic enum example. +#[php_enum] +enum MyEnum { + /// Variant1 of MyEnum. + /// This variant represents the first case. + Variant1, + #[php(name = "Variant_2")] + Variant2, + /// Variant3 of MyEnum. + #[php(change_case = "UPPER_CASE")] + Variant3, +} + +#[php_enum] +#[php(name = "MyIntValuesEnum")] +enum MyEnumWithIntValues { + #[php(value = 1)] + Variant1, + #[php(value = 42)] + Variant2, +} + +#[php_enum] +#[php(change_case = "UPPER_CASE")] +enum MyEnumWithStringValues { + #[php(value = "foo")] + Variant1, + #[php(value = "bar")] + Variant2, +} diff --git a/src/builders/enum_builder.rs b/src/builders/enum_builder.rs index 1d1ea998ac..b96111759d 100644 --- a/src/builders/enum_builder.rs +++ b/src/builders/enum_builder.rs @@ -2,6 +2,7 @@ use std::{ffi::CString, ptr}; use crate::{ builders::FunctionBuilder, + convert::IntoZval, describe::DocComments, enum_::EnumCase, error::Result, @@ -114,7 +115,10 @@ impl EnumBuilder { let name = ZendStr::new_interned(case.name, true); let value = match &case.discriminant { Some(value) => { - let value: Zval = value.try_into()?; + let value: Zval = match value { + crate::enum_::Discriminant::Int(i) => i.into_zval(false)?, + crate::enum_::Discriminant::String(s) => s.into_zval(true)?, + }; let mut zv = core::mem::ManuallyDrop::new(value); (&raw mut zv).cast() } @@ -134,3 +138,73 @@ impl EnumBuilder { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::enum_::Discriminant; + + const case1: EnumCase = EnumCase { + name: "Variant1", + discriminant: None, + docs: &[], + }; + const case2: EnumCase = EnumCase { + name: "Variant2", + discriminant: Some(Discriminant::Int(42)), + docs: &[], + }; + const case3: EnumCase = EnumCase { + name: "Variant3", + discriminant: Some(Discriminant::String("foo")), + docs: &[], + }; + + #[test] + fn test_new_enum_builder() { + let builder = EnumBuilder::new("MyEnum"); + assert_eq!(builder.name, "MyEnum"); + assert!(builder.methods.is_empty()); + assert!(builder.cases.is_empty()); + assert_eq!(builder.datatype, DataType::Undef); + assert!(builder.register.is_none()); + } + + #[test] + fn test_enum_case() { + let builder = EnumBuilder::new("MyEnum").case(&case1); + assert_eq!(builder.cases.len(), 1); + assert_eq!(builder.cases[0].name, "Variant1"); + assert_eq!(builder.datatype, DataType::Undef); + + let builder = EnumBuilder::new("MyEnum").case(&case2); + assert_eq!(builder.cases.len(), 1); + assert_eq!(builder.cases[0].name, "Variant2"); + assert_eq!(builder.cases[0].discriminant, Some(Discriminant::Int(42))); + assert_eq!(builder.datatype, DataType::Long); + + let builder = EnumBuilder::new("MyEnum").case(&case3); + assert_eq!(builder.cases.len(), 1); + assert_eq!(builder.cases[0].name, "Variant3"); + assert_eq!( + builder.cases[0].discriminant, + Some(Discriminant::String("foo")) + ); + assert_eq!(builder.datatype, DataType::String); + } + + #[test] + #[should_panic(expected = "Cannot add case with data type Long to enum with data type Undef")] + fn test_enum_case_mismatch() { + #[allow(unused_must_use)] + EnumBuilder::new("MyEnum").case(&case1).case(&case2); // This should panic because case2 has a different data type + } + + const docs: DocComments = &["This is a test enum"]; + #[test] + fn test_docs() { + let builder = EnumBuilder::new("MyEnum").docs(docs); + assert_eq!(builder.docs.len(), 1); + assert_eq!(builder.docs[0], "This is a test enum"); + } +} diff --git a/src/builders/module.rs b/src/builders/module.rs index 2a37295099..929f2cf305 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -369,6 +369,8 @@ mod tests { assert!(builder.request_shutdown_func.is_none()); assert!(builder.post_deactivate_func.is_none()); assert!(builder.info_func.is_none()); + #[cfg(feature = "enum")] + assert!(builder.enums.is_empty()); } #[test] diff --git a/src/enum_.rs b/src/enum_.rs index 8c72ec71db..0e139f2fa1 100644 --- a/src/enum_.rs +++ b/src/enum_.rs @@ -126,6 +126,7 @@ impl EnumCase { } /// Represents the discriminant of an enum case in PHP, which can be either an integer or a string. +#[derive(Debug, PartialEq, Eq)] pub enum Discriminant { /// An integer discriminant. Int(i64), @@ -139,7 +140,28 @@ impl TryFrom<&Discriminant> for Zval { fn try_from(value: &Discriminant) -> Result { match value { Discriminant::Int(i) => i.into_zval(false), - Discriminant::String(s) => s.into_zval(true), + Discriminant::String(s) => s.into_zval(false), } } } + +#[cfg(test)] +#[cfg(feature = "embed")] +mod tests { + #![allow(clippy::unwrap_used)] + use super::*; + use crate::embed::Embed; + + #[test] + fn test_zval_try_from_discriminant() { + Embed::run(|| { + let zval_int: Zval = Zval::try_from(&Discriminant::Int(42)).unwrap(); + assert!(zval_int.is_long()); + assert_eq!(zval_int.long().unwrap(), 42); + + let zval_str: Zval = Zval::try_from(&Discriminant::String("foo")).unwrap(); + assert!(zval_str.is_string()); + assert_eq!(zval_str.string().unwrap().to_string(), "foo"); + }); + } +} From 58d0763cf6787f22ca4ec3dbd4d880a74ddbd022 Mon Sep 17 00:00:00 2001 From: Xenira <1288524+Xenira@users.noreply.github.com> Date: Sun, 20 Jul 2025 16:13:52 +0200 Subject: [PATCH 10/10] refactor(enum): use raii box pattern for enum values Refs: #178 --- src/builders/enum_builder.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/builders/enum_builder.rs b/src/builders/enum_builder.rs index b96111759d..a56379cd20 100644 --- a/src/builders/enum_builder.rs +++ b/src/builders/enum_builder.rs @@ -4,7 +4,7 @@ use crate::{ builders::FunctionBuilder, convert::IntoZval, describe::DocComments, - enum_::EnumCase, + enum_::{Discriminant, EnumCase}, error::Result, ffi::{zend_enum_add_case, zend_register_internal_enum}, flags::{DataType, MethodFlags}, @@ -114,14 +114,7 @@ impl EnumBuilder { for case in self.cases { let name = ZendStr::new_interned(case.name, true); let value = match &case.discriminant { - Some(value) => { - let value: Zval = match value { - crate::enum_::Discriminant::Int(i) => i.into_zval(false)?, - crate::enum_::Discriminant::String(s) => s.into_zval(true)?, - }; - let mut zv = core::mem::ManuallyDrop::new(value); - (&raw mut zv).cast() - } + Some(value) => Self::create_enum_value(value)?, None => ptr::null_mut(), }; unsafe { @@ -137,6 +130,16 @@ impl EnumBuilder { Ok(()) } + + fn create_enum_value(discriminant: &Discriminant) -> Result<*mut Zval> { + let value: Zval = match discriminant { + Discriminant::Int(i) => i.into_zval(false)?, + Discriminant::String(s) => s.into_zval(true)?, + }; + + let boxed_value = Box::new(value); + Ok(Box::into_raw(boxed_value).cast()) + } } #[cfg(test)]