From eb345389e1c30bb3ab7ee1a6900ebce90a0a702e Mon Sep 17 00:00:00 2001 From: jmjoy Date: Sat, 5 Apr 2025 19:19:12 +0800 Subject: [PATCH 1/7] Add enum --- Cargo.lock | 12 ++ phper-build/src/lib.rs | 18 +- phper-sys/php_wrapper.c | 4 + phper/Cargo.toml | 6 + phper/src/classes.rs | 10 +- phper/src/enums.rs | 378 ++++++++++++++++++++++++++++++++++++++++ phper/src/lib.rs | 2 + phper/src/modules.rs | 22 +++ 8 files changed, 441 insertions(+), 11 deletions(-) create mode 100644 phper/src/enums.rs diff --git a/Cargo.lock b/Cargo.lock index ef1189b9..7a974670 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1003,6 +1003,7 @@ dependencies = [ "phper-build", "phper-macros", "phper-sys", + "sealed", "thiserror 2.0.11", ] @@ -1365,6 +1366,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sealed" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "security-framework" version = "2.11.1" diff --git a/phper-build/src/lib.rs b/phper-build/src/lib.rs index 76b440bf..8563e175 100644 --- a/phper-build/src/lib.rs +++ b/phper-build/src/lib.rs @@ -24,24 +24,28 @@ pub fn register_all() { pub fn register_configures() { // versions println!( - "cargo:rustc-cfg=phper_major_version=\"{}\"", + "cargo::rustc-cfg=phper_major_version=\"{}\"", PHP_MAJOR_VERSION ); println!( - "cargo:rustc-cfg=phper_minor_version=\"{}\"", + "cargo::rustc-cfg=phper_minor_version=\"{}\"", PHP_MINOR_VERSION ); println!( - "cargo:rustc-cfg=phper_release_version=\"{}\"", + "cargo::rustc-cfg=phper_release_version=\"{}\"", PHP_RELEASE_VERSION ); if PHP_DEBUG != 0 { - println!("cargo:rustc-cfg=phper_debug"); + println!("cargo::rustc-cfg=phper_debug"); } if USING_ZTS != 0 { - println!("cargo:rustc-cfg=phper_zts"); + println!("cargo::rustc-cfg=phper_zts"); + } + + if PHP_VERSION_ID >= 80100 { + println!("cargo::rustc-cfg=phper_enum_supported"); } } @@ -49,7 +53,7 @@ pub fn register_configures() { pub fn register_link_args() { #[cfg(target_os = "macos")] { - println!("cargo:rustc-link-arg=-undefined"); - println!("cargo:rustc-link-arg=dynamic_lookup"); + println!("cargo::rustc-link-arg=-undefined"); + println!("cargo::rustc-link-arg=dynamic_lookup"); } } diff --git a/phper-sys/php_wrapper.c b/phper-sys/php_wrapper.c index 5935d7da..555aaeab 100644 --- a/phper-sys/php_wrapper.c +++ b/phper-sys/php_wrapper.c @@ -22,6 +22,10 @@ #include #endif +#if PHP_VERSION_ID >= 80100 +#include +#endif + typedef ZEND_INI_MH(phper_zend_ini_mh); typedef zend_class_entry * diff --git a/phper/Cargo.toml b/phper/Cargo.toml index 6f13cd99..01ee95c1 100644 --- a/phper/Cargo.toml +++ b/phper/Cargo.toml @@ -28,6 +28,7 @@ indexmap = "2.7.1" phper-alloc = { workspace = true } phper-macros = { workspace = true } phper-sys = { workspace = true } +sealed = "0.6.0" thiserror = "2.0.11" [build-dependencies] @@ -44,4 +45,9 @@ unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(phper_minor_version, values("3"))', 'cfg(phper_minor_version, values("4"))', 'cfg(phper_zts)', + 'cfg(phper_enum_supported)', ] } + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true diff --git a/phper/src/classes.rs b/phper/src/classes.rs index 5ce729cc..ef56ddf5 100644 --- a/phper/src/classes.rs +++ b/phper/src/classes.rs @@ -233,7 +233,7 @@ fn find_global_class_entry_ptr(name: impl AsRef) -> *mut zend_class_entry { } #[derive(Clone)] -enum InnerClassEntry { +pub(crate) enum InnerClassEntry { Ptr(*const zend_class_entry), Name(String), } @@ -931,7 +931,7 @@ pub struct ConstantEntity { } impl ConstantEntity { - fn new(name: impl Into, value: impl Into) -> Self { + pub(crate) fn new(name: impl Into, value: impl Into) -> Self { Self { name: name.into(), value: value.into(), @@ -1027,7 +1027,7 @@ pub enum Visibility { pub(crate) type RawVisibility = u32; #[allow(clippy::useless_conversion)] -unsafe extern "C" fn create_object(ce: *mut zend_class_entry) -> *mut zend_object { +pub(crate) unsafe extern "C" fn create_object(ce: *mut zend_class_entry) -> *mut zend_object { unsafe { // Get real ce which hold state_constructor. let real_ce = find_real_ce(ce).unwrap(); @@ -1146,7 +1146,9 @@ unsafe fn clone_object_common(object: *mut zend_object) -> *mut zend_object { } } -unsafe fn add_class_constant(class_ce: *mut _zend_class_entry, constant: &ConstantEntity) { +pub(crate) unsafe fn add_class_constant( + class_ce: *mut _zend_class_entry, constant: &ConstantEntity, +) { let name_ptr = constant.name.as_ptr() as *const c_char; let name_len = constant.name.len(); unsafe { diff --git a/phper/src/enums.rs b/phper/src/enums.rs new file mode 100644 index 00000000..fb1df901 --- /dev/null +++ b/phper/src/enums.rs @@ -0,0 +1,378 @@ +// Copyright (c) 2022 PHPER Framework Team +// PHPER is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +//! Apis relate to PHP enum. +#![cfg(phper_enum_supported)] + +use crate::{ + classes::{ + ClassEntry, ConstantEntity, InnerClassEntry, Interface, RawVisibility, StateCloner, + StateConstructor, Visibility, add_class_constant, create_object, + }, + errors::Throwable, + functions::{Function, FunctionEntry, HandlerMap, Method, MethodEntity}, + objects::{AnyState, StateObj, ZObj}, + sys::*, + types::Scalar, + utils::ensure_end_with_zero, + values::ZVal, +}; +use sealed::sealed; +use std::{ + any::Any, + cell::RefCell, + ffi::{CStr, CString, c_char, c_void}, + fmt::Debug, + marker::PhantomData, + mem::{ManuallyDrop, size_of, zeroed}, + ops::{Deref, DerefMut}, + ptr::{null, null_mut}, + rc::Rc, +}; + +/// Trait representing a backing type for enum values. +#[sealed] +pub trait EnumBackingType: Into { + /// Returns the PHP enum type representation for this backing type. + fn enum_type() -> EnumType; +} + +#[sealed] +impl EnumBackingType for () { + fn enum_type() -> EnumType { + EnumType::Pure + } +} + +#[sealed] +impl EnumBackingType for i64 { + fn enum_type() -> EnumType { + EnumType::IntBacked + } +} + +#[sealed] +impl EnumBackingType for String { + fn enum_type() -> EnumType { + EnumType::StringBacked + } +} + +/// Enum type in PHP +pub enum EnumType { + /// Pure enum (like `enum Foo { case A, case B }`) + Pure, + /// Int backed enum (like `enum Foo: int { case A = 1, case B = 2 }`) + IntBacked, + /// String backed enum (like `enum Foo: string { case A = 'a', case B = 'b' + /// }`) + StringBacked, +} + +/// Enum case definition for PHP enum +struct EnumCase { + name: CString, + value: Scalar, +} + +/// Builder for registering an enum. +/// B is a backing type that implements EnumBackingType +pub struct EnumEntity { + enum_name: CString, + enum_type: EnumType, + method_entities: Vec, + cases: Vec, + constants: Vec, + interfaces: Vec, + bound_enum: StateEnum, + state_constructor: Rc, + _p: PhantomData<(B, *mut ())>, +} + +// Simplified constructor methods +impl EnumEntity { + /// General constructor, automatically determines enum type based on generic + /// B + pub fn new(enum_name: impl Into) -> Self { + Self::new_with_state_constructor(enum_name, || ()) + } +} + +impl EnumEntity { + /// Creates an enum entity with a custom state constructor. + /// + /// This constructor allows creation of enums with associated state data + /// that will be instantiated for each enum instance through the + /// provided state constructor function. + /// + /// # Parameters + /// + /// * `enum_name` - The name of the enum to register in PHP + /// * `state_constructor` - Function that creates the initial state for enum + /// instances + /// + /// # Returns + /// + /// Returns a new `EnumEntity` instance configured with the provided state + /// constructor + /// + /// # Example + /// + /// ``` + /// # use phper::enums::EnumEntity; + /// struct MyState { + /// counter: i32, + /// } + /// + /// let enum_entity = + /// EnumEntity::<(), MyState>::new_with_state_constructor("MyEnum", || MyState { counter: 0 }); + /// ``` + pub fn new_with_state_constructor( + enum_name: impl Into, state_constructor: impl Fn() -> T + 'static, + ) -> Self { + Self { + enum_name: ensure_end_with_zero(enum_name), + enum_type: B::enum_type(), + method_entities: Vec::new(), + cases: Vec::new(), + constants: Vec::new(), + interfaces: Vec::new(), + bound_enum: StateEnum::null(), + state_constructor: Rc::new(move || { + let state = state_constructor(); + let boxed = Box::new(state) as Box; + Box::into_raw(boxed) + }), + _p: PhantomData, + } + } + + /// Add a case to the enum with the given name and value. + /// + /// # Parameters + /// + /// * `name` - The name of the enum case + /// * `value` - The value associated with the enum case, type determined by + /// backing type B + /// + /// # Returns + /// + /// Returns `&mut Self` to allow method chaining + pub fn add_case(&mut self, name: impl Into, value: B) -> &mut Self { + self.cases.push(EnumCase { + name: ensure_end_with_zero(name), + value: value.into(), + }); + self + } + + /// Add member method to enum that can access the enum state. + pub fn add_method( + &mut self, name: impl Into, vis: Visibility, handler: F, + ) -> &mut MethodEntity + where + F: Fn(&mut StateObj, &mut [ZVal]) -> Result + 'static, + Z: Into + 'static, + E: Throwable + 'static, + { + self.method_entities.push(MethodEntity::new( + name, + Some(Rc::new(Method::new(handler))), + vis, + )); + self.method_entities.last_mut().unwrap() + } + + /// Add constant to enum + pub fn add_constant(&mut self, name: impl Into, value: impl Into) { + let constant = ConstantEntity::new(name, value); + self.constants.push(constant); + } + + /// Register enum to `implements` the interface + pub fn implements(&mut self, interface: Interface) { + self.interfaces.push(interface); + } + + /// Get the bound enum. + #[inline] + pub fn bound_enum(&self) -> StateEnum { + self.bound_enum.clone() + } + + unsafe fn function_entries(&self) -> *const zend_function_entry { + unsafe { + let mut methods = self + .method_entities + .iter() + .map(|method| FunctionEntry::from_method_entity(method)) + .collect::>(); + + methods.push(zeroed::()); + + // Store the state constructor pointer to zend_class_entry. + methods.push(self.take_state_constructor_into_function_entry()); + + // Store the state cloner pointer to zend_class_entry. + methods.push(self.take_state_cloner_into_function_entry()); + + Box::into_raw(methods.into_boxed_slice()).cast() + } + } + + pub(crate) fn handler_map(&self) -> HandlerMap { + self.method_entities + .iter() + .filter_map(|method| { + method.handler.as_ref().map(|handler| { + ( + (Some(self.enum_name.clone()), method.name.clone()), + handler.clone(), + ) + }) + }) + .collect() + } + + #[allow(clippy::useless_conversion)] + pub(crate) unsafe fn init(&self) -> *mut zend_class_entry { + unsafe { + let backing_type = match self.enum_type { + EnumType::Pure => IS_NULL, + EnumType::IntBacked => IS_LONG, + EnumType::StringBacked => IS_STRING, + } as u8; + + let class_ce = zend_register_internal_enum( + self.enum_name.as_ptr().cast(), + backing_type, + self.function_entries(), + ); + + self.bound_enum.bind(class_ce); + + for interface in &self.interfaces { + let interface_ce = interface.as_class_entry().as_ptr(); + zend_class_implements(class_ce, 1, interface_ce); + } + + for constant in &self.constants { + add_class_constant(class_ce, constant); + } + + // Register all enum cases + for case in &self.cases { + register_enum_case(class_ce, &case.name, &case.value); + } + + *phper_get_create_object(class_ce) = Some(create_object); + + class_ce + } + } + + unsafe fn take_state_constructor_into_function_entry(&self) -> zend_function_entry { + unsafe { + let mut entry = zeroed::(); + let ptr = &mut entry as *mut _ as *mut *const StateConstructor; + let state_constructor = Rc::into_raw(self.state_constructor.clone()); + ptr.write(state_constructor); + entry + } + } + + unsafe fn take_state_cloner_into_function_entry(&self) -> zend_function_entry { + unsafe { zeroed::() } + } +} + +/// Wrapper for the PHP enum that holds state +pub struct StateEnum { + inner: Rc>, + _p: PhantomData, +} + +impl StateEnum { + fn null() -> Self { + Self { + inner: Rc::new(RefCell::new(InnerClassEntry::Ptr(null()))), + _p: PhantomData, + } + } + + /// Create from name, which will be looked up from globals. + pub fn from_name(name: impl Into) -> Self { + Self { + inner: Rc::new(RefCell::new(InnerClassEntry::Name(name.into()))), + _p: PhantomData, + } + } + + fn bind(&self, ptr: *mut zend_class_entry) { + match &mut *self.inner.borrow_mut() { + InnerClassEntry::Ptr(p) => { + *p = ptr; + } + InnerClassEntry::Name(_) => { + unreachable!("Cannot bind() an StateEnum created with from_name()"); + } + } + } + + /// Converts to class entry. + pub fn as_class_entry(&self) -> &ClassEntry { + let inner = self.inner.borrow().clone(); + match inner { + InnerClassEntry::Ptr(ptr) => unsafe { ClassEntry::from_ptr(ptr) }, + InnerClassEntry::Name(name) => { + let entry = ClassEntry::from_globals(name).unwrap(); + *self.inner.borrow_mut() = InnerClassEntry::Ptr(entry.as_ptr()); + entry + } + } + } +} + +impl Clone for StateEnum { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + _p: self._p, + } + } +} + +// Helper function to register enum case +unsafe fn register_enum_case( + class_ce: *mut zend_class_entry, case_name: &CStr, case_value: &Scalar, +) { + unsafe { + match case_value { + Scalar::I64(value) => { + zend_enum_add_case_cstr( + class_ce, + case_name.as_ptr(), + ZVal::from(*value).as_mut_ptr(), + ); + } + Scalar::String(value) => { + zend_enum_add_case_cstr( + class_ce, + case_name.as_ptr(), + ZVal::from(value.clone()).as_mut_ptr(), + ); + } + Scalar::Null => { + zend_enum_add_case_cstr(class_ce, case_name.as_ptr(), null_mut()); + } + _ => unreachable!(), + }; + } +} diff --git a/phper/src/lib.rs b/phper/src/lib.rs index fb22512b..1a2800c5 100644 --- a/phper/src/lib.rs +++ b/phper/src/lib.rs @@ -11,6 +11,7 @@ #![warn(rust_2018_idioms, missing_docs)] #![warn(clippy::dbg_macro, clippy::print_stdout)] #![doc = include_str!("../README.md")] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] #[macro_use] mod macros; @@ -18,6 +19,7 @@ mod macros; pub mod arrays; pub mod classes; pub(crate) mod constants; +pub mod enums; pub mod errors; pub mod functions; pub mod ini; diff --git a/phper/src/modules.rs b/phper/src/modules.rs index f1fba874..41a82b44 100644 --- a/phper/src/modules.rs +++ b/phper/src/modules.rs @@ -13,6 +13,7 @@ use crate::{ classes::{ClassEntity, Interface, InterfaceEntity, StateClass}, constants::Constant, + enums::{EnumEntity, StateEnum}, errors::Throwable, functions::{Function, FunctionEntity, FunctionEntry, HandlerMap}, ini, @@ -61,6 +62,11 @@ unsafe extern "C" fn module_startup(_type: c_int, module_number: c_int) -> c_int module.handler_map.extend(class_entity.handler_map()); } + for enum_entity in &module.enum_entities { + let ce = enum_entity.init(); + module.handler_map.extend(enum_entity.handler_map()); + } + if let Some(f) = take(&mut module.module_init) { f(); } @@ -140,6 +146,7 @@ pub struct Module { function_entities: Vec, class_entities: Vec>, interface_entities: Vec, + enum_entities: Vec>, constants: Vec, ini_entities: Vec, infos: HashMap, @@ -163,6 +170,7 @@ impl Module { function_entities: vec![], class_entities: Default::default(), interface_entities: Default::default(), + enum_entities: Default::default(), constants: Default::default(), ini_entities: Default::default(), infos: Default::default(), @@ -219,6 +227,20 @@ impl Module { bound_interface } + /// Register enum to module. + #[cfg(phper_enum_supported)] + pub fn add_enum( + &mut self, enum_entity: crate::enums::EnumEntity, + ) -> crate::enums::StateEnum { + let bound_enum = enum_entity.bound_enum(); + self.enum_entities.push(unsafe { + transmute::, crate::enums::EnumEntity<(), ()>>( + enum_entity, + ) + }); + bound_enum + } + /// Register constant to module. pub fn add_constant(&mut self, name: impl Into, value: impl Into) { self.constants.push(Constant::new(name, value)); From 2e8f3a3eb14027c8840870294ae01dee9c174f08 Mon Sep 17 00:00:00 2001 From: jmjoy Date: Sat, 5 Apr 2025 20:13:03 +0800 Subject: [PATCH 2/7] Add enum --- phper/src/enums.rs | 115 ++++++++++++++++++++++++++++++++++++++----- phper/src/modules.rs | 4 +- 2 files changed, 105 insertions(+), 14 deletions(-) diff --git a/phper/src/enums.rs b/phper/src/enums.rs index fb1df901..4db0ac22 100644 --- a/phper/src/enums.rs +++ b/phper/src/enums.rs @@ -13,12 +13,12 @@ use crate::{ classes::{ - ClassEntry, ConstantEntity, InnerClassEntry, Interface, RawVisibility, StateCloner, - StateConstructor, Visibility, add_class_constant, create_object, + ClassEntry, ConstantEntity, InnerClassEntry, Interface, StateConstructor, Visibility, + add_class_constant, create_object, }, errors::Throwable, - functions::{Function, FunctionEntry, HandlerMap, Method, MethodEntity}, - objects::{AnyState, StateObj, ZObj}, + functions::{FunctionEntry, HandlerMap, Method, MethodEntity}, + objects::StateObj, sys::*, types::Scalar, utils::ensure_end_with_zero, @@ -28,11 +28,9 @@ use sealed::sealed; use std::{ any::Any, cell::RefCell, - ffi::{CStr, CString, c_char, c_void}, - fmt::Debug, + ffi::{CStr, CString}, marker::PhantomData, - mem::{ManuallyDrop, size_of, zeroed}, - ops::{Deref, DerefMut}, + mem::zeroed, ptr::{null, null_mut}, rc::Rc, }; @@ -82,6 +80,98 @@ struct EnumCase { value: Scalar, } +/// Reference to a specific enum case +pub struct StateEnumCase { + enum_name: CString, + case_name: CString, + bound_enum: StateEnum, + _p: PhantomData, +} + +impl StateEnumCase { + fn new(enum_name: CString, case_name: CString, bound_enum: StateEnum) -> Self { + Self { + enum_name, + case_name, + bound_enum, + _p: PhantomData, + } + } + + /// Gets the name of this enum case + pub fn name(&self) -> &CStr { + &self.case_name + } + + /// Gets the StateEnum this case belongs to + pub fn enum_type(&self) -> StateEnum { + self.bound_enum.clone() + } + + /// Gets the corresponding enum case object instance + /// + /// This requires the enum to be fully registered in PHP. + pub fn get_case_object(&self) -> crate::Result<&StateObj> { + // Get the class entry for the enum + let ce = self.bound_enum.as_class_entry(); + + unsafe { + // Find the case in the enum + let case_zval = zend_enum_get_case_cstr(ce.as_ptr() as *mut _, self.case_name.as_ptr()); + + if case_zval.is_null() { + return Err(crate::Error::boxed(format!( + "Enum case {} not found in enum {}", + self.case_name.to_string_lossy(), + self.enum_name.to_string_lossy() + ))); + } + + // Convert to StateObj + Ok(StateObj::::from_object_ptr(phper_z_obj_p( + case_zval as *const _, + ))) + } + } + + /// Gets the corresponding enum case object instance + /// + /// This requires the enum to be fully registered in PHP. + pub fn get_mut_case_object(&mut self) -> crate::Result<&mut StateObj> { + // Get the class entry for the enum + let ce = self.bound_enum.as_class_entry(); + + unsafe { + // Find the case in the enum + let case_zval = zend_enum_get_case_cstr(ce.as_ptr() as *mut _, self.case_name.as_ptr()); + + if case_zval.is_null() { + return Err(crate::Error::boxed(format!( + "Enum case {} not found in enum {}", + self.case_name.to_string_lossy(), + self.enum_name.to_string_lossy() + ))); + } + + // Convert to StateObj + Ok(StateObj::::from_mut_object_ptr(phper_z_obj_p( + case_zval as *const _, + ))) + } + } +} + +impl Clone for StateEnumCase { + fn clone(&self) -> Self { + Self { + enum_name: self.enum_name.clone(), + case_name: self.case_name.clone(), + bound_enum: self.bound_enum.clone(), + _p: PhantomData, + } + } +} + /// Builder for registering an enum. /// B is a backing type that implements EnumBackingType pub struct EnumEntity { @@ -164,13 +254,14 @@ impl EnumEntity { /// /// # Returns /// - /// Returns `&mut Self` to allow method chaining - pub fn add_case(&mut self, name: impl Into, value: B) -> &mut Self { + /// Returns a reference to the created enum case + pub fn add_case(&mut self, name: impl Into, value: B) -> StateEnumCase { + let case_name = ensure_end_with_zero(name); self.cases.push(EnumCase { - name: ensure_end_with_zero(name), + name: case_name.clone(), value: value.into(), }); - self + StateEnumCase::new(self.enum_name.clone(), case_name, self.bound_enum.clone()) } /// Add member method to enum that can access the enum state. diff --git a/phper/src/modules.rs b/phper/src/modules.rs index 41a82b44..8b49fdf8 100644 --- a/phper/src/modules.rs +++ b/phper/src/modules.rs @@ -13,7 +13,7 @@ use crate::{ classes::{ClassEntity, Interface, InterfaceEntity, StateClass}, constants::Constant, - enums::{EnumEntity, StateEnum}, + enums::EnumEntity, errors::Throwable, functions::{Function, FunctionEntity, FunctionEntry, HandlerMap}, ini, @@ -63,7 +63,7 @@ unsafe extern "C" fn module_startup(_type: c_int, module_number: c_int) -> c_int } for enum_entity in &module.enum_entities { - let ce = enum_entity.init(); + enum_entity.init(); module.handler_map.extend(enum_entity.handler_map()); } From 660f6522615ed9a73f435f411549eacebcbb9031 Mon Sep 17 00:00:00 2001 From: jmjoy Date: Sun, 6 Apr 2025 00:13:11 +0800 Subject: [PATCH 3/7] Add enum --- .../doc/_06_module/_08_register_enum/index.md | 273 ++++++++++++++ phper-doc/src/lib.rs | 3 + phper/src/classes.rs | 6 +- phper/src/enums.rs | 346 +++++------------- phper/src/modules.rs | 14 +- tests/integration/Cargo.toml | 1 + tests/integration/src/enums.rs | 66 ++++ tests/integration/src/lib.rs | 3 + tests/integration/tests/integration.rs | 2 + tests/integration/tests/php/enums.php | 53 +++ tests/integration/tests/php/phpinfo.php | 4 + 11 files changed, 503 insertions(+), 268 deletions(-) create mode 100644 phper-doc/doc/_06_module/_08_register_enum/index.md create mode 100644 tests/integration/src/enums.rs create mode 100644 tests/integration/tests/php/enums.php diff --git a/phper-doc/doc/_06_module/_08_register_enum/index.md b/phper-doc/doc/_06_module/_08_register_enum/index.md new file mode 100644 index 00000000..8b109b10 --- /dev/null +++ b/phper-doc/doc/_06_module/_08_register_enum/index.md @@ -0,0 +1,273 @@ +# Register Enums + +> PHP 8.1 and above introduced the Enum functionality. `PHPER` allowing you to create PHP enums using Rust code. + +In `PHPER`, you can use the [`add_enum`](phper::modules::Module::add_enum) method to register enums. +According to PHP's enum specification, `PHPER` supports three types of enums: + +1. Pure enums (without values) +2. Integer-backed enums +3. String-backed enums + +## Creating Pure Enums + +Pure enums are the simplest type of enum, having only member names without associated values. Use `EnumEntity<()>` to create a pure enum (or simply use `EnumEntity::new()` since `()` is the default type parameter). + +```rust,no_run +use phper::{modules::Module, php_get_module, enums::EnumEntity, classes::Visibility}; + +#[php_get_module] +pub fn get_module() -> Module { + let mut module = Module::new( + env!("CARGO_CRATE_NAME"), + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_AUTHORS"), + ); + + // Create a pure enum + let mut status = EnumEntity::new("Status"); + + // Add enum cases (without values) + status.add_case("PENDING", ()); + status.add_case("ACTIVE", ()); + status.add_case("INACTIVE", ()); + + // Register the enum to the module + module.add_enum(status); + + module +} +``` + +This is equivalent to the following PHP code: + +```php +enum Status { + case PENDING; + case ACTIVE; + case INACTIVE; +} +``` + +## Creating Integer-Backed Enums + +Integer-backed enums associate each enum member with an integer value. Use `EnumEntity` to create an integer-backed enum. + +```rust,no_run +use phper::{modules::Module, php_get_module, enums::EnumEntity}; + +#[php_get_module] +pub fn get_module() -> Module { + let mut module = Module::new( + env!("CARGO_CRATE_NAME"), + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_AUTHORS"), + ); + + // Create an integer-backed enum + let mut level = EnumEntity::::new("Level"); + + // Add enum cases with their associated integer values + level.add_case("LOW", 1); + level.add_case("MEDIUM", 5); + level.add_case("HIGH", 10); + + // Register the enum to the module + module.add_enum(level); + + module +} +``` + +This is equivalent to the following PHP code: + +```php +enum Level: int { + case LOW = 1; + case MEDIUM = 5; + case HIGH = 10; +} +``` + +## Creating String-Backed Enums + +String-backed enums associate each enum member with a string value. Use `EnumEntity` to create a string-backed enum. + +```rust,no_run +use phper::{modules::Module, php_get_module, enums::EnumEntity}; + +#[php_get_module] +pub fn get_module() -> Module { + let mut module = Module::new( + env!("CARGO_CRATE_NAME"), + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_AUTHORS"), + ); + + // Create a string-backed enum + let mut color = EnumEntity::::new("Color"); + + // Add enum cases with their associated string values + color.add_case("RED", "FF0000".to_string()); + color.add_case("GREEN", "00FF00".to_string()); + color.add_case("BLUE", "0000FF".to_string()); + + // Register the enum to the module + module.add_enum(color); + + module +} +``` + +This is equivalent to the following PHP code: + +```php +enum Color: string { + case RED = "FF0000"; + case GREEN = "00FF00"; + case BLUE = "0000FF"; +} +``` + +## Adding Constants + +Enums can contain constants. Use the `add_constant` method to add constants to an enum. + +```rust,no_run +use phper::{modules::Module, php_get_module, enums::EnumEntity}; + +let mut status = EnumEntity::new("Status"); + +// Add enum cases +status.add_case("PENDING", ()); +status.add_case("ACTIVE", ()); + +// Add constants +status.add_constant("VERSION", "1.0.0"); +status.add_constant("MAX_ATTEMPTS", 3); +``` + +This is equivalent to the following PHP code: + +```php +enum Status { + case PENDING; + case ACTIVE; + + public const VERSION = "1.0.0"; + public const MAX_ATTEMPTS = 3; +} +``` + +## Adding Static Methods + +You can add static methods to enums. Use the `add_static_method` method to add a static method to an enum. + +```rust,no_run +use phper::{modules::Module, php_get_module, enums::EnumEntity, classes::Visibility}; +use std::convert::Infallible; + +let mut status = EnumEntity::new("Status"); + +// Add enum cases +status.add_case("PENDING", ()); +status.add_case("ACTIVE", ()); + +// Add static method +status.add_static_method("getDescription", Visibility::Public, |_| { + Ok::<_, Infallible>("Status enumeration for tracking item states") +}); +``` + +This is equivalent to the following PHP code: + +```php +enum Status { + case PENDING; + case ACTIVE; + + public static function getDescription(): string { + return "Status enumeration for tracking item states"; + } +} +``` + +## Implementing Interfaces + +You can make enums implement interfaces. Use the `implements` method to make an enum implement a specific interface. + +```rust,no_run +use phper::{modules::Module, php_get_module, enums::EnumEntity, classes::Interface}; + +let mut color = EnumEntity::::new("Color"); + +// Add enum cases +color.add_case("RED", "FF0000".to_string()); +color.add_case("GREEN", "00FF00".to_string()); + +// Implement interface +color.implements(Interface::from_name("JsonSerializable")); + +// Note: You need to add necessary methods to satisfy interface requirements +// For example, JsonSerializable interface requires the implementation of jsonSerialize method +``` + +## Using Built-in Enum Methods + +PHP enums come with some built-in methods. Pure enums (`UnitEnum`) have the `cases()` method, while backed enums (`BackedEnum`) additionally have the `from()` and `tryFrom()` methods. + +```php +// Examples of using built-in methods in PHP +$allCases = Color::cases(); // Returns an array of all enum cases + +// Only available for backed enums +$colorFromValue = Color::from("FF0000"); // Returns RED +$colorOrNull = Color::tryFrom("INVALID"); // Returns null (when the value doesn't exist) +``` + +## Complete Example + +Here's a comprehensive example using both pure and backed enums: + +```rust,no_run +use phper::{ + modules::Module, + php_get_module, + enums::EnumEntity, + classes::Visibility +}; +use std::convert::Infallible; + +#[php_get_module] +pub fn get_module() -> Module { + let mut module = Module::new( + env!("CARGO_CRATE_NAME"), + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_AUTHORS"), + ); + + // Pure enum + let mut status = EnumEntity::new("Status"); + status.add_case("PENDING", ()); + status.add_case("ACTIVE", ()); + status.add_case("INACTIVE", ()); + status.add_constant("VERSION", "1.0.0"); + status.add_static_method("getDescription", Visibility::Public, |_| { + Ok::<_, Infallible>("Status enumeration") + }); + + // Integer-backed enum + let mut level = EnumEntity::::new("Level"); + level.add_case("LOW", 1); + level.add_case("MEDIUM", 5); + level.add_case("HIGH", 10); + + // Register enums to the module + module.add_enum(status); + module.add_enum(level); + + module +} +``` + +> **Note**: PHP enums require PHP 8.1 or higher. Make sure your extension sets the correct PHP version requirements. diff --git a/phper-doc/src/lib.rs b/phper-doc/src/lib.rs index 24cbadd9..e534def5 100644 --- a/phper-doc/src/lib.rs +++ b/phper-doc/src/lib.rs @@ -69,6 +69,9 @@ pub mod _06_module { #[doc = include_str!("../doc/_06_module/_07_register_interface/index.md")] pub mod _07_register_interface {} + + #[doc = include_str!("../doc/_06_module/_08_register_enum/index.md")] + pub mod _08_register_enum {} } /// TODO diff --git a/phper/src/classes.rs b/phper/src/classes.rs index ef56ddf5..4dbb769e 100644 --- a/phper/src/classes.rs +++ b/phper/src/classes.rs @@ -940,14 +940,16 @@ impl ConstantEntity { } /// Builder for declare class property. -struct PropertyEntity { +pub(crate) struct PropertyEntity { name: String, visibility: RawVisibility, value: Scalar, } impl PropertyEntity { - fn new(name: impl Into, visibility: Visibility, value: impl Into) -> Self { + pub(crate) fn new( + name: impl Into, visibility: Visibility, value: impl Into, + ) -> Self { Self { name: name.into(), visibility: visibility as RawVisibility, diff --git a/phper/src/enums.rs b/phper/src/enums.rs index 4db0ac22..bb5a7dae 100644 --- a/phper/src/enums.rs +++ b/phper/src/enums.rs @@ -8,17 +8,21 @@ // NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. // See the Mulan PSL v2 for more details. -//! Apis relate to PHP enum. +//! APIs related to PHP enum functionality. +//! +//! This module provides Rust wrappers for PHP enum functionality, allowing you +//! to define and work with PHP enums from Rust code. It supports pure enums, +//! integer-backed enums, and string-backed enums, corresponding to their PHP +//! counterparts. +//! +//! The implementation respects the PHP 8.1+ enum feature set and provides a +//! type-safe interface for creating enum cases and handling enum values. #![cfg(phper_enum_supported)] use crate::{ - classes::{ - ClassEntry, ConstantEntity, InnerClassEntry, Interface, StateConstructor, Visibility, - add_class_constant, create_object, - }, + classes::{ConstantEntity, Interface, Visibility, add_class_constant}, errors::Throwable, - functions::{FunctionEntry, HandlerMap, Method, MethodEntity}, - objects::StateObj, + functions::{Function, FunctionEntry, HandlerMap, MethodEntity}, sys::*, types::Scalar, utils::ensure_end_with_zero, @@ -26,16 +30,18 @@ use crate::{ }; use sealed::sealed; use std::{ - any::Any, - cell::RefCell, ffi::{CStr, CString}, marker::PhantomData, mem::zeroed, - ptr::{null, null_mut}, + ptr::null_mut, rc::Rc, }; /// Trait representing a backing type for enum values. +/// +/// This trait is implemented by types that can serve as backing values +/// for PHP enums. The trait is sealed to ensure only supported types +/// can be used as enum backing types. #[sealed] pub trait EnumBackingType: Into { /// Returns the PHP enum type representation for this backing type. @@ -63,7 +69,12 @@ impl EnumBackingType for String { } } -/// Enum type in PHP +/// Enum type in PHP. +/// +/// Represents the three possible types of PHP enums: +/// - Pure enums (no backing value) +/// - Integer-backed enums +/// - String-backed enums pub enum EnumType { /// Pure enum (like `enum Foo { case A, case B }`) Pure, @@ -74,159 +85,47 @@ pub enum EnumType { StringBacked, } -/// Enum case definition for PHP enum +/// Enum case definition for PHP enum. +/// +/// Represents a single case within a PHP enum, storing its name +/// and associated value. struct EnumCase { name: CString, value: Scalar, } -/// Reference to a specific enum case -pub struct StateEnumCase { - enum_name: CString, - case_name: CString, - bound_enum: StateEnum, - _p: PhantomData, -} - -impl StateEnumCase { - fn new(enum_name: CString, case_name: CString, bound_enum: StateEnum) -> Self { - Self { - enum_name, - case_name, - bound_enum, - _p: PhantomData, - } - } - - /// Gets the name of this enum case - pub fn name(&self) -> &CStr { - &self.case_name - } - - /// Gets the StateEnum this case belongs to - pub fn enum_type(&self) -> StateEnum { - self.bound_enum.clone() - } - - /// Gets the corresponding enum case object instance - /// - /// This requires the enum to be fully registered in PHP. - pub fn get_case_object(&self) -> crate::Result<&StateObj> { - // Get the class entry for the enum - let ce = self.bound_enum.as_class_entry(); - - unsafe { - // Find the case in the enum - let case_zval = zend_enum_get_case_cstr(ce.as_ptr() as *mut _, self.case_name.as_ptr()); - - if case_zval.is_null() { - return Err(crate::Error::boxed(format!( - "Enum case {} not found in enum {}", - self.case_name.to_string_lossy(), - self.enum_name.to_string_lossy() - ))); - } - - // Convert to StateObj - Ok(StateObj::::from_object_ptr(phper_z_obj_p( - case_zval as *const _, - ))) - } - } - - /// Gets the corresponding enum case object instance - /// - /// This requires the enum to be fully registered in PHP. - pub fn get_mut_case_object(&mut self) -> crate::Result<&mut StateObj> { - // Get the class entry for the enum - let ce = self.bound_enum.as_class_entry(); - - unsafe { - // Find the case in the enum - let case_zval = zend_enum_get_case_cstr(ce.as_ptr() as *mut _, self.case_name.as_ptr()); - - if case_zval.is_null() { - return Err(crate::Error::boxed(format!( - "Enum case {} not found in enum {}", - self.case_name.to_string_lossy(), - self.enum_name.to_string_lossy() - ))); - } - - // Convert to StateObj - Ok(StateObj::::from_mut_object_ptr(phper_z_obj_p( - case_zval as *const _, - ))) - } - } -} - -impl Clone for StateEnumCase { - fn clone(&self) -> Self { - Self { - enum_name: self.enum_name.clone(), - case_name: self.case_name.clone(), - bound_enum: self.bound_enum.clone(), - _p: PhantomData, - } - } -} - -/// Builder for registering an enum. -/// B is a backing type that implements EnumBackingType -pub struct EnumEntity { +/// Builder for registering a PHP enum. +/// +/// This struct facilitates the creation and registration of PHP enums from Rust +/// code. The generic parameter B represents the backing type and determines the +/// enum type. +/// +/// # Type Parameters +/// +/// * `B` - A type that implements `EnumBackingType`, determining the enum's +/// backing type. Use `()` for pure enums, `i64` for int-backed enums, or +/// `String` for string-backed enums. +pub struct EnumEntity { enum_name: CString, enum_type: EnumType, method_entities: Vec, cases: Vec, constants: Vec, interfaces: Vec, - bound_enum: StateEnum, - state_constructor: Rc, _p: PhantomData<(B, *mut ())>, } -// Simplified constructor methods -impl EnumEntity { - /// General constructor, automatically determines enum type based on generic - /// B - pub fn new(enum_name: impl Into) -> Self { - Self::new_with_state_constructor(enum_name, || ()) - } -} - -impl EnumEntity { - /// Creates an enum entity with a custom state constructor. - /// - /// This constructor allows creation of enums with associated state data - /// that will be instantiated for each enum instance through the - /// provided state constructor function. +impl EnumEntity { + /// Creates a new enum builder with the specified name. /// /// # Parameters /// - /// * `enum_name` - The name of the enum to register in PHP - /// * `state_constructor` - Function that creates the initial state for enum - /// instances + /// * `enum_name` - The name of the PHP enum to create /// /// # Returns /// - /// Returns a new `EnumEntity` instance configured with the provided state - /// constructor - /// - /// # Example - /// - /// ``` - /// # use phper::enums::EnumEntity; - /// struct MyState { - /// counter: i32, - /// } - /// - /// let enum_entity = - /// EnumEntity::<(), MyState>::new_with_state_constructor("MyEnum", || MyState { counter: 0 }); - /// ``` - pub fn new_with_state_constructor( - enum_name: impl Into, state_constructor: impl Fn() -> T + 'static, - ) -> Self { + /// A new `EnumEntity` instance configured for the specified enum type + pub fn new(enum_name: impl Into) -> Self { Self { enum_name: ensure_end_with_zero(enum_name), enum_type: B::enum_type(), @@ -234,12 +133,6 @@ impl EnumEntity { cases: Vec::new(), constants: Vec::new(), interfaces: Vec::new(), - bound_enum: StateEnum::null(), - state_constructor: Rc::new(move || { - let state = state_constructor(); - let boxed = Box::new(state) as Box; - Box::into_raw(boxed) - }), _p: PhantomData, } } @@ -251,53 +144,61 @@ impl EnumEntity { /// * `name` - The name of the enum case /// * `value` - The value associated with the enum case, type determined by /// backing type B - /// - /// # Returns - /// - /// Returns a reference to the created enum case - pub fn add_case(&mut self, name: impl Into, value: B) -> StateEnumCase { + pub fn add_case(&mut self, name: impl Into, value: B) { let case_name = ensure_end_with_zero(name); self.cases.push(EnumCase { name: case_name.clone(), value: value.into(), }); - StateEnumCase::new(self.enum_name.clone(), case_name, self.bound_enum.clone()) } - /// Add member method to enum that can access the enum state. - pub fn add_method( + /// Adds a static method to the enum. + /// + /// # Parameters + /// + /// * `name` - The name of the method + /// * `vis` - The visibility of the method (public, protected, or private) + /// * `handler` - The function that implements the method logic + /// + /// # Returns + /// + /// A mutable reference to the created `MethodEntity` for further + /// configuration + pub fn add_static_method( &mut self, name: impl Into, vis: Visibility, handler: F, ) -> &mut MethodEntity where - F: Fn(&mut StateObj, &mut [ZVal]) -> Result + 'static, + F: Fn(&mut [ZVal]) -> Result + 'static, Z: Into + 'static, E: Throwable + 'static, { - self.method_entities.push(MethodEntity::new( - name, - Some(Rc::new(Method::new(handler))), - vis, - )); + let mut entity = MethodEntity::new(name, Some(Rc::new(Function::new(handler))), vis); + entity.set_vis_static(); + self.method_entities.push(entity); self.method_entities.last_mut().unwrap() } - /// Add constant to enum + /// Adds a constant to the enum. + /// + /// # Parameters + /// + /// * `name` - The name of the constant + /// * `value` - The value of the constant, which will be converted to a + /// Scalar pub fn add_constant(&mut self, name: impl Into, value: impl Into) { let constant = ConstantEntity::new(name, value); self.constants.push(constant); } - /// Register enum to `implements` the interface + /// Registers the enum to implement the specified interface. + /// + /// # Parameters + /// + /// * `interface` - The interface that the enum should implement pub fn implements(&mut self, interface: Interface) { self.interfaces.push(interface); } - /// Get the bound enum. - #[inline] - pub fn bound_enum(&self) -> StateEnum { - self.bound_enum.clone() - } - unsafe fn function_entries(&self) -> *const zend_function_entry { unsafe { let mut methods = self @@ -308,12 +209,6 @@ impl EnumEntity { methods.push(zeroed::()); - // Store the state constructor pointer to zend_class_entry. - methods.push(self.take_state_constructor_into_function_entry()); - - // Store the state cloner pointer to zend_class_entry. - methods.push(self.take_state_cloner_into_function_entry()); - Box::into_raw(methods.into_boxed_slice()).cast() } } @@ -347,8 +242,6 @@ impl EnumEntity { self.function_entries(), ); - self.bound_enum.bind(class_ce); - for interface in &self.interfaces { let interface_ce = interface.as_class_entry().as_ptr(); zend_class_implements(class_ce, 1, interface_ce); @@ -363,84 +256,18 @@ impl EnumEntity { register_enum_case(class_ce, &case.name, &case.value); } - *phper_get_create_object(class_ce) = Some(create_object); - class_ce } } - - unsafe fn take_state_constructor_into_function_entry(&self) -> zend_function_entry { - unsafe { - let mut entry = zeroed::(); - let ptr = &mut entry as *mut _ as *mut *const StateConstructor; - let state_constructor = Rc::into_raw(self.state_constructor.clone()); - ptr.write(state_constructor); - entry - } - } - - unsafe fn take_state_cloner_into_function_entry(&self) -> zend_function_entry { - unsafe { zeroed::() } - } -} - -/// Wrapper for the PHP enum that holds state -pub struct StateEnum { - inner: Rc>, - _p: PhantomData, -} - -impl StateEnum { - fn null() -> Self { - Self { - inner: Rc::new(RefCell::new(InnerClassEntry::Ptr(null()))), - _p: PhantomData, - } - } - - /// Create from name, which will be looked up from globals. - pub fn from_name(name: impl Into) -> Self { - Self { - inner: Rc::new(RefCell::new(InnerClassEntry::Name(name.into()))), - _p: PhantomData, - } - } - - fn bind(&self, ptr: *mut zend_class_entry) { - match &mut *self.inner.borrow_mut() { - InnerClassEntry::Ptr(p) => { - *p = ptr; - } - InnerClassEntry::Name(_) => { - unreachable!("Cannot bind() an StateEnum created with from_name()"); - } - } - } - - /// Converts to class entry. - pub fn as_class_entry(&self) -> &ClassEntry { - let inner = self.inner.borrow().clone(); - match inner { - InnerClassEntry::Ptr(ptr) => unsafe { ClassEntry::from_ptr(ptr) }, - InnerClassEntry::Name(name) => { - let entry = ClassEntry::from_globals(name).unwrap(); - *self.inner.borrow_mut() = InnerClassEntry::Ptr(entry.as_ptr()); - entry - } - } - } } -impl Clone for StateEnum { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - _p: self._p, - } - } -} - -// Helper function to register enum case +/// Helper function to register an enum case with the PHP engine. +/// +/// # Parameters +/// +/// * `class_ce` - Pointer to the class entry +/// * `case_name` - Name of the enum case +/// * `case_value` - Value associated with the case unsafe fn register_enum_case( class_ce: *mut zend_class_entry, case_name: &CStr, case_value: &Scalar, ) { @@ -454,11 +281,16 @@ unsafe fn register_enum_case( ); } Scalar::String(value) => { - zend_enum_add_case_cstr( - class_ce, - case_name.as_ptr(), - ZVal::from(value.clone()).as_mut_ptr(), + #[allow(clippy::useless_conversion)] + let value_ptr = phper_zend_string_init( + value.as_ptr().cast(), + value.len().try_into().unwrap(), + true.into(), ); + let mut value = ZVal::from(()); + phper_zval_str(value.as_mut_ptr(), value_ptr); + + zend_enum_add_case_cstr(class_ce, case_name.as_ptr(), value.as_mut_ptr()); } Scalar::Null => { zend_enum_add_case_cstr(class_ce, case_name.as_ptr(), null_mut()); diff --git a/phper/src/modules.rs b/phper/src/modules.rs index 8b49fdf8..5f95cb42 100644 --- a/phper/src/modules.rs +++ b/phper/src/modules.rs @@ -146,7 +146,7 @@ pub struct Module { function_entities: Vec, class_entities: Vec>, interface_entities: Vec, - enum_entities: Vec>, + enum_entities: Vec>, constants: Vec, ini_entities: Vec, infos: HashMap, @@ -229,16 +229,12 @@ impl Module { /// Register enum to module. #[cfg(phper_enum_supported)] - pub fn add_enum( - &mut self, enum_entity: crate::enums::EnumEntity, - ) -> crate::enums::StateEnum { - let bound_enum = enum_entity.bound_enum(); + pub fn add_enum( + &mut self, enum_entity: crate::enums::EnumEntity, + ) { self.enum_entities.push(unsafe { - transmute::, crate::enums::EnumEntity<(), ()>>( - enum_entity, - ) + transmute::, crate::enums::EnumEntity<()>>(enum_entity) }); - bound_enum } /// Register constant to module. diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index 039e552c..bbc0a29b 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -33,4 +33,5 @@ phper-build = { workspace = true } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(phper_major_version, values("8"))', + 'cfg(phper_enum_supported)', ] } diff --git a/tests/integration/src/enums.rs b/tests/integration/src/enums.rs new file mode 100644 index 00000000..0f6fc3e9 --- /dev/null +++ b/tests/integration/src/enums.rs @@ -0,0 +1,66 @@ +// Copyright (c) 2022 PHPER Framework Team +// PHPER is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +#![cfg(phper_enum_supported)] + +use phper::{classes::Visibility, enums::EnumEntity, modules::Module}; +use std::convert::Infallible; + +pub fn integrate(module: &mut Module) { + // Create pure enum (without backing type) + create_pure_enum(module); + + // Create int-backed enum + create_int_backed_enum(module); + + // Create string-backed enum + create_string_backed_enum(module); +} + +fn create_pure_enum(module: &mut Module) { + let mut enum_entity = EnumEntity::new("IntegrationTest\\PureEnum"); + + // Add enum cases + enum_entity.add_case("ONE", ()); + enum_entity.add_case("TWO", ()); + enum_entity.add_case("THREE", ()); + + // Add constants + enum_entity.add_constant("VERSION", "1.0.0"); + + // Add static method + enum_entity.add_static_method("getDescription", Visibility::Public, |_| { + Ok::<_, Infallible>("Pure enum implementation") + }); + + module.add_enum(enum_entity); +} + +fn create_int_backed_enum(module: &mut Module) { + let mut enum_entity = EnumEntity::::new("IntegrationTest\\IntEnum"); + + // Add enum cases with integer values + enum_entity.add_case("LOW", 1); + enum_entity.add_case("MEDIUM", 5); + enum_entity.add_case("HIGH", 10); + + module.add_enum(enum_entity); +} + +fn create_string_backed_enum(module: &mut Module) { + let mut enum_entity = EnumEntity::::new("IntegrationTest\\StringEnum"); + + // Add enum cases with string values + enum_entity.add_case("RED", "FF0000".to_string()); + enum_entity.add_case("GREEN", "00FF00".to_string()); + enum_entity.add_case("BLUE", "0000FF".to_string()); + + module.add_enum(enum_entity); +} diff --git a/tests/integration/src/lib.rs b/tests/integration/src/lib.rs index 6c9292e0..72a60921 100644 --- a/tests/integration/src/lib.rs +++ b/tests/integration/src/lib.rs @@ -14,6 +14,7 @@ mod arguments; mod arrays; mod classes; mod constants; +mod enums; mod errors; mod functions; mod ini; @@ -45,6 +46,8 @@ pub fn get_module() -> Module { errors::integrate(&mut module); references::integrate(&mut module); typehints::integrate(&mut module); + #[cfg(phper_enum_supported)] + enums::integrate(&mut module); module } diff --git a/tests/integration/tests/integration.rs b/tests/integration/tests/integration.rs index f6c5bbf8..7abe2a66 100644 --- a/tests/integration/tests/integration.rs +++ b/tests/integration/tests/integration.rs @@ -43,6 +43,7 @@ fn test_cli() { &tests_php_dir.join("errors.php"), &tests_php_dir.join("reflection.php"), &tests_php_dir.join("typehints.php"), + &tests_php_dir.join("enums.php"), ], ); } @@ -70,4 +71,5 @@ fn test_fpm() { test_fpm_request("GET", &tests_php_dir, "/values.php", None, None); test_fpm_request("GET", &tests_php_dir, "/constants.php", None, None); test_fpm_request("GET", &tests_php_dir, "/ini.php", None, None); + test_fpm_request("GET", &tests_php_dir, "/enums.php", None, None); } diff --git a/tests/integration/tests/php/enums.php b/tests/integration/tests/php/enums.php new file mode 100644 index 00000000..98397492 --- /dev/null +++ b/tests/integration/tests/php/enums.php @@ -0,0 +1,53 @@ +name, 'ONE'); +assert_eq(IntegrationTest\PureEnum::TWO->name, 'TWO'); +assert_eq(IntegrationTest\PureEnum::THREE->name, 'THREE'); + +// Test int-backed enum +assert_true(enum_exists('IntegrationTest\IntEnum'), 'IntEnum should exist'); +assert_eq(IntegrationTest\IntEnum::LOW->value, 1, 'IntEnum::LOW value should be 1'); +assert_eq(IntegrationTest\IntEnum::MEDIUM->value, 5, 'IntEnum::MEDIUM value should be 5'); +assert_eq(IntegrationTest\IntEnum::HIGH->value, 10, 'IntEnum::HIGH value should be 10'); + +// Test string-backed enum +assert_true(enum_exists('IntegrationTest\StringEnum'), 'StringEnum should exist'); +assert_eq(IntegrationTest\StringEnum::RED->value, 'FF0000', 'StringEnum::RED value should be FF0000'); +assert_eq(IntegrationTest\StringEnum::GREEN->value, '00FF00', 'StringEnum::GREEN value should be 00FF00'); +assert_eq(IntegrationTest\StringEnum::BLUE->value, '0000FF', 'StringEnum::BLUE value should be 0000FF'); + +// Test reflection API +$reflection = new ReflectionEnum(IntegrationTest\StringEnum::class); +assert_true($reflection->isBacked(), 'StringEnum should be a backed enum'); +assert_true($reflection->hasCase('RED'), 'StringEnum should have a RED case'); +assert_true($reflection->hasCase('GREEN'), 'StringEnum should have a GREEN case'); +assert_true($reflection->hasCase('BLUE'), 'StringEnum should have a BLUE case'); diff --git a/tests/integration/tests/php/phpinfo.php b/tests/integration/tests/php/phpinfo.php index d3e1bfdf..44fae0cd 100644 --- a/tests/integration/tests/php/phpinfo.php +++ b/tests/integration/tests/php/phpinfo.php @@ -10,4 +10,8 @@ // NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. // See the Mulan PSL v2 for more details. +ob_start(); phpinfo(); +$output = ob_get_contents(); +ob_end_clean(); +echo substr($output, 0, 100) . '...'; From 39435bc02a9fe3f9b23e942fe019fbe291c0f5d3 Mon Sep 17 00:00:00 2001 From: jmjoy Date: Sun, 6 Apr 2025 00:30:56 +0800 Subject: [PATCH 4/7] Adapt --- Cargo.lock | 1 + phper-doc/Cargo.toml | 8 ++++++++ phper-doc/src/lib.rs | 1 + phper/src/modules.rs | 6 ++++-- tests/integration/tests/php/enums.php | 18 +++++++++--------- 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7a974670..5692330f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1027,6 +1027,7 @@ name = "phper-doc" version = "0.15.1" dependencies = [ "phper", + "phper-build", "reqwest", "thiserror 2.0.11", ] diff --git a/phper-doc/Cargo.toml b/phper-doc/Cargo.toml index 983d5153..fb9a983a 100644 --- a/phper-doc/Cargo.toml +++ b/phper-doc/Cargo.toml @@ -25,3 +25,11 @@ phper = { workspace = true } [dev-dependencies] thiserror = "2.0.11" reqwest = { version = "0.12.12", features = ["blocking", "cookies"] } + +[build-dependencies] +phper-build = { workspace = true } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(phper_enum_supported)', +] } diff --git a/phper-doc/src/lib.rs b/phper-doc/src/lib.rs index e534def5..555a8aa3 100644 --- a/phper-doc/src/lib.rs +++ b/phper-doc/src/lib.rs @@ -70,6 +70,7 @@ pub mod _06_module { #[doc = include_str!("../doc/_06_module/_07_register_interface/index.md")] pub mod _07_register_interface {} + #[cfg(phper_enum_supported)] #[doc = include_str!("../doc/_06_module/_08_register_enum/index.md")] pub mod _08_register_enum {} } diff --git a/phper/src/modules.rs b/phper/src/modules.rs index 5f95cb42..0c3dd1bf 100644 --- a/phper/src/modules.rs +++ b/phper/src/modules.rs @@ -13,7 +13,6 @@ use crate::{ classes::{ClassEntity, Interface, InterfaceEntity, StateClass}, constants::Constant, - enums::EnumEntity, errors::Throwable, functions::{Function, FunctionEntity, FunctionEntry, HandlerMap}, ini, @@ -62,6 +61,7 @@ unsafe extern "C" fn module_startup(_type: c_int, module_number: c_int) -> c_int module.handler_map.extend(class_entity.handler_map()); } + #[cfg(phper_enum_supported)] for enum_entity in &module.enum_entities { enum_entity.init(); module.handler_map.extend(enum_entity.handler_map()); @@ -146,7 +146,8 @@ pub struct Module { function_entities: Vec, class_entities: Vec>, interface_entities: Vec, - enum_entities: Vec>, + #[cfg(phper_enum_supported)] + enum_entities: Vec>, constants: Vec, ini_entities: Vec, infos: HashMap, @@ -170,6 +171,7 @@ impl Module { function_entities: vec![], class_entities: Default::default(), interface_entities: Default::default(), + #[cfg(phper_enum_supported)] enum_entities: Default::default(), constants: Default::default(), ini_entities: Default::default(), diff --git a/tests/integration/tests/php/enums.php b/tests/integration/tests/php/enums.php index 98397492..a45a806c 100644 --- a/tests/integration/tests/php/enums.php +++ b/tests/integration/tests/php/enums.php @@ -29,21 +29,21 @@ assert_eq(IntegrationTest\PureEnum::getDescription(), 'Pure enum implementation', 'PureEnum::getDescription() should return proper value'); // Test direct access to enum members -assert_eq(IntegrationTest\PureEnum::ONE->name, 'ONE'); -assert_eq(IntegrationTest\PureEnum::TWO->name, 'TWO'); -assert_eq(IntegrationTest\PureEnum::THREE->name, 'THREE'); +assert_eq((IntegrationTest\PureEnum::ONE)->name, 'ONE'); +assert_eq((IntegrationTest\PureEnum::TWO)->name, 'TWO'); +assert_eq((IntegrationTest\PureEnum::THREE)->name, 'THREE'); // Test int-backed enum assert_true(enum_exists('IntegrationTest\IntEnum'), 'IntEnum should exist'); -assert_eq(IntegrationTest\IntEnum::LOW->value, 1, 'IntEnum::LOW value should be 1'); -assert_eq(IntegrationTest\IntEnum::MEDIUM->value, 5, 'IntEnum::MEDIUM value should be 5'); -assert_eq(IntegrationTest\IntEnum::HIGH->value, 10, 'IntEnum::HIGH value should be 10'); +assert_eq((IntegrationTest\IntEnum::LOW)->value, 1, 'IntEnum::LOW value should be 1'); +assert_eq((IntegrationTest\IntEnum::MEDIUM)->value, 5, 'IntEnum::MEDIUM value should be 5'); +assert_eq((IntegrationTest\IntEnum::HIGH)->value, 10, 'IntEnum::HIGH value should be 10'); // Test string-backed enum assert_true(enum_exists('IntegrationTest\StringEnum'), 'StringEnum should exist'); -assert_eq(IntegrationTest\StringEnum::RED->value, 'FF0000', 'StringEnum::RED value should be FF0000'); -assert_eq(IntegrationTest\StringEnum::GREEN->value, '00FF00', 'StringEnum::GREEN value should be 00FF00'); -assert_eq(IntegrationTest\StringEnum::BLUE->value, '0000FF', 'StringEnum::BLUE value should be 0000FF'); +assert_eq((IntegrationTest\StringEnum::RED)->value, 'FF0000', 'StringEnum::RED value should be FF0000'); +assert_eq((IntegrationTest\StringEnum::GREEN)->value, '00FF00', 'StringEnum::GREEN value should be 00FF00'); +assert_eq((IntegrationTest\StringEnum::BLUE)->value, '0000FF', 'StringEnum::BLUE value should be 0000FF'); // Test reflection API $reflection = new ReflectionEnum(IntegrationTest\StringEnum::class); From 75e3479f8f703b7d63b8e82269976a5036ad4993 Mon Sep 17 00:00:00 2001 From: jmjoy Date: Sun, 6 Apr 2025 08:02:05 +0800 Subject: [PATCH 5/7] Fix --- phper/src/modules.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phper/src/modules.rs b/phper/src/modules.rs index 0c3dd1bf..469a3a42 100644 --- a/phper/src/modules.rs +++ b/phper/src/modules.rs @@ -147,7 +147,7 @@ pub struct Module { class_entities: Vec>, interface_entities: Vec, #[cfg(phper_enum_supported)] - enum_entities: Vec>, + enum_entities: Vec>, constants: Vec, ini_entities: Vec, infos: HashMap, From e4b39d68d56428a7f63f41b77617028a31508244 Mon Sep 17 00:00:00 2001 From: jmjoy Date: Sun, 6 Apr 2025 08:11:15 +0800 Subject: [PATCH 6/7] Use MaybeUninit for string --- phper/src/enums.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phper/src/enums.rs b/phper/src/enums.rs index bb5a7dae..34a5f06a 100644 --- a/phper/src/enums.rs +++ b/phper/src/enums.rs @@ -32,7 +32,7 @@ use sealed::sealed; use std::{ ffi::{CStr, CString}, marker::PhantomData, - mem::zeroed, + mem::{MaybeUninit, zeroed}, ptr::null_mut, rc::Rc, }; @@ -287,7 +287,7 @@ unsafe fn register_enum_case( value.len().try_into().unwrap(), true.into(), ); - let mut value = ZVal::from(()); + let mut value = MaybeUninit::::uninit(); phper_zval_str(value.as_mut_ptr(), value_ptr); zend_enum_add_case_cstr(class_ce, case_name.as_ptr(), value.as_mut_ptr()); From ce2357c2be061443f8e5b3fb71236c2f012a2707 Mon Sep 17 00:00:00 2001 From: jmjoy Date: Sun, 6 Apr 2025 12:10:49 +0800 Subject: [PATCH 7/7] Adjust the documentation of the enum --- phper-doc/Cargo.toml | 4 ++++ phper-doc/build.rs | 13 +++++++++++++ phper-doc/src/lib.rs | 1 + 3 files changed, 18 insertions(+) create mode 100644 phper-doc/build.rs diff --git a/phper-doc/Cargo.toml b/phper-doc/Cargo.toml index fb9a983a..9aacebd7 100644 --- a/phper-doc/Cargo.toml +++ b/phper-doc/Cargo.toml @@ -33,3 +33,7 @@ phper-build = { workspace = true } unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(phper_enum_supported)', ] } + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true diff --git a/phper-doc/build.rs b/phper-doc/build.rs new file mode 100644 index 00000000..a024aa00 --- /dev/null +++ b/phper-doc/build.rs @@ -0,0 +1,13 @@ +// Copyright (c) 2022 PHPER Framework Team +// PHPER is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +fn main() { + phper_build::register_all(); +} diff --git a/phper-doc/src/lib.rs b/phper-doc/src/lib.rs index 555a8aa3..756c1787 100644 --- a/phper-doc/src/lib.rs +++ b/phper-doc/src/lib.rs @@ -11,6 +11,7 @@ #![warn(rust_2018_idioms, missing_docs)] #![warn(clippy::dbg_macro, clippy::print_stdout)] #![doc = include_str!("../README.md")] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] pub use phper;