From 27d96cdbc6504292592ea93b7f650b2a5fb7b8fa Mon Sep 17 00:00:00 2001 From: norbytus Date: Thu, 10 Jul 2025 21:25:04 +0300 Subject: [PATCH 01/25] feat: Add interface bindings --- allowed_bindings.rs | 1 + docsrs_bindings.rs | 5 +++++ src/builders/class.rs | 38 +++++++++++++++++++++++++++----------- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/allowed_bindings.rs b/allowed_bindings.rs index 81a2a78c6..573501182 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -118,6 +118,7 @@ bind! { zend_register_double_constant, zend_register_ini_entries, zend_register_internal_enum, + zend_register_internal_interface, zend_ini_entry_def, zend_register_internal_class_ex, zend_register_long_constant, diff --git a/docsrs_bindings.rs b/docsrs_bindings.rs index e23a8302d..8294e0a07 100644 --- a/docsrs_bindings.rs +++ b/docsrs_bindings.rs @@ -1898,6 +1898,11 @@ extern "C" { parent_ce: *mut zend_class_entry, ) -> *mut zend_class_entry; } +extern "C" { + pub fn zend_register_internal_interface( + orig_class_entry: *mut zend_class_entry, + ) -> *mut zend_class_entry; +} extern "C" { pub fn zend_is_callable( callable: *mut zval, diff --git a/src/builders/class.rs b/src/builders/class.rs index f837f43ae..55f155173 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -9,7 +9,7 @@ use crate::{ exception::PhpException, ffi::{ zend_declare_class_constant, zend_declare_property, zend_do_implement_interface, - zend_register_internal_class_ex, + zend_register_internal_class_ex, zend_register_internal_interface, }, flags::{ClassFlags, MethodFlags, PropertyFlags}, types::{ZendClassObject, ZendObject, ZendStr, Zval}, @@ -306,16 +306,24 @@ impl ClassBuilder { let func = Box::into_raw(methods.into_boxed_slice()) as *const FunctionEntry; self.ce.info.internal.builtin_functions = func; - let class = unsafe { - zend_register_internal_class_ex( - &raw mut self.ce, - match self.extends { - Some((ptr, _)) => ptr::from_ref(ptr()).cast_mut(), - None => std::ptr::null_mut(), - }, - ) - .as_mut() - .ok_or(Error::InvalidPointer)? + let class = if self.ce.flags().contains(ClassFlags::Interface) { + unsafe { + zend_register_internal_interface(&mut self.ce) + .as_mut() + .ok_or(Error::InvalidPointer)? + } + } else { + unsafe { + zend_register_internal_class_ex( + &raw mut self.ce, + match self.extends { + Some((ptr, _)) => ptr::from_ref(ptr()).cast_mut(), + None => std::ptr::null_mut(), + }, + ) + .as_mut() + .ok_or(Error::InvalidPointer)? + } }; // disable serialization if the class has an associated object @@ -467,6 +475,14 @@ mod tests { assert!(class.register.is_some()); } + #[test] + fn test_registration_interface() { + let class = ClassBuilder::new("Foo") + .flags(ClassFlags::Interface) + .registration(|_| {}); + assert!(class.register.is_some()); + } + #[test] fn test_docs() { let class = ClassBuilder::new("Foo").docs(&["Doc 1"]); From 9ed4ad8ebd2785994a74850866ea19e940b4dae2 Mon Sep 17 00:00:00 2001 From: norbytus Date: Sun, 13 Jul 2025 10:51:54 +0300 Subject: [PATCH 02/25] feat: Add interface builders --- src/builders/class.rs | 73 ++++++++++++++++++++++++++++++++++++++++++ src/builders/module.rs | 29 +++++++++++++++++ src/lib.rs | 8 ++--- 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/builders/class.rs b/src/builders/class.rs index 55f155173..db69ab6e1 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -386,6 +386,79 @@ impl ClassBuilder { } } +pub struct InterfaceBuilder { + class_builder: ClassBuilder, +} + +impl InterfaceBuilder { + pub fn new>(name: T) -> Self { + Self { + class_builder: ClassBuilder::new(name), + } + } + + pub fn implements(mut self, interface: ClassEntryInfo) -> Self { + self.class_builder = self.class_builder.implements(interface); + + self + } + + pub fn method(mut self, func: FunctionBuilder<'static>, flags: MethodFlags) -> Self { + self.class_builder = self.class_builder.method(func, flags); + + self + } + + pub fn constant>( + mut self, + name: T, + value: impl IntoZval + 'static, + docs: DocComments, + ) -> Result { + self.class_builder = self.class_builder.constant(name, value, docs)?; + + Ok(self) + } + + pub fn dyn_constant>( + mut self, + name: T, + value: &'static dyn IntoZvalDyn, + docs: DocComments, + ) -> Result { + self.class_builder = self.class_builder.dyn_constant(name, value, docs)?; + + Ok(self) + } + + pub fn flags(mut self, flags: ClassFlags) -> Self { + self.class_builder = self.class_builder.flags(flags); + self + } + + pub fn object_override(mut self) -> Self { + self.class_builder = self.class_builder.object_override::(); + + self + } + + pub fn registration(mut self, register: fn(&'static mut ClassEntry)) -> Self { + self.class_builder = self.class_builder.registration(register); + + self + } + + pub fn docs(mut self, docs: DocComments) -> Self { + self.class_builder = self.class_builder.docs(docs); + self + } + + pub fn builder(mut self) -> ClassBuilder { + self.class_builder = self.class_builder.flags(ClassFlags::Interface); + self.class_builder + } +} + #[cfg(test)] mod tests { use crate::test::test_function; diff --git a/src/builders/module.rs b/src/builders/module.rs index 1eea007e5..dfa0e8088 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -4,6 +4,7 @@ use super::{ClassBuilder, FunctionBuilder}; #[cfg(feature = "enum")] use crate::{builders::enum_builder::EnumBuilder, enum_::RegisteredEnum}; use crate::{ + builders::class::InterfaceBuilder, class::RegisteredClass, constant::IntoConst, describe::DocComments, @@ -192,6 +193,34 @@ impl ModuleBuilder<'_> { self } + pub fn interface(mut self) -> Self { + self.classes.push(|| { + let mut builder = InterfaceBuilder::new(T::CLASS_NAME); + for (method, flags) in T::method_builders() { + builder = builder.method(method, flags); + } + for (name, value, docs) in T::constants() { + builder = builder + .dyn_constant(*name, *value, docs) + .expect("Failed to register constant"); + } + + let mut class_builder = builder.builder(); + + if let Some(modifier) = T::BUILDER_MODIFIER { + class_builder = modifier(class_builder); + } + + class_builder + .object_override::() + .registration(|ce| { + T::get_metadata().set_ce(ce); + }) + .docs(T::DOC_COMMENTS) + }); + self + } + /// Adds a class to the extension. /// /// # Panics diff --git a/src/lib.rs b/src/lib.rs index 4b51f6413..b7a533a73 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,8 +55,8 @@ pub mod prelude { pub use crate::php_println; pub use crate::types::ZendCallable; pub use crate::{ - php_class, php_const, php_extern, php_function, php_impl, php_module, wrap_constant, - wrap_function, zend_fastcall, ZvalConvert, + php_class, php_const, php_extern, php_function, php_impl, php_interface, php_module, + wrap_constant, wrap_function, zend_fastcall, ZvalConvert, }; } @@ -72,6 +72,6 @@ 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, + php_class, php_const, php_extern, php_function, php_impl, php_interface, php_module, + wrap_constant, wrap_function, zend_fastcall, ZvalConvert, }; From eabefe9a25d6403b05aa408ce478624486e66599 Mon Sep 17 00:00:00 2001 From: norbytus Date: Sun, 13 Jul 2025 10:52:09 +0300 Subject: [PATCH 03/25] feat(macro): Add macro to declare interface from trait --- crates/macros/src/interface.rs | 139 +++++++++++++++++++++++++++++++++ crates/macros/src/lib.rs | 23 ++++++ 2 files changed, 162 insertions(+) create mode 100644 crates/macros/src/interface.rs diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs new file mode 100644 index 000000000..11f499b51 --- /dev/null +++ b/crates/macros/src/interface.rs @@ -0,0 +1,139 @@ +use darling::util::Flag; +use darling::{FromAttributes, FromMeta, ToTokens}; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{Attribute, Expr, Fields, ItemStruct, ItemTrait}; + +use crate::helpers::get_docs; +use crate::parsing::{PhpRename, RenameRule}; +use crate::prelude::*; + +#[derive(FromAttributes, Debug, Default)] +#[darling(attributes(php), forward_attrs(doc), default)] +pub struct StructAttributes { + #[darling(flatten)] + rename: PhpRename, +} + +pub fn parser(mut input: ItemTrait) -> Result { + let attr = StructAttributes::from_attributes(&input.attrs)?; + let ident = &input.ident; + + let interface_name = format_ident!("PhpInterface{ident}"); + let name = attr.rename.rename(ident.to_string(), RenameRule::Pascal); + + Ok(quote! { + #input + + pub struct #interface_name; + + impl ::ext_php_rs::class::RegisteredClass for #interface_name { + const CLASS_NAME: &'static str = #name; + + const BUILDER_MODIFIER: Option< + fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder, + > = None; + + const EXTENDS: Option<::ext_php_rs::class::ClassEntryInfo> = None; + + const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::Interface; + + const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[]; + + fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { + static METADATA: ::ext_php_rs::class::ClassMetadata<#interface_name> = + ::ext_php_rs::class::ClassMetadata::new(); + + &METADATA + } + + fn method_builders() -> Vec<( + ::ext_php_rs::builders::FunctionBuilder<'static>, + ::ext_php_rs::flags::MethodFlags, + )> { + vec![ ] + } + + fn constructor() -> Option<::ext_php_rs::class::ConstructorMeta> { + None + } + + fn constants() -> &'static [( + &'static str, + &'static dyn ext_php_rs::convert::IntoZvalDyn, + ext_php_rs::describe::DocComments, + )] { + &[] + } + + fn get_properties<'a>() -> std::collections::HashMap<&'static str, PropertyInfo<'a, Self>> { + HashMap::new() + } + + } + + impl<'a> ::ext_php_rs::convert::FromZendObject<'a> for &'a #interface_name { + #[inline] + fn from_zend_object( + obj: &'a ::ext_php_rs::types::ZendObject, + ) -> ::ext_php_rs::error::Result { + let obj = ::ext_php_rs::types::ZendClassObject::<#interface_name>::from_zend_obj(obj) + .ok_or(::ext_php_rs::error::Error::InvalidScope)?; + Ok(&**obj) + } + } + impl<'a> ::ext_php_rs::convert::FromZendObjectMut<'a> for &'a mut #interface_name { + #[inline] + fn from_zend_object_mut( + obj: &'a mut ::ext_php_rs::types::ZendObject, + ) -> ::ext_php_rs::error::Result { + let obj = ::ext_php_rs::types::ZendClassObject::<#interface_name>::from_zend_obj_mut(obj) + .ok_or(::ext_php_rs::error::Error::InvalidScope)?; + Ok(&mut **obj) + } + } + impl<'a> ::ext_php_rs::convert::FromZval<'a> for &'a #interface_name { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(Some( + <#interface_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME, + )); + #[inline] + fn from_zval(zval: &'a ::ext_php_rs::types::Zval) -> ::std::option::Option { + ::from_zend_object(zval.object()?).ok() + } + } + impl<'a> ::ext_php_rs::convert::FromZvalMut<'a> for &'a mut #interface_name { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(Some( + <#interface_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME, + )); + #[inline] + fn from_zval_mut(zval: &'a mut ::ext_php_rs::types::Zval) -> ::std::option::Option { + ::from_zend_object_mut(zval.object_mut()?) + .ok() + } + } + impl ::ext_php_rs::convert::IntoZendObject for #interface_name { + #[inline] + fn into_zend_object( + self, + ) -> ::ext_php_rs::error::Result<::ext_php_rs::boxed::ZBox<::ext_php_rs::types::ZendObject>> + { + Ok(::ext_php_rs::types::ZendClassObject::new(self).into()) + } + } + impl ::ext_php_rs::convert::IntoZval for #interface_name { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(Some( + <#interface_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME, + )); + const NULLABLE: bool = false; + #[inline] + fn set_zval( + self, + zv: &mut ::ext_php_rs::types::Zval, + persistent: bool, + ) -> ::ext_php_rs::error::Result<()> { + use ::ext_php_rs::convert::IntoZendObject; + self.into_zend_object()?.set_zval(zv, persistent) + } + } + }) +} diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 553bb3de8..444bd34d6 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -7,14 +7,22 @@ mod fastcall; mod function; mod helpers; mod impl_; +mod interface; mod module; mod parsing; mod syn_ext; mod zval; use proc_macro::TokenStream; +<<<<<<< HEAD use proc_macro2::TokenStream as TokenStream2; use syn::{DeriveInput, ItemConst, ItemEnum, ItemFn, ItemForeignMod, ItemImpl, ItemStruct}; +======= +use syn::{ + parse_macro_input, DeriveInput, ItemConst, ItemFn, ItemForeignMod, ItemImpl, ItemStruct, + ItemTrait, +}; +>>>>>>> 1a0a9d6 (feat(macro): Add macro to declare interface from trait) extern crate proc_macro; @@ -339,6 +347,21 @@ fn php_enum_internal(_args: TokenStream2, input: TokenStream2) -> TokenStream2 { enum_::parser(input).unwrap_or_else(|e| e.to_compile_error()) } +#[proc_macro_attribute] +pub fn php_interface(args: TokenStream, input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemTrait); + + if !args.is_empty() { + return err!(input => "`#[php_interface]` not apply args") + .to_compile_error() + .into(); + } + + interface::parser(input) + .unwrap_or_else(|e| e.to_compile_error()) + .into() +} + // BEGIN DOCS FROM function.md /// # `#[php_function]` Attribute /// From 263a0868d587eda93ce20e1bdc95cbe268b48e0c Mon Sep 17 00:00:00 2001 From: norbytus Date: Sun, 13 Jul 2025 22:10:45 +0300 Subject: [PATCH 04/25] feat: Add tests --- crates/macros/src/helpers.rs | 10 ++++++++ crates/macros/src/interface.rs | 23 +++++++++++++++---- tests/src/integration/interface/interface.php | 18 +++++++++++++++ tests/src/integration/interface/mod.rs | 18 +++++++++++++++ tests/src/integration/mod.rs | 1 + 5 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 tests/src/integration/interface/interface.php create mode 100644 tests/src/integration/interface/mod.rs diff --git a/crates/macros/src/helpers.rs b/crates/macros/src/helpers.rs index 162c904fd..025af1991 100644 --- a/crates/macros/src/helpers.rs +++ b/crates/macros/src/helpers.rs @@ -24,3 +24,13 @@ pub fn get_docs(attrs: &[Attribute]) -> Result> { }) .collect::>>() } + +pub trait CleanPhpAttr { + fn clean_php(&mut self); +} + +impl CleanPhpAttr for Vec { + fn clean_php(&mut self) { + self.retain(|attr| !attr.path().is_ident("php")); + } +} diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index 11f499b51..2479ab5b9 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -1,10 +1,9 @@ -use darling::util::Flag; -use darling::{FromAttributes, FromMeta, ToTokens}; +use darling::{FromAttributes}; use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Attribute, Expr, Fields, ItemStruct, ItemTrait}; +use syn::{ItemTrait, TraitItem, TraitItemFn}; +use crate::helpers::CleanPhpAttr; -use crate::helpers::get_docs; use crate::parsing::{PhpRename, RenameRule}; use crate::prelude::*; @@ -21,6 +20,20 @@ pub fn parser(mut input: ItemTrait) -> Result { let interface_name = format_ident!("PhpInterface{ident}"); let name = attr.rename.rename(ident.to_string(), RenameRule::Pascal); + input.attrs.clean_php(); + + let mut interface_methods: Vec = Vec::new(); + for i in input.items.clone().into_iter() { + match i { + TraitItem::Fn(f) => { + if f.default.is_some() { + bail!("Interface could not have default impl"); + } + interface_methods.push(f); + } + _ => {} + } + }; Ok(quote! { #input @@ -66,7 +79,7 @@ pub fn parser(mut input: ItemTrait) -> Result { &[] } - fn get_properties<'a>() -> std::collections::HashMap<&'static str, PropertyInfo<'a, Self>> { + fn get_properties<'a>() -> std::collections::HashMap<&'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self>> { HashMap::new() } diff --git a/tests/src/integration/interface/interface.php b/tests/src/integration/interface/interface.php new file mode 100644 index 000000000..29d7c1fa1 --- /dev/null +++ b/tests/src/integration/interface/interface.php @@ -0,0 +1,18 @@ + ModuleBuilder { + builder.interface::() +} + +#[cfg(test)] +mod tests { + #[test] + fn interface_work() { + assert!(crate::integration::test::run_php("interface/interface.php")); + } +} diff --git a/tests/src/integration/mod.rs b/tests/src/integration/mod.rs index cc18e5108..c472e4ff2 100644 --- a/tests/src/integration/mod.rs +++ b/tests/src/integration/mod.rs @@ -17,6 +17,7 @@ pub mod object; pub mod string; pub mod types; pub mod variadic_args; +pub mod interface; #[cfg(test)] mod test { From aa2290a509fc518068ca47ffbd8c17371d5cb902 Mon Sep 17 00:00:00 2001 From: norbytus Date: Sun, 20 Jul 2025 12:53:31 +0300 Subject: [PATCH 05/25] feat: Add missing things in interface builder --- src/builders/module.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/builders/module.rs b/src/builders/module.rs index dfa0e8088..ac0d12d64 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -199,6 +199,14 @@ impl ModuleBuilder<'_> { for (method, flags) in T::method_builders() { builder = builder.method(method, flags); } + for interface in T::IMPLEMENTS { + builder = builder.implements(*interface); + } + for (name, value, docs) in T::constants() { + builder = builder + .dyn_constant(*name, *value, docs) + .expect("Failed to register constant"); + } for (name, value, docs) in T::constants() { builder = builder .dyn_constant(*name, *value, docs) From cec3b923dda8b2a6c2455b8b03c0bd811738fe82 Mon Sep 17 00:00:00 2001 From: norbytus Date: Sun, 20 Jul 2025 17:15:42 +0300 Subject: [PATCH 06/25] feat: Add methods, const registration for interface --- crates/macros/src/class.rs | 19 +- crates/macros/src/function.rs | 35 ++++ crates/macros/src/impl_.rs | 18 +- crates/macros/src/interface.rs | 176 ++++++++++++++++-- src/builders/class.rs | 33 ---- src/builders/module.rs | 5 - tests/src/integration/interface/interface.php | 37 +++- tests/src/integration/interface/mod.rs | 33 +++- 8 files changed, 271 insertions(+), 85 deletions(-) diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index 103a238dd..3a5cc8434 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -1,7 +1,7 @@ use darling::util::Flag; use darling::{FromAttributes, FromMeta, ToTokens}; use proc_macro2::TokenStream; -use quote::quote; +use quote::{quote, TokenStreamExt}; use syn::{Attribute, Expr, Fields, ItemStruct}; use crate::helpers::get_docs; @@ -28,8 +28,17 @@ pub struct StructAttributes { #[derive(FromMeta, Debug)] pub struct ClassEntryAttribute { - ce: syn::Expr, - stub: String, + pub ce: syn::Expr, + pub stub: String, +} + +impl ToTokens for ClassEntryAttribute { + fn to_tokens(&self, tokens: &mut TokenStream) { + let ce = &self.ce; + let stub = &self.stub; + let token = quote! { (#ce, #stub) }; + tokens.append_all(token); + } } pub fn parser(mut input: ItemStruct) -> Result { @@ -151,10 +160,8 @@ fn generate_registered_class_impl( }; let extends = if let Some(extends) = extends { - let ce = &extends.ce; - let stub = &extends.stub; quote! { - Some((#ce, #stub)) + Some(#extends) } } else { quote! { None } diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index d24f61fe6..7577a8c11 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -131,6 +131,41 @@ impl<'a> Function<'a> { format_ident!("_internal_{}", &self.ident) } + pub fn abstract_function_builder(&self) -> TokenStream { + let name = &self.name; + let (required, not_required) = self.args.split_args(self.optional.as_ref()); + + // `entry` impl + let required_args = required + .iter() + .map(TypedArg::arg_builder) + .collect::>(); + let not_required_args = not_required + .iter() + .map(TypedArg::arg_builder) + .collect::>(); + + let returns = self.build_returns(); + let docs = if self.docs.is_empty() { + quote! {} + } else { + let docs = &self.docs; + quote! { + .docs(&[#(#docs),*]) + } + }; + + quote! { + ::ext_php_rs::builders::FunctionBuilder::new_abstract(#name) + #(.arg(#required_args))* + .not_required() + #(.arg(#not_required_args))* + #returns + #docs + } + + } + /// Generates the function builder for the function. pub fn function_builder(&self, call_type: CallType) -> TokenStream { let name = &self.name; diff --git a/crates/macros/src/impl_.rs b/crates/macros/src/impl_.rs index 353448eb5..521f83313 100644 --- a/crates/macros/src/impl_.rs +++ b/crates/macros/src/impl_.rs @@ -125,7 +125,7 @@ struct ParsedImpl<'a> { } #[derive(Debug, Eq, Hash, PartialEq)] -enum MethodModifier { +pub enum MethodModifier { Abstract, Static, } @@ -141,7 +141,7 @@ impl quote::ToTokens for MethodModifier { } #[derive(Debug)] -struct FnBuilder { +pub struct FnBuilder { /// Tokens which represent the `FunctionBuilder` for this function. pub builder: TokenStream, /// The visibility of this method. @@ -151,13 +151,19 @@ struct FnBuilder { } #[derive(Debug)] -struct Constant<'a> { +pub struct Constant<'a> { /// Name of the constant in PHP land. - name: String, + pub name: String, /// Identifier of the constant in Rust land. - ident: &'a syn::Ident, + pub ident: &'a syn::Ident, /// Documentation for the constant. - docs: Vec, + pub docs: Vec, +} + +impl<'a> Constant<'a> { + pub fn new(name: String, ident: &'a syn::Ident, docs: Vec) -> Self { + Self {name, ident, docs} + } } impl<'a> ParsedImpl<'a> { diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index 2479ab5b9..931786443 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -1,10 +1,17 @@ +use std::collections::{HashMap, HashSet}; + +use darling::util::Flag; use darling::{FromAttributes}; use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{ItemTrait, TraitItem, TraitItemFn}; -use crate::helpers::CleanPhpAttr; +use syn::{Expr, Ident, ItemTrait, Path, TraitItem, TraitItemConst, TraitItemFn}; +use crate::class::ClassEntryAttribute; +use crate::constant::PhpConstAttribute; +use crate::function::{Args, Function}; +use crate::helpers::{get_docs, CleanPhpAttr}; -use crate::parsing::{PhpRename, RenameRule}; +use crate::impl_::{Constant, FnBuilder, MethodModifier}; +use crate::parsing::{PhpRename, RenameRule, Visibility}; use crate::prelude::*; #[derive(FromAttributes, Debug, Default)] @@ -12,6 +19,8 @@ use crate::prelude::*; pub struct StructAttributes { #[darling(flatten)] rename: PhpRename, + #[darling(multiple)] + extends: Vec, } pub fn parser(mut input: ItemTrait) -> Result { @@ -19,27 +28,67 @@ pub fn parser(mut input: ItemTrait) -> Result { let ident = &input.ident; let interface_name = format_ident!("PhpInterface{ident}"); + let ts = quote! { #interface_name }; + let path: Path = syn::parse2(ts)?; + let name = attr.rename.rename(ident.to_string(), RenameRule::Pascal); input.attrs.clean_php(); - let mut interface_methods: Vec = Vec::new(); - for i in input.items.clone().into_iter() { - match i { - TraitItem::Fn(f) => { - if f.default.is_some() { - bail!("Interface could not have default impl"); + let methods: Vec = input.items.iter_mut() + .flat_map( + |item: &mut TraitItem| { + match item { + TraitItem::Fn(f) => Some(f), + _ => None, } - interface_methods.push(f); + }) + .flat_map(|f| f.parse()) + .collect(); + + let constants: Vec<_> = input.items.iter_mut() + .flat_map(|item: &mut TraitItem| { + match item { + TraitItem::Const(c) => Some(c), + _ => None, } - _ => {} + }) + .flat_map(|c| c.parse()) + .map(|c| { + let name = &c.name; + let ident = c.ident; + let docs = &c.docs; + quote! { + (#name, &#path::#ident, &[#(#docs),*]) + } + }) + .collect(); + + let impl_const: Vec<&TraitItemConst> = input.items.iter().flat_map(|item| { + match item { + TraitItem::Const(c) => Some(c), + _ => None, } - }; + }) + .map(|c| { + if c.default.is_none() { + bail!("Interface const canot be empty"); + } + Ok(c) + }) + .flat_map(|c| c) + .collect(); + + let implements = attr.extends; Ok(quote! { #input pub struct #interface_name; + impl #interface_name { + #(pub #impl_const)* + } + impl ::ext_php_rs::class::RegisteredClass for #interface_name { const CLASS_NAME: &'static str = #name; @@ -51,7 +100,9 @@ pub fn parser(mut input: ItemTrait) -> Result { const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::Interface; - const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[]; + const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[ + #(#implements,)* + ]; fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { static METADATA: ::ext_php_rs::class::ClassMetadata<#interface_name> = @@ -64,7 +115,7 @@ pub fn parser(mut input: ItemTrait) -> Result { ::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags, )> { - vec![ ] + vec![#(#methods),*] } fn constructor() -> Option<::ext_php_rs::class::ConstructorMeta> { @@ -76,7 +127,8 @@ pub fn parser(mut input: ItemTrait) -> Result { &'static dyn ext_php_rs::convert::IntoZvalDyn, ext_php_rs::describe::DocComments, )] { - &[] + use ::ext_php_rs::internal::class::PhpClassImpl; + ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_constants() } fn get_properties<'a>() -> std::collections::HashMap<&'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self>> { @@ -84,6 +136,28 @@ pub fn parser(mut input: ItemTrait) -> Result { } } + impl ::ext_php_rs::internal::class::PhpClassImpl<#path> + for ::ext_php_rs::internal::class::PhpClassImplCollector<#path> + { + fn get_methods(self) -> ::std::vec::Vec< + (::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags) + > { + vec![] + } + + fn get_method_props<'a>(self) -> ::std::collections::HashMap<&'static str, ::ext_php_rs::props::Property<'a, #path>> { + todo!() + } + + fn get_constructor(self) -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta<#path>> { + None + } + + fn get_constants(self) -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn, &'static [&'static str])] { + &[#(#constants),*] + } + } + impl<'a> ::ext_php_rs::convert::FromZendObject<'a> for &'a #interface_name { #[inline] @@ -150,3 +224,75 @@ pub fn parser(mut input: ItemTrait) -> Result { } }) } + +#[derive(FromAttributes, Default, Debug)] +#[darling(default, attributes(php), forward_attrs(doc))] +pub struct PhpFunctionInterfaceAttribute { + #[darling(flatten)] + rename: PhpRename, + defaults: HashMap, + optional: Option, + vis: Option, + attrs: Vec, + getter: Flag, + setter: Flag, + constructor: Flag, +} + +trait Parse<'a, T> { + fn parse(&'a mut self) -> Result; +} + +impl<'a> Parse<'a, Constant<'a>> for TraitItemConst { + fn parse(&'a mut self) -> Result> { + let attr = PhpConstAttribute::from_attributes(&self.attrs)?; + let name = self.ident.to_string(); + let docs = get_docs(&attr.attrs)?; + self.attrs.clean_php(); + + Ok(Constant::new(name, &self.ident, docs)) + } +} + +impl<'a> Parse<'a, FnBuilder> for TraitItemFn { + fn parse(&'a mut self) -> Result { + let php_attr = PhpFunctionInterfaceAttribute::from_attributes( + &self.attrs + )?; + if self.default.is_some() { + bail!("Interface could not have default impl"); + } + + let mut args = Args::parse_from_fnargs( + self.sig.inputs.iter(), + php_attr.defaults + )?; + let docs = get_docs(&php_attr.attrs)?; + + self.attrs.clean_php(); + + let mut modifiers: HashSet = HashSet::new(); + modifiers.insert(MethodModifier::Abstract); + if args.typed.first().is_some_and(|arg| arg.name == "self_") { + args.typed.pop(); + } else if args.receiver.is_none() { + modifiers.insert(MethodModifier::Static); + }; + + let f = Function::new( + &self.sig, + php_attr + .rename + .rename(self.sig.ident.to_string(), RenameRule::Camel), + args, + php_attr.optional, + docs, + ); + + Ok(FnBuilder { + builder: f.abstract_function_builder(), + vis: php_attr.vis.unwrap_or(Visibility::Public), + modifiers, + }) + } +} diff --git a/src/builders/class.rs b/src/builders/class.rs index db69ab6e1..db65ee044 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -409,17 +409,6 @@ impl InterfaceBuilder { self } - pub fn constant>( - mut self, - name: T, - value: impl IntoZval + 'static, - docs: DocComments, - ) -> Result { - self.class_builder = self.class_builder.constant(name, value, docs)?; - - Ok(self) - } - pub fn dyn_constant>( mut self, name: T, @@ -431,28 +420,6 @@ impl InterfaceBuilder { Ok(self) } - pub fn flags(mut self, flags: ClassFlags) -> Self { - self.class_builder = self.class_builder.flags(flags); - self - } - - pub fn object_override(mut self) -> Self { - self.class_builder = self.class_builder.object_override::(); - - self - } - - pub fn registration(mut self, register: fn(&'static mut ClassEntry)) -> Self { - self.class_builder = self.class_builder.registration(register); - - self - } - - pub fn docs(mut self, docs: DocComments) -> Self { - self.class_builder = self.class_builder.docs(docs); - self - } - pub fn builder(mut self) -> ClassBuilder { self.class_builder = self.class_builder.flags(ClassFlags::Interface); self.class_builder diff --git a/src/builders/module.rs b/src/builders/module.rs index ac0d12d64..478a18d26 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -207,11 +207,6 @@ impl ModuleBuilder<'_> { .dyn_constant(*name, *value, docs) .expect("Failed to register constant"); } - for (name, value, docs) in T::constants() { - builder = builder - .dyn_constant(*name, *value, docs) - .expect("Failed to register constant"); - } let mut class_builder = builder.builder(); diff --git a/tests/src/integration/interface/interface.php b/tests/src/integration/interface/interface.php index 29d7c1fa1..c9aa6cd0d 100644 --- a/tests/src/integration/interface/interface.php +++ b/tests/src/integration/interface/interface.php @@ -1,18 +1,35 @@ nonStatic($data), $other->nonStatic($data)); + } } +$f = new Test(); -/* $example = new InterfaceExampleUsage; */ -/* assert( */ -/* is_a($example, ExtPhpRs\Interface\EmptyObjectInterface::class), */ -/* \sprintf( */ -/* 'Class should be implements of interface: %s', */ -/* Test\TestInterface::class */ -/* ) */ -/* ); */ +assert(is_a($f, Throwable::class)); +assert($f->nonStatic('Rust') === 'Rust - TEST'); +assert(ExtPhpRs\Interface\EmptyObjectInterface::HELLO === "HELLO"); +assert($f::HELLO === "HELLO"); +assert(Test::HELLO === "HELLO"); +assert($f->refToLikeThisClass('TEST', $f) === 'TEST - TEST | TEST - TEST'); diff --git a/tests/src/integration/interface/mod.rs b/tests/src/integration/interface/mod.rs index 7310599be..760481cef 100644 --- a/tests/src/integration/interface/mod.rs +++ b/tests/src/integration/interface/mod.rs @@ -1,18 +1,31 @@ -use ext_php_rs::prelude::*; use std::collections::HashMap; +use ext_php_rs::types::ZendClassObject; +use ext_php_rs::php_interface; +use ext_php_rs::{php_module, prelude::ModuleBuilder}; +use ext_php_rs::zend::ce; + #[php_interface] +#[php(extends(ce = ce::throwable, stub = "\\Throwable"))] #[php(name = "ExtPhpRs\\Interface\\EmptyObjectInterface")] -pub trait EmptyObjectInterface { } +pub trait EmptyObjectTrait +{ + const HELLO: &'static str = "HELLO"; + + fn void(); -pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { - builder.interface::() + fn non_static(&self, data: String) -> String; + + fn ref_to_like_this_class( + &self, + data: String, + other: &ZendClassObject + ) -> String; } -#[cfg(test)] -mod tests { - #[test] - fn interface_work() { - assert!(crate::integration::test::run_php("interface/interface.php")); - } +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module + .interface::() } + From d5443b5523295217d97ed840cb0d3ab9717aacad Mon Sep 17 00:00:00 2001 From: norbytus Date: Sun, 20 Jul 2025 17:40:19 +0300 Subject: [PATCH 07/25] feat: Add tests for interface registration --- tests/src/integration/interface/mod.rs | 15 ++++++++++----- tests/src/lib.rs | 1 + 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/src/integration/interface/mod.rs b/tests/src/integration/interface/mod.rs index 760481cef..fa6c7109c 100644 --- a/tests/src/integration/interface/mod.rs +++ b/tests/src/integration/interface/mod.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use ext_php_rs::types::ZendClassObject; -use ext_php_rs::php_interface; +use ext_php_rs::{php_class, php_impl, php_interface}; use ext_php_rs::{php_module, prelude::ModuleBuilder}; use ext_php_rs::zend::ce; @@ -23,9 +23,14 @@ pub trait EmptyObjectTrait ) -> String; } -#[php_module] -pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { - module - .interface::() +pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { + builder.interface::() } +#[cfg(test)] +mod tests { + #[test] + fn interface_work() { + assert!(crate::integration::test::run_php("interface/interface.php")); + } +} diff --git a/tests/src/lib.rs b/tests/src/lib.rs index ced895ded..3b7b2141d 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -31,6 +31,7 @@ pub fn build_module(module: ModuleBuilder) -> ModuleBuilder { module = integration::object::build_module(module); module = integration::string::build_module(module); module = integration::variadic_args::build_module(module); + module = integration::interface::build_module(module); module } From 458d0194cb3152b8f1cadd6566b003f632f5ccd4 Mon Sep 17 00:00:00 2001 From: norbytus Date: Mon, 21 Jul 2025 18:58:42 +0300 Subject: [PATCH 08/25] chore: Add internal function for interface attribute macros --- crates/macros/src/lib.rs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 444bd34d6..0c19d2f1c 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -14,15 +14,8 @@ mod syn_ext; mod zval; use proc_macro::TokenStream; -<<<<<<< HEAD use proc_macro2::TokenStream as TokenStream2; -use syn::{DeriveInput, ItemConst, ItemEnum, ItemFn, ItemForeignMod, ItemImpl, ItemStruct}; -======= -use syn::{ - parse_macro_input, DeriveInput, ItemConst, ItemFn, ItemForeignMod, ItemImpl, ItemStruct, - ItemTrait, -}; ->>>>>>> 1a0a9d6 (feat(macro): Add macro to declare interface from trait) +use syn::{DeriveInput, ItemConst, ItemEnum, ItemFn, ItemForeignMod, ItemImpl, ItemStruct, ItemTrait}; extern crate proc_macro; @@ -349,13 +342,11 @@ fn php_enum_internal(_args: TokenStream2, input: TokenStream2) -> TokenStream2 { #[proc_macro_attribute] pub fn php_interface(args: TokenStream, input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemTrait); + php_interface_internal(args.into(), input.into()).into() +} - if !args.is_empty() { - return err!(input => "`#[php_interface]` not apply args") - .to_compile_error() - .into(); - } +fn php_interface_internal(_args: TokenStream2, input: TokenStream2) -> TokenStream2 { + let input = parse_macro_input2!(input as ItemTrait).into(); interface::parser(input) .unwrap_or_else(|e| e.to_compile_error()) From 2f85f7310c3220bfbbd11fec214de27fae58c278 Mon Sep 17 00:00:00 2001 From: norbytus Date: Mon, 21 Jul 2025 19:31:10 +0300 Subject: [PATCH 09/25] feat: Add doc for interface macros and add test for expand --- crates/macros/src/interface.rs | 2 +- crates/macros/src/lib.rs | 42 +++ .../macros/tests/expand/interface.expanded.rs | 242 ++++++++++++++++++ crates/macros/tests/expand/interface.rs | 26 ++ tests/src/integration/interface/interface.php | 2 +- 5 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 crates/macros/tests/expand/interface.expanded.rs create mode 100644 crates/macros/tests/expand/interface.rs diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index 931786443..08bb33ad9 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -71,7 +71,7 @@ pub fn parser(mut input: ItemTrait) -> Result { }) .map(|c| { if c.default.is_none() { - bail!("Interface const canot be empty"); + bail!("Interface const cannot be empty"); } Ok(c) }) diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 0c19d2f1c..c6880de93 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -340,6 +340,46 @@ fn php_enum_internal(_args: TokenStream2, input: TokenStream2) -> TokenStream2 { enum_::parser(input).unwrap_or_else(|e| e.to_compile_error()) } +// BEGIN DOCS FROM interface.md +/// # `#[php_interface]` Attribute +/// +/// Traits can be exported to PHP as interface with the `#[php_interface]` attribute +/// macro. This attribute generate empty struct and derives the `RegisteredClass`. +/// To register the interface use the `interface::()` method +/// on the `ModuleBuilder` in the `#[php_module]` macro. +/// +/// ## Options +/// +/// The `#[php_interface]` attribute can be configured with the following options: +/// - `#[php(name = "InterfaceName")]` or `#[php(change_case = snake_case)]`: Sets +/// the name of the interface in PHP. The default is the `PascalCase` name of the +/// interface. +/// - `#[php(extends(ce = ce::throwable, stub = "\\Throwable"))]` +/// to extends interface from other interface +/// +/// ### Example +/// +/// This example creates a PHP interface extend from php buildin Throwable. +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// +/// #[php_interface] +/// #[php(extends(ce = ce::throwable, stub = "\\Throwable"))] +/// #[php(name = "LibName\\Exception\\MyCustomDomainException")] +/// pub trait MyCustomDomainException { +/// fn createWithMessage(message: String) -> Self; +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module.interface::() +/// } +/// # fn main() {} +/// ``` +// END DOCS FROM interface.md #[proc_macro_attribute] pub fn php_interface(args: TokenStream, input: TokenStream) -> TokenStream { php_interface_internal(args.into(), input.into()).into() @@ -1266,6 +1306,7 @@ mod tests { } fn runtime_expand_attr(path: &PathBuf) { + dbg!(path); let file = std::fs::File::open(path).expect("Failed to open expand test file"); runtime_macros::emulate_attributelike_macro_expansion( file, @@ -1273,6 +1314,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_interface", php_interface_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/interface.expanded.rs b/crates/macros/tests/expand/interface.expanded.rs new file mode 100644 index 000000000..d691d20bc --- /dev/null +++ b/crates/macros/tests/expand/interface.expanded.rs @@ -0,0 +1,242 @@ +#![feature(prelude_import)] +#[prelude_import] +use std::prelude::rust_2024::*; +#[macro_use] +extern crate std; +use ext_php_rs::types::ZendClassObject; +use ext_php_rs::php_interface; +use ext_php_rs::zend::ce; +pub trait EmptyObjectTrait { + const HELLO: &'static str = "HELLO"; + const ONE: u64 = 12; + fn void(); + fn non_static(&self, data: String) -> String; + fn ref_to_like_this_class( + &self, + data: String, + other: &ZendClassObject, + ) -> String; +} +pub struct PhpInterfaceEmptyObjectTrait; +impl PhpInterfaceEmptyObjectTrait { + pub const HELLO: &'static str = "HELLO"; + pub const ONE: u64 = 12; +} +impl ::ext_php_rs::class::RegisteredClass for PhpInterfaceEmptyObjectTrait { + const CLASS_NAME: &'static str = "ExtPhpRs\\Interface\\EmptyObjectInterface"; + const BUILDER_MODIFIER: Option< + fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder, + > = None; + const EXTENDS: Option<::ext_php_rs::class::ClassEntryInfo> = None; + const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::Interface; + const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[ + (ce::throwable, "\\Throwable"), + ]; + fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { + static METADATA: ::ext_php_rs::class::ClassMetadata< + PhpInterfaceEmptyObjectTrait, + > = ::ext_php_rs::class::ClassMetadata::new(); + &METADATA + } + fn method_builders() -> Vec< + ( + ::ext_php_rs::builders::FunctionBuilder<'static>, + ::ext_php_rs::flags::MethodFlags, + ), + > { + <[_]>::into_vec( + ::alloc::boxed::box_new([ + ( + ::ext_php_rs::builders::FunctionBuilder::new_abstract("void") + .not_required(), + ::ext_php_rs::flags::MethodFlags::Public + | ::ext_php_rs::flags::MethodFlags::Abstract + | ::ext_php_rs::flags::MethodFlags::Static, + ), + ( + ::ext_php_rs::builders::FunctionBuilder::new_abstract("nonStatic") + .arg( + ::ext_php_rs::args::Arg::new( + "data", + ::TYPE, + ), + ) + .not_required() + .returns( + ::TYPE, + false, + ::NULLABLE, + ), + ::ext_php_rs::flags::MethodFlags::Public + | ::ext_php_rs::flags::MethodFlags::Abstract, + ), + ( + ::ext_php_rs::builders::FunctionBuilder::new_abstract( + "refToLikeThisClass", + ) + .arg( + ::ext_php_rs::args::Arg::new( + "data", + ::TYPE, + ), + ) + .arg( + ::ext_php_rs::args::Arg::new( + "other", + <&ZendClassObject< + PhpInterfaceEmptyObjectTrait, + > as ::ext_php_rs::convert::FromZvalMut>::TYPE, + ), + ) + .not_required() + .returns( + ::TYPE, + false, + ::NULLABLE, + ), + ::ext_php_rs::flags::MethodFlags::Public + | ::ext_php_rs::flags::MethodFlags::Abstract, + ), + ]), + ) + } + fn constructor() -> Option<::ext_php_rs::class::ConstructorMeta> { + None + } + fn constants() -> &'static [( + &'static str, + &'static dyn ext_php_rs::convert::IntoZvalDyn, + ext_php_rs::describe::DocComments, + )] { + use ::ext_php_rs::internal::class::PhpClassImpl; + ::ext_php_rs::internal::class::PhpClassImplCollector::::default() + .get_constants() + } + fn get_properties<'a>() -> std::collections::HashMap< + &'static str, + ::ext_php_rs::internal::property::PropertyInfo<'a, Self>, + > { + HashMap::new() + } +} +impl ::ext_php_rs::internal::class::PhpClassImpl +for ::ext_php_rs::internal::class::PhpClassImplCollector { + fn get_methods( + self, + ) -> ::std::vec::Vec< + ( + ::ext_php_rs::builders::FunctionBuilder<'static>, + ::ext_php_rs::flags::MethodFlags, + ), + > { + ::alloc::vec::Vec::new() + } + fn get_method_props<'a>( + self, + ) -> ::std::collections::HashMap< + &'static str, + ::ext_php_rs::props::Property<'a, PhpInterfaceEmptyObjectTrait>, + > { + ::core::panicking::panic("not yet implemented") + } + fn get_constructor( + self, + ) -> ::std::option::Option< + ::ext_php_rs::class::ConstructorMeta, + > { + None + } + fn get_constants( + self, + ) -> &'static [( + &'static str, + &'static dyn ::ext_php_rs::convert::IntoZvalDyn, + &'static [&'static str], + )] { + &[ + ("HELLO", &PhpInterfaceEmptyObjectTrait::HELLO, &[]), + ("ONE", &PhpInterfaceEmptyObjectTrait::ONE, &[]), + ] + } +} +impl<'a> ::ext_php_rs::convert::FromZendObject<'a> for &'a PhpInterfaceEmptyObjectTrait { + #[inline] + fn from_zend_object( + obj: &'a ::ext_php_rs::types::ZendObject, + ) -> ::ext_php_rs::error::Result { + let obj = ::ext_php_rs::types::ZendClassObject::< + PhpInterfaceEmptyObjectTrait, + >::from_zend_obj(obj) + .ok_or(::ext_php_rs::error::Error::InvalidScope)?; + Ok(&**obj) + } +} +impl<'a> ::ext_php_rs::convert::FromZendObjectMut<'a> +for &'a mut PhpInterfaceEmptyObjectTrait { + #[inline] + fn from_zend_object_mut( + obj: &'a mut ::ext_php_rs::types::ZendObject, + ) -> ::ext_php_rs::error::Result { + let obj = ::ext_php_rs::types::ZendClassObject::< + PhpInterfaceEmptyObjectTrait, + >::from_zend_obj_mut(obj) + .ok_or(::ext_php_rs::error::Error::InvalidScope)?; + Ok(&mut **obj) + } +} +impl<'a> ::ext_php_rs::convert::FromZval<'a> for &'a PhpInterfaceEmptyObjectTrait { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object( + Some( + ::CLASS_NAME, + ), + ); + #[inline] + fn from_zval(zval: &'a ::ext_php_rs::types::Zval) -> ::std::option::Option { + ::from_zend_object(zval.object()?) + .ok() + } +} +impl<'a> ::ext_php_rs::convert::FromZvalMut<'a> +for &'a mut PhpInterfaceEmptyObjectTrait { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object( + Some( + ::CLASS_NAME, + ), + ); + #[inline] + fn from_zval_mut( + zval: &'a mut ::ext_php_rs::types::Zval, + ) -> ::std::option::Option { + ::from_zend_object_mut( + zval.object_mut()?, + ) + .ok() + } +} +impl ::ext_php_rs::convert::IntoZendObject for PhpInterfaceEmptyObjectTrait { + #[inline] + fn into_zend_object( + self, + ) -> ::ext_php_rs::error::Result< + ::ext_php_rs::boxed::ZBox<::ext_php_rs::types::ZendObject>, + > { + Ok(::ext_php_rs::types::ZendClassObject::new(self).into()) + } +} +impl ::ext_php_rs::convert::IntoZval for PhpInterfaceEmptyObjectTrait { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object( + Some( + ::CLASS_NAME, + ), + ); + const NULLABLE: bool = false; + #[inline] + fn set_zval( + self, + zv: &mut ::ext_php_rs::types::Zval, + persistent: bool, + ) -> ::ext_php_rs::error::Result<()> { + use ::ext_php_rs::convert::IntoZendObject; + self.into_zend_object()?.set_zval(zv, persistent) + } +} diff --git a/crates/macros/tests/expand/interface.rs b/crates/macros/tests/expand/interface.rs new file mode 100644 index 000000000..f3b2b00c4 --- /dev/null +++ b/crates/macros/tests/expand/interface.rs @@ -0,0 +1,26 @@ +#[macro_use] +extern crate ext_php_rs_derive; + +use ext_php_rs::types::ZendClassObject; +use ext_php_rs::php_interface; +use ext_php_rs::zend::ce; + +#[php_interface] +#[php(extends(ce = ce::throwable, stub = "\\Throwable"))] +#[php(name = "ExtPhpRs\\Interface\\EmptyObjectInterface")] +pub trait EmptyObjectTrait +{ + const HELLO: &'static str = "HELLO"; + + const ONE: u64 = 12; + + fn void(); + + fn non_static(&self, data: String) -> String; + + fn ref_to_like_this_class( + &self, + data: String, + other: &ZendClassObject + ) -> String; +} diff --git a/tests/src/integration/interface/interface.php b/tests/src/integration/interface/interface.php index c9aa6cd0d..e2a934b11 100644 --- a/tests/src/integration/interface/interface.php +++ b/tests/src/integration/interface/interface.php @@ -4,7 +4,7 @@ assert(interface_exists('ExtPhpRs\Interface\EmptyObjectInterface'), 'Interface not exist'); -assert(is_a('ExtPhpRs\Interface\EmptyObjectInterface', Throwable::class, true), 'Interface sould extend Throwable'); +assert(is_a('ExtPhpRs\Interface\EmptyObjectInterface', Throwable::class, true), 'Interface could extend Throwable'); final class Test extends Exception implements ExtPhpRs\Interface\EmptyObjectInterface From aba640f48d0f8aa838537303beb5b0032f27f3a9 Mon Sep 17 00:00:00 2001 From: norbytus Date: Mon, 21 Jul 2025 19:52:15 +0300 Subject: [PATCH 10/25] chore: CI things --- crates/macros/src/function.rs | 1 - crates/macros/src/impl_.rs | 2 +- crates/macros/src/interface.rs | 58 ++++++++++++-------------- crates/macros/src/lib.rs | 10 ++--- src/builders/class.rs | 2 +- src/builders/module.rs | 9 +++- tests/src/integration/interface/mod.rs | 7 ++-- tests/src/integration/mod.rs | 2 +- 8 files changed, 44 insertions(+), 47 deletions(-) diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index 7577a8c11..34b2570e0 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -163,7 +163,6 @@ impl<'a> Function<'a> { #returns #docs } - } /// Generates the function builder for the function. diff --git a/crates/macros/src/impl_.rs b/crates/macros/src/impl_.rs index 521f83313..429aa3827 100644 --- a/crates/macros/src/impl_.rs +++ b/crates/macros/src/impl_.rs @@ -162,7 +162,7 @@ pub struct Constant<'a> { impl<'a> Constant<'a> { pub fn new(name: String, ident: &'a syn::Ident, docs: Vec) -> Self { - Self {name, ident, docs} + Self { name, ident, docs } } } diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index 08bb33ad9..d21a6a65a 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -1,14 +1,14 @@ use std::collections::{HashMap, HashSet}; -use darling::util::Flag; -use darling::{FromAttributes}; -use proc_macro2::TokenStream; -use quote::{format_ident, quote}; -use syn::{Expr, Ident, ItemTrait, Path, TraitItem, TraitItemConst, TraitItemFn}; use crate::class::ClassEntryAttribute; use crate::constant::PhpConstAttribute; use crate::function::{Args, Function}; use crate::helpers::{get_docs, CleanPhpAttr}; +use darling::util::Flag; +use darling::FromAttributes; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{Expr, Ident, ItemTrait, Path, TraitItem, TraitItemConst, TraitItemFn}; use crate::impl_::{Constant, FnBuilder, MethodModifier}; use crate::parsing::{PhpRename, RenameRule, Visibility}; @@ -34,23 +34,22 @@ pub fn parser(mut input: ItemTrait) -> Result { let name = attr.rename.rename(ident.to_string(), RenameRule::Pascal); input.attrs.clean_php(); - let methods: Vec = input.items.iter_mut() - .flat_map( - |item: &mut TraitItem| { - match item { - TraitItem::Fn(f) => Some(f), - _ => None, - } - }) + let methods: Vec = input + .items + .iter_mut() + .flat_map(|item: &mut TraitItem| match item { + TraitItem::Fn(f) => Some(f), + _ => None, + }) .flat_map(|f| f.parse()) .collect(); - let constants: Vec<_> = input.items.iter_mut() - .flat_map(|item: &mut TraitItem| { - match item { - TraitItem::Const(c) => Some(c), - _ => None, - } + let constants: Vec<_> = input + .items + .iter_mut() + .flat_map(|item: &mut TraitItem| match item { + TraitItem::Const(c) => Some(c), + _ => None, }) .flat_map(|c| c.parse()) .map(|c| { @@ -63,19 +62,19 @@ pub fn parser(mut input: ItemTrait) -> Result { }) .collect(); - let impl_const: Vec<&TraitItemConst> = input.items.iter().flat_map(|item| { - match item { + let impl_const: Vec<&TraitItemConst> = input + .items + .iter() + .flat_map(|item| match item { TraitItem::Const(c) => Some(c), _ => None, - } - }) - .map(|c| { + }) + .flat_map(|c| { if c.default.is_none() { bail!("Interface const cannot be empty"); } Ok(c) }) - .flat_map(|c| c) .collect(); let implements = attr.extends; @@ -256,17 +255,12 @@ impl<'a> Parse<'a, Constant<'a>> for TraitItemConst { impl<'a> Parse<'a, FnBuilder> for TraitItemFn { fn parse(&'a mut self) -> Result { - let php_attr = PhpFunctionInterfaceAttribute::from_attributes( - &self.attrs - )?; + let php_attr = PhpFunctionInterfaceAttribute::from_attributes(&self.attrs)?; if self.default.is_some() { bail!("Interface could not have default impl"); } - let mut args = Args::parse_from_fnargs( - self.sig.inputs.iter(), - php_attr.defaults - )?; + let mut args = Args::parse_from_fnargs(self.sig.inputs.iter(), php_attr.defaults)?; let docs = get_docs(&php_attr.attrs)?; self.attrs.clean_php(); diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index c6880de93..640438d5d 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -15,7 +15,9 @@ mod zval; use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; -use syn::{DeriveInput, ItemConst, ItemEnum, ItemFn, ItemForeignMod, ItemImpl, ItemStruct, ItemTrait}; +use syn::{ + DeriveInput, ItemConst, ItemEnum, ItemFn, ItemForeignMod, ItemImpl, ItemStruct, ItemTrait, +}; extern crate proc_macro; @@ -386,11 +388,9 @@ pub fn php_interface(args: TokenStream, input: TokenStream) -> TokenStream { } fn php_interface_internal(_args: TokenStream2, input: TokenStream2) -> TokenStream2 { - let input = parse_macro_input2!(input as ItemTrait).into(); + let input = parse_macro_input2!(input as ItemTrait); - interface::parser(input) - .unwrap_or_else(|e| e.to_compile_error()) - .into() + interface::parser(input).unwrap_or_else(|e| e.to_compile_error()) } // BEGIN DOCS FROM function.md diff --git a/src/builders/class.rs b/src/builders/class.rs index db65ee044..b95cbfb12 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -308,7 +308,7 @@ impl ClassBuilder { let class = if self.ce.flags().contains(ClassFlags::Interface) { unsafe { - zend_register_internal_interface(&mut self.ce) + zend_register_internal_interface(&raw mut self.ce) .as_mut() .ok_or(Error::InvalidPointer)? } diff --git a/src/builders/module.rs b/src/builders/module.rs index 478a18d26..f25e4ace8 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -1,8 +1,6 @@ use std::{convert::TryFrom, ffi::CString, mem, ptr}; use super::{ClassBuilder, FunctionBuilder}; -#[cfg(feature = "enum")] -use crate::{builders::enum_builder::EnumBuilder, enum_::RegisteredEnum}; use crate::{ builders::class::InterfaceBuilder, class::RegisteredClass, @@ -13,6 +11,8 @@ use crate::{ zend::{FunctionEntry, ModuleEntry}, PHP_DEBUG, PHP_ZTS, }; +#[cfg(feature = "enum")] +use crate::{builders::enum_builder::EnumBuilder, enum_::RegisteredEnum}; /// Builds a Zend module extension to be registered with PHP. Must be called /// from within an external function called `get_module`, returning a mutable @@ -193,6 +193,11 @@ impl ModuleBuilder<'_> { self } + /// Adds a interface to the extension. + /// + /// # Panics + /// + /// * Panics if a constant could not be registered. pub fn interface(mut self) -> Self { self.classes.push(|| { let mut builder = InterfaceBuilder::new(T::CLASS_NAME); diff --git a/tests/src/integration/interface/mod.rs b/tests/src/integration/interface/mod.rs index fa6c7109c..788d24f4a 100644 --- a/tests/src/integration/interface/mod.rs +++ b/tests/src/integration/interface/mod.rs @@ -1,15 +1,14 @@ use std::collections::HashMap; use ext_php_rs::types::ZendClassObject; +use ext_php_rs::zend::ce; use ext_php_rs::{php_class, php_impl, php_interface}; use ext_php_rs::{php_module, prelude::ModuleBuilder}; -use ext_php_rs::zend::ce; #[php_interface] #[php(extends(ce = ce::throwable, stub = "\\Throwable"))] #[php(name = "ExtPhpRs\\Interface\\EmptyObjectInterface")] -pub trait EmptyObjectTrait -{ +pub trait EmptyObjectTrait { const HELLO: &'static str = "HELLO"; fn void(); @@ -19,7 +18,7 @@ pub trait EmptyObjectTrait fn ref_to_like_this_class( &self, data: String, - other: &ZendClassObject + other: &ZendClassObject, ) -> String; } diff --git a/tests/src/integration/mod.rs b/tests/src/integration/mod.rs index c472e4ff2..59c06d36a 100644 --- a/tests/src/integration/mod.rs +++ b/tests/src/integration/mod.rs @@ -9,6 +9,7 @@ pub mod defaults; pub mod enum_; pub mod exception; pub mod globals; +pub mod interface; pub mod iterator; pub mod magic_method; pub mod nullable; @@ -17,7 +18,6 @@ pub mod object; pub mod string; pub mod types; pub mod variadic_args; -pub mod interface; #[cfg(test)] mod test { From f99b28a819129e4b43d70a1a55149c7818a1b751 Mon Sep 17 00:00:00 2001 From: norbytus Date: Mon, 21 Jul 2025 21:55:55 +0300 Subject: [PATCH 11/25] feat: Change const registration for interface --- crates/macros/src/interface.rs | 76 ++++++++++++++-------------------- src/builders/class.rs | 40 ------------------ src/builders/interface.rs | 45 ++++++++++++++++++++ src/builders/mod.rs | 1 + src/builders/module.rs | 2 +- src/interface.rs | 5 +++ src/lib.rs | 1 + 7 files changed, 83 insertions(+), 87 deletions(-) create mode 100644 src/builders/interface.rs create mode 100644 src/interface.rs diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index d21a6a65a..a11cdb118 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -34,48 +34,9 @@ pub fn parser(mut input: ItemTrait) -> Result { let name = attr.rename.rename(ident.to_string(), RenameRule::Pascal); input.attrs.clean_php(); - let methods: Vec = input - .items - .iter_mut() - .flat_map(|item: &mut TraitItem| match item { - TraitItem::Fn(f) => Some(f), - _ => None, - }) - .flat_map(|f| f.parse()) - .collect(); - - let constants: Vec<_> = input - .items - .iter_mut() - .flat_map(|item: &mut TraitItem| match item { - TraitItem::Const(c) => Some(c), - _ => None, - }) - .flat_map(|c| c.parse()) - .map(|c| { - let name = &c.name; - let ident = c.ident; - let docs = &c.docs; - quote! { - (#name, &#path::#ident, &[#(#docs),*]) - } - }) - .collect(); + let methods: Vec = input.parse()?; - let impl_const: Vec<&TraitItemConst> = input - .items - .iter() - .flat_map(|item| match item { - TraitItem::Const(c) => Some(c), - _ => None, - }) - .flat_map(|c| { - if c.default.is_none() { - bail!("Interface const cannot be empty"); - } - Ok(c) - }) - .collect(); + let constants: Vec = input.parse()?; let implements = attr.extends; @@ -84,10 +45,6 @@ pub fn parser(mut input: ItemTrait) -> Result { pub struct #interface_name; - impl #interface_name { - #(pub #impl_const)* - } - impl ::ext_php_rs::class::RegisteredClass for #interface_name { const CLASS_NAME: &'static str = #name; @@ -153,7 +110,7 @@ pub fn parser(mut input: ItemTrait) -> Result { } fn get_constants(self) -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn, &'static [&'static str])] { - &[#(#constants),*] + &[] } } @@ -242,6 +199,33 @@ trait Parse<'a, T> { fn parse(&'a mut self) -> Result; } +impl<'a> Parse<'a, Vec> for ItemTrait { + fn parse(&'a mut self) -> Result> { + Ok(self + .items + .iter_mut() + .filter_map(|item: &mut TraitItem| match item { + TraitItem::Fn(f) => Some(f), + _ => None, + }) + .flat_map(Parse::parse) + .collect()) + } +} + +impl<'a> Parse<'a, Vec>> for ItemTrait { + fn parse(&'a mut self) -> Result>> { + Ok(self.items + .iter_mut() + .filter_map(|item: &mut TraitItem| match item { + TraitItem::Const(c) => Some(c), + _ => None, + }) + .flat_map(Parse::parse) + .collect()) + } +} + impl<'a> Parse<'a, Constant<'a>> for TraitItemConst { fn parse(&'a mut self) -> Result> { let attr = PhpConstAttribute::from_attributes(&self.attrs)?; diff --git a/src/builders/class.rs b/src/builders/class.rs index b95cbfb12..afdb0057d 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -386,46 +386,6 @@ impl ClassBuilder { } } -pub struct InterfaceBuilder { - class_builder: ClassBuilder, -} - -impl InterfaceBuilder { - pub fn new>(name: T) -> Self { - Self { - class_builder: ClassBuilder::new(name), - } - } - - pub fn implements(mut self, interface: ClassEntryInfo) -> Self { - self.class_builder = self.class_builder.implements(interface); - - self - } - - pub fn method(mut self, func: FunctionBuilder<'static>, flags: MethodFlags) -> Self { - self.class_builder = self.class_builder.method(func, flags); - - self - } - - pub fn dyn_constant>( - mut self, - name: T, - value: &'static dyn IntoZvalDyn, - docs: DocComments, - ) -> Result { - self.class_builder = self.class_builder.dyn_constant(name, value, docs)?; - - Ok(self) - } - - pub fn builder(mut self) -> ClassBuilder { - self.class_builder = self.class_builder.flags(ClassFlags::Interface); - self.class_builder - } -} - #[cfg(test)] mod tests { use crate::test::test_function; diff --git a/src/builders/interface.rs b/src/builders/interface.rs new file mode 100644 index 000000000..d40a47bc6 --- /dev/null +++ b/src/builders/interface.rs @@ -0,0 +1,45 @@ +use crate::builders::FunctionBuilder; +use crate::flags::{ClassFlags, MethodFlags}; +use crate::{builders::ClassBuilder, class::ClassEntryInfo, convert::IntoZvalDyn, describe::DocComments}; +use crate::error::Result; + +pub struct InterfaceBuilder { + class_builder: ClassBuilder, +} + +impl InterfaceBuilder { + pub fn new>(name: T) -> Self { + Self { + class_builder: ClassBuilder::new(name), + } + } + + pub fn implements(mut self, interface: ClassEntryInfo) -> Self { + self.class_builder = self.class_builder.implements(interface); + + self + } + + pub fn method(mut self, func: FunctionBuilder<'static>, flags: MethodFlags) -> Self { + self.class_builder = self.class_builder.method(func, flags); + + self + } + + pub fn dyn_constant>( + mut self, + name: T, + value: &'static dyn IntoZvalDyn, + docs: DocComments, + ) -> Result { + self.class_builder = self.class_builder.dyn_constant(name, value, docs)?; + + Ok(self) + } + + pub fn builder(mut self) -> ClassBuilder { + self.class_builder = self.class_builder.flags(ClassFlags::Interface); + self.class_builder + } +} + diff --git a/src/builders/mod.rs b/src/builders/mod.rs index c439c8168..b49ab5367 100644 --- a/src/builders/mod.rs +++ b/src/builders/mod.rs @@ -5,6 +5,7 @@ mod class; #[cfg(feature = "enum")] mod enum_builder; mod function; +mod interface; #[cfg(all(php82, feature = "embed"))] mod ini; mod module; diff --git a/src/builders/module.rs b/src/builders/module.rs index f25e4ace8..48ce54fef 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}; use crate::{ - builders::class::InterfaceBuilder, + builders::interface::InterfaceBuilder, class::RegisteredClass, constant::IntoConst, describe::DocComments, diff --git a/src/interface.rs b/src/interface.rs new file mode 100644 index 000000000..8bd092295 --- /dev/null +++ b/src/interface.rs @@ -0,0 +1,5 @@ +use crate::{convert::IntoZval, describe::DocComments}; + +pub trait RegisteredInterface { + fn constants() -> &'static [(&'static str, &'static impl IntoZval, DocComments)]; +} diff --git a/src/lib.rs b/src/lib.rs index b7a533a73..d8d26666e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ pub mod flags; pub mod macros; pub mod boxed; pub mod class; +pub mod interface; #[cfg(any(docs, feature = "closure"))] #[cfg_attr(docs, doc(cfg(feature = "closure")))] pub mod closure; From 75ce70ee13fdabf6a0cb1e47bb0f36a0015c4168 Mon Sep 17 00:00:00 2001 From: norbytus Date: Tue, 22 Jul 2025 19:49:56 +0300 Subject: [PATCH 12/25] feat: Rewrite attribute parse --- crates/macros/src/interface.rs | 58 +++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index a11cdb118..49006d024 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -7,10 +7,10 @@ use crate::helpers::{get_docs, CleanPhpAttr}; use darling::util::Flag; use darling::FromAttributes; use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, ToTokens}; use syn::{Expr, Ident, ItemTrait, Path, TraitItem, TraitItemConst, TraitItemFn}; -use crate::impl_::{Constant, FnBuilder, MethodModifier}; +use crate::impl_::{FnBuilder, MethodModifier}; use crate::parsing::{PhpRename, RenameRule, Visibility}; use crate::prelude::*; @@ -110,7 +110,7 @@ pub fn parser(mut input: ItemTrait) -> Result { } fn get_constants(self) -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn, &'static [&'static str])] { - &[] + &[#(#constants),*] } } @@ -195,10 +195,32 @@ pub struct PhpFunctionInterfaceAttribute { constructor: Flag, } +#[derive(Default)] +struct InterfaceData<'a> { + attrs: StructAttributes, + methods: Vec, + constants: Vec> +} + trait Parse<'a, T> { fn parse(&'a mut self) -> Result; } +impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait { + fn parse(&'a mut self) -> Result> { + let mut data = InterfaceData::default(); + for item in self.items.iter_mut() { + match item { + TraitItem::Fn(f) => data.methods.push(f.parse()?), + TraitItem::Const(c) => data.constants.push(c.parse()?), + _ => {} + } + } + + Ok(data) + } +} + impl<'a> Parse<'a, Vec> for ItemTrait { fn parse(&'a mut self) -> Result> { Ok(self @@ -226,14 +248,42 @@ impl<'a> Parse<'a, Vec>> for ItemTrait { } } +struct Constant<'a> { + name: String, + expr: &'a Expr, + docs: Vec, +} + +impl ToTokens for Constant<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let name = &self.name; + let expr = &self.expr; + let docs = &self.docs; + quote! { + (#name, #expr, &[#(#docs),*]) + }.to_tokens(tokens); + } +} + +impl<'a> Constant<'a> { + fn new(name: String, expr: &'a Expr, docs: Vec) -> Self { + Self {name, expr, docs} + } +} + impl<'a> Parse<'a, Constant<'a>> for TraitItemConst { fn parse(&'a mut self) -> Result> { + if self.default.is_none() { + bail!("Interface const could not be empty"); + } + let attr = PhpConstAttribute::from_attributes(&self.attrs)?; let name = self.ident.to_string(); let docs = get_docs(&attr.attrs)?; self.attrs.clean_php(); - Ok(Constant::new(name, &self.ident, docs)) + let (_, expr) = self.default.as_ref().unwrap(); + Ok(Constant::new(name, expr, docs)) } } From 04fdc1029a70dd0ff50804d0cccd51fde22991a4 Mon Sep 17 00:00:00 2001 From: norbytus Date: Mon, 4 Aug 2025 17:39:38 +0300 Subject: [PATCH 13/25] refactor: Change parser function --- crates/macros/src/impl_.rs | 6 - crates/macros/src/interface.rs | 410 ++++++++++-------- src/builders/interface.rs | 7 +- src/builders/mod.rs | 2 +- src/builders/module.rs | 4 +- src/lib.rs | 2 +- tests/src/integration/interface/interface.php | 3 - tests/src/integration/interface/mod.rs | 2 - 8 files changed, 227 insertions(+), 209 deletions(-) diff --git a/crates/macros/src/impl_.rs b/crates/macros/src/impl_.rs index 429aa3827..210f5712e 100644 --- a/crates/macros/src/impl_.rs +++ b/crates/macros/src/impl_.rs @@ -160,12 +160,6 @@ pub struct Constant<'a> { pub docs: Vec, } -impl<'a> Constant<'a> { - pub fn new(name: String, ident: &'a syn::Ident, docs: Vec) -> Self { - Self { name, ident, docs } - } -} - impl<'a> ParsedImpl<'a> { /// Create a new, empty parsed impl block. /// diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index 49006d024..c51464ed8 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -24,79 +24,88 @@ pub struct StructAttributes { } pub fn parser(mut input: ItemTrait) -> Result { - let attr = StructAttributes::from_attributes(&input.attrs)?; - let ident = &input.ident; - - let interface_name = format_ident!("PhpInterface{ident}"); - let ts = quote! { #interface_name }; - let path: Path = syn::parse2(ts)?; - - let name = attr.rename.rename(ident.to_string(), RenameRule::Pascal); - input.attrs.clean_php(); - - let methods: Vec = input.parse()?; - - let constants: Vec = input.parse()?; - - let implements = attr.extends; + let interface_data: InterfaceData = input.parse()?; + let interface_tokens = quote! { #interface_data }; Ok(quote! { #input - pub struct #interface_name; + #interface_tokens + }) +} - impl ::ext_php_rs::class::RegisteredClass for #interface_name { - const CLASS_NAME: &'static str = #name; +trait Parse<'a, T> { + fn parse(&'a mut self) -> Result; +} - const BUILDER_MODIFIER: Option< - fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder, - > = None; +struct InterfaceData<'a> { + ident: &'a Ident, + name: String, + path: Path, + attrs: StructAttributes, + methods: Vec, + constants: Vec>, +} - const EXTENDS: Option<::ext_php_rs::class::ClassEntryInfo> = None; +impl ToTokens for InterfaceData<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let interface_name = format_ident!("PhpInterface{}", self.ident); + let name = &self.name; + let implements = &self.attrs.extends; + let methods_sig = &self.methods; + let path = &self.path; + quote! { + pub struct #interface_name; - const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::Interface; + impl ::ext_php_rs::class::RegisteredClass for #interface_name { + const CLASS_NAME: &'static str = #name; - const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[ - #(#implements,)* - ]; + const BUILDER_MODIFIER: Option< + fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder, + > = None; - fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { - static METADATA: ::ext_php_rs::class::ClassMetadata<#interface_name> = - ::ext_php_rs::class::ClassMetadata::new(); + const EXTENDS: Option<::ext_php_rs::class::ClassEntryInfo> = None; - &METADATA - } + const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::Interface; - fn method_builders() -> Vec<( - ::ext_php_rs::builders::FunctionBuilder<'static>, - ::ext_php_rs::flags::MethodFlags, - )> { - vec![#(#methods),*] - } + const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[ + #(#implements,)* + ]; - fn constructor() -> Option<::ext_php_rs::class::ConstructorMeta> { - None - } + fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { + static METADATA: ::ext_php_rs::class::ClassMetadata<#interface_name> = + ::ext_php_rs::class::ClassMetadata::new(); - fn constants() -> &'static [( - &'static str, - &'static dyn ext_php_rs::convert::IntoZvalDyn, - ext_php_rs::describe::DocComments, - )] { - use ::ext_php_rs::internal::class::PhpClassImpl; - ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_constants() - } + &METADATA + } + + fn method_builders() -> Vec<( + ::ext_php_rs::builders::FunctionBuilder<'static>, + ::ext_php_rs::flags::MethodFlags, + )> { + vec![#(#methods_sig),*] + } - fn get_properties<'a>() -> std::collections::HashMap<&'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self>> { - HashMap::new() + fn constructor() -> Option<::ext_php_rs::class::ConstructorMeta> { + None + } + fn constants() -> &'static [( + &'static str, + &'static dyn ext_php_rs::convert::IntoZvalDyn, + ext_php_rs::describe::DocComments, + )] { + use ::ext_php_rs::internal::class::PhpClassImpl; + ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_constants() + } + + fn get_properties<'a>() -> std::collections::HashMap<&'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self>> { + HashMap::new() + } } - } - impl ::ext_php_rs::internal::class::PhpClassImpl<#path> - for ::ext_php_rs::internal::class::PhpClassImplCollector<#path> - { + impl ::ext_php_rs::internal::class::PhpClassImpl<#path> for ::ext_php_rs::internal::class::PhpClassImplCollector<#path> { fn get_methods(self) -> ::std::vec::Vec< - (::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags) + (::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags) > { vec![] } @@ -110,75 +119,118 @@ pub fn parser(mut input: ItemTrait) -> Result { } fn get_constants(self) -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn, &'static [&'static str])] { - &[#(#constants),*] + &[] } } - - impl<'a> ::ext_php_rs::convert::FromZendObject<'a> for &'a #interface_name { - #[inline] - fn from_zend_object( - obj: &'a ::ext_php_rs::types::ZendObject, - ) -> ::ext_php_rs::error::Result { - let obj = ::ext_php_rs::types::ZendClassObject::<#interface_name>::from_zend_obj(obj) - .ok_or(::ext_php_rs::error::Error::InvalidScope)?; - Ok(&**obj) + impl<'a> ::ext_php_rs::convert::FromZendObject<'a> for &'a #interface_name { + #[inline] + fn from_zend_object( + obj: &'a ::ext_php_rs::types::ZendObject, + ) -> ::ext_php_rs::error::Result { + let obj = ::ext_php_rs::types::ZendClassObject::<#interface_name>::from_zend_obj(obj) + .ok_or(::ext_php_rs::error::Error::InvalidScope)?; + Ok(&**obj) + } } - } - impl<'a> ::ext_php_rs::convert::FromZendObjectMut<'a> for &'a mut #interface_name { - #[inline] - fn from_zend_object_mut( - obj: &'a mut ::ext_php_rs::types::ZendObject, - ) -> ::ext_php_rs::error::Result { - let obj = ::ext_php_rs::types::ZendClassObject::<#interface_name>::from_zend_obj_mut(obj) - .ok_or(::ext_php_rs::error::Error::InvalidScope)?; - Ok(&mut **obj) + impl<'a> ::ext_php_rs::convert::FromZendObjectMut<'a> for &'a mut #interface_name { + #[inline] + fn from_zend_object_mut( + obj: &'a mut ::ext_php_rs::types::ZendObject, + ) -> ::ext_php_rs::error::Result { + let obj = ::ext_php_rs::types::ZendClassObject::<#interface_name>::from_zend_obj_mut(obj) + .ok_or(::ext_php_rs::error::Error::InvalidScope)?; + Ok(&mut **obj) + } } - } - impl<'a> ::ext_php_rs::convert::FromZval<'a> for &'a #interface_name { - const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(Some( - <#interface_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME, - )); - #[inline] - fn from_zval(zval: &'a ::ext_php_rs::types::Zval) -> ::std::option::Option { - ::from_zend_object(zval.object()?).ok() + impl<'a> ::ext_php_rs::convert::FromZval<'a> for &'a #interface_name { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(Some( + <#interface_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME, + )); + #[inline] + fn from_zval(zval: &'a ::ext_php_rs::types::Zval) -> ::std::option::Option { + ::from_zend_object(zval.object()?).ok() + } } - } - impl<'a> ::ext_php_rs::convert::FromZvalMut<'a> for &'a mut #interface_name { - const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(Some( - <#interface_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME, - )); - #[inline] - fn from_zval_mut(zval: &'a mut ::ext_php_rs::types::Zval) -> ::std::option::Option { - ::from_zend_object_mut(zval.object_mut()?) - .ok() + impl<'a> ::ext_php_rs::convert::FromZvalMut<'a> for &'a mut #interface_name { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(Some( + <#interface_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME, + )); + #[inline] + fn from_zval_mut(zval: &'a mut ::ext_php_rs::types::Zval) -> ::std::option::Option { + ::from_zend_object_mut(zval.object_mut()?) + .ok() + } } - } - impl ::ext_php_rs::convert::IntoZendObject for #interface_name { - #[inline] - fn into_zend_object( - self, - ) -> ::ext_php_rs::error::Result<::ext_php_rs::boxed::ZBox<::ext_php_rs::types::ZendObject>> - { - Ok(::ext_php_rs::types::ZendClassObject::new(self).into()) + impl ::ext_php_rs::convert::IntoZendObject for #interface_name { + #[inline] + fn into_zend_object( + self, + ) -> ::ext_php_rs::error::Result<::ext_php_rs::boxed::ZBox<::ext_php_rs::types::ZendObject>> + { + Ok(::ext_php_rs::types::ZendClassObject::new(self).into()) + } } + impl ::ext_php_rs::convert::IntoZval for #interface_name { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(Some( + <#interface_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME, + )); + const NULLABLE: bool = false; + #[inline] + fn set_zval( + self, + zv: &mut ::ext_php_rs::types::Zval, + persistent: bool, + ) -> ::ext_php_rs::error::Result<()> { + use ::ext_php_rs::convert::IntoZendObject; + self.into_zend_object()?.set_zval(zv, persistent) + } + } + }.to_tokens(tokens); + } +} + +impl<'a> InterfaceData<'a> { + fn new( + ident: &'a Ident, + name: String, + path: Path, + attrs: StructAttributes, + methods: Vec, + constants: Vec>, + ) -> Self { + Self { + ident, + name, + path, + attrs, + methods, + constants, } - impl ::ext_php_rs::convert::IntoZval for #interface_name { - const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(Some( - <#interface_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME, - )); - const NULLABLE: bool = false; - #[inline] - fn set_zval( - self, - zv: &mut ::ext_php_rs::types::Zval, - persistent: bool, - ) -> ::ext_php_rs::error::Result<()> { - use ::ext_php_rs::convert::IntoZendObject; - self.into_zend_object()?.set_zval(zv, persistent) + } +} + +impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait { + fn parse(&'a mut self) -> Result> { + let attrs = StructAttributes::from_attributes(&self.attrs)?; + let ident = &self.ident; + let name = attrs.rename.rename(ident.to_string(), RenameRule::Pascal); + self.attrs.clean_php(); + let interface_name = format_ident!("PhpInterface{ident}"); + let ts = quote! { #interface_name }; + let path: Path = syn::parse2(ts)?; + let mut data = InterfaceData::new(ident, name, path, attrs, Vec::new(), Vec::new()); + + for item in &mut self.items { + match item { + TraitItem::Fn(f) => data.methods.push(f.parse()?), + TraitItem::Const(c) => data.constants.push(c.parse()?), + _ => {} } } - }) + + Ok(data) + } } #[derive(FromAttributes, Default, Debug)] @@ -195,56 +247,55 @@ pub struct PhpFunctionInterfaceAttribute { constructor: Flag, } -#[derive(Default)] -struct InterfaceData<'a> { - attrs: StructAttributes, - methods: Vec, - constants: Vec> -} +impl<'a> Parse<'a, FnBuilder> for TraitItemFn { + fn parse(&'a mut self) -> Result { + let php_attr = PhpFunctionInterfaceAttribute::from_attributes(&self.attrs)?; + if self.default.is_some() { + bail!("Interface could not have default impl"); + } -trait Parse<'a, T> { - fn parse(&'a mut self) -> Result; -} + let mut args = Args::parse_from_fnargs(self.sig.inputs.iter(), php_attr.defaults)?; + let docs = get_docs(&php_attr.attrs)?; -impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait { - fn parse(&'a mut self) -> Result> { - let mut data = InterfaceData::default(); - for item in self.items.iter_mut() { - match item { - TraitItem::Fn(f) => data.methods.push(f.parse()?), - TraitItem::Const(c) => data.constants.push(c.parse()?), - _ => {} - } + self.attrs.clean_php(); + + let mut modifiers: HashSet = HashSet::new(); + modifiers.insert(MethodModifier::Abstract); + if args.typed.first().is_some_and(|arg| arg.name == "self_") { + args.typed.pop(); + } else if args.receiver.is_none() { + modifiers.insert(MethodModifier::Static); } - Ok(data) - } -} + let f = Function::new( + &self.sig, + php_attr + .rename + .rename(self.sig.ident.to_string(), RenameRule::Camel), + args, + php_attr.optional, + docs, + ); -impl<'a> Parse<'a, Vec> for ItemTrait { - fn parse(&'a mut self) -> Result> { - Ok(self - .items - .iter_mut() - .filter_map(|item: &mut TraitItem| match item { - TraitItem::Fn(f) => Some(f), - _ => None, + Ok(FnBuilder { + builder: f.abstract_function_builder(), + vis: php_attr.vis.unwrap_or(Visibility::Public), + modifiers, }) - .flat_map(Parse::parse) - .collect()) } } -impl<'a> Parse<'a, Vec>> for ItemTrait { - fn parse(&'a mut self) -> Result>> { - Ok(self.items - .iter_mut() - .filter_map(|item: &mut TraitItem| match item { - TraitItem::Const(c) => Some(c), - _ => None, - }) - .flat_map(Parse::parse) - .collect()) +impl<'a> Parse<'a, Vec> for ItemTrait { + fn parse(&'a mut self) -> Result> { + Ok(self + .items + .iter_mut() + .filter_map(|item: &mut TraitItem| match item { + TraitItem::Fn(f) => Some(f), + _ => None, + }) + .flat_map(Parse::parse) + .collect()) } } @@ -261,13 +312,14 @@ impl ToTokens for Constant<'_> { let docs = &self.docs; quote! { (#name, #expr, &[#(#docs),*]) - }.to_tokens(tokens); + } + .to_tokens(tokens); } } impl<'a> Constant<'a> { fn new(name: String, expr: &'a Expr, docs: Vec) -> Self { - Self {name, expr, docs} + Self { name, expr, docs } } } @@ -287,40 +339,16 @@ impl<'a> Parse<'a, Constant<'a>> for TraitItemConst { } } -impl<'a> Parse<'a, FnBuilder> for TraitItemFn { - fn parse(&'a mut self) -> Result { - let php_attr = PhpFunctionInterfaceAttribute::from_attributes(&self.attrs)?; - if self.default.is_some() { - bail!("Interface could not have default impl"); - } - - let mut args = Args::parse_from_fnargs(self.sig.inputs.iter(), php_attr.defaults)?; - let docs = get_docs(&php_attr.attrs)?; - - self.attrs.clean_php(); - - let mut modifiers: HashSet = HashSet::new(); - modifiers.insert(MethodModifier::Abstract); - if args.typed.first().is_some_and(|arg| arg.name == "self_") { - args.typed.pop(); - } else if args.receiver.is_none() { - modifiers.insert(MethodModifier::Static); - }; - - let f = Function::new( - &self.sig, - php_attr - .rename - .rename(self.sig.ident.to_string(), RenameRule::Camel), - args, - php_attr.optional, - docs, - ); - - Ok(FnBuilder { - builder: f.abstract_function_builder(), - vis: php_attr.vis.unwrap_or(Visibility::Public), - modifiers, - }) +impl<'a> Parse<'a, Vec>> for ItemTrait { + fn parse(&'a mut self) -> Result>> { + Ok(self + .items + .iter_mut() + .filter_map(|item: &mut TraitItem| match item { + TraitItem::Const(c) => Some(c), + _ => None, + }) + .flat_map(Parse::parse) + .collect()) } } diff --git a/src/builders/interface.rs b/src/builders/interface.rs index d40a47bc6..b247e41ae 100644 --- a/src/builders/interface.rs +++ b/src/builders/interface.rs @@ -1,7 +1,9 @@ use crate::builders::FunctionBuilder; -use crate::flags::{ClassFlags, MethodFlags}; -use crate::{builders::ClassBuilder, class::ClassEntryInfo, convert::IntoZvalDyn, describe::DocComments}; use crate::error::Result; +use crate::flags::{ClassFlags, MethodFlags}; +use crate::{ + builders::ClassBuilder, class::ClassEntryInfo, convert::IntoZvalDyn, describe::DocComments, +}; pub struct InterfaceBuilder { class_builder: ClassBuilder, @@ -42,4 +44,3 @@ impl InterfaceBuilder { self.class_builder } } - diff --git a/src/builders/mod.rs b/src/builders/mod.rs index b49ab5367..7920df93e 100644 --- a/src/builders/mod.rs +++ b/src/builders/mod.rs @@ -5,9 +5,9 @@ mod class; #[cfg(feature = "enum")] mod enum_builder; mod function; -mod interface; #[cfg(all(php82, feature = "embed"))] mod ini; +mod interface; mod module; #[cfg(feature = "embed")] mod sapi; diff --git a/src/builders/module.rs b/src/builders/module.rs index 48ce54fef..2c7b20896 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_::RegisteredEnum}; use crate::{ builders::interface::InterfaceBuilder, class::RegisteredClass, @@ -11,8 +13,6 @@ use crate::{ zend::{FunctionEntry, ModuleEntry}, PHP_DEBUG, PHP_ZTS, }; -#[cfg(feature = "enum")] -use crate::{builders::enum_builder::EnumBuilder, enum_::RegisteredEnum}; /// Builds a Zend module extension to be registered with PHP. Must be called /// from within an external function called `get_module`, returning a mutable diff --git a/src/lib.rs b/src/lib.rs index d8d26666e..548992167 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,6 @@ pub mod flags; pub mod macros; pub mod boxed; pub mod class; -pub mod interface; #[cfg(any(docs, feature = "closure"))] #[cfg_attr(docs, doc(cfg(feature = "closure")))] pub mod closure; @@ -31,6 +30,7 @@ pub mod describe; pub mod embed; #[cfg(feature = "enum")] pub mod enum_; +pub mod interface; #[doc(hidden)] pub mod internal; pub mod props; diff --git a/tests/src/integration/interface/interface.php b/tests/src/integration/interface/interface.php index e2a934b11..7d885696b 100644 --- a/tests/src/integration/interface/interface.php +++ b/tests/src/integration/interface/interface.php @@ -29,7 +29,4 @@ public function refToLikeThisClass( assert(is_a($f, Throwable::class)); assert($f->nonStatic('Rust') === 'Rust - TEST'); -assert(ExtPhpRs\Interface\EmptyObjectInterface::HELLO === "HELLO"); -assert($f::HELLO === "HELLO"); -assert(Test::HELLO === "HELLO"); assert($f->refToLikeThisClass('TEST', $f) === 'TEST - TEST | TEST - TEST'); diff --git a/tests/src/integration/interface/mod.rs b/tests/src/integration/interface/mod.rs index 788d24f4a..0018b668c 100644 --- a/tests/src/integration/interface/mod.rs +++ b/tests/src/integration/interface/mod.rs @@ -9,8 +9,6 @@ use ext_php_rs::{php_module, prelude::ModuleBuilder}; #[php(extends(ce = ce::throwable, stub = "\\Throwable"))] #[php(name = "ExtPhpRs\\Interface\\EmptyObjectInterface")] pub trait EmptyObjectTrait { - const HELLO: &'static str = "HELLO"; - fn void(); fn non_static(&self, data: String) -> String; From 9b13b24a50b3e001b8d2da12422584fdd0d4f29c Mon Sep 17 00:00:00 2001 From: norbytus Date: Mon, 4 Aug 2025 17:39:50 +0300 Subject: [PATCH 14/25] fix: Add path to hashmap --- crates/macros/src/interface.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index c51464ed8..df827e796 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -98,8 +98,8 @@ impl ToTokens for InterfaceData<'_> { ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_constants() } - fn get_properties<'a>() -> std::collections::HashMap<&'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self>> { - HashMap::new() + fn get_properties<'a>() -> ::std::collections::HashMap<&'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self>> { + ::std::collections::HashMap::new() } } From 19a902d184e953e50c503c5f11e148fe9377d1b1 Mon Sep 17 00:00:00 2001 From: norbytus Date: Mon, 4 Aug 2025 17:39:55 +0300 Subject: [PATCH 15/25] fix: Fix constant registration for interface --- crates/macros/src/interface.rs | 13 ++++++++----- tests/src/integration/interface/interface.php | 2 ++ tests/src/integration/interface/mod.rs | 6 ++++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index df827e796..866f67c24 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -54,6 +54,7 @@ impl ToTokens for InterfaceData<'_> { let implements = &self.attrs.extends; let methods_sig = &self.methods; let path = &self.path; + let constants = &self.constants; quote! { pub struct #interface_name; @@ -89,6 +90,7 @@ impl ToTokens for InterfaceData<'_> { fn constructor() -> Option<::ext_php_rs::class::ConstructorMeta> { None } + fn constants() -> &'static [( &'static str, &'static dyn ext_php_rs::convert::IntoZvalDyn, @@ -99,7 +101,7 @@ impl ToTokens for InterfaceData<'_> { } fn get_properties<'a>() -> ::std::collections::HashMap<&'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self>> { - ::std::collections::HashMap::new() + panic!("Non supported for Interface"); } } @@ -107,11 +109,11 @@ impl ToTokens for InterfaceData<'_> { fn get_methods(self) -> ::std::vec::Vec< (::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags) > { - vec![] + panic!("Non supported for Interface"); } fn get_method_props<'a>(self) -> ::std::collections::HashMap<&'static str, ::ext_php_rs::props::Property<'a, #path>> { - todo!() + panic!("Non supported for Interface"); } fn get_constructor(self) -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta<#path>> { @@ -119,7 +121,7 @@ impl ToTokens for InterfaceData<'_> { } fn get_constants(self) -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn, &'static [&'static str])] { - &[] + &[#(#constants),*] } } @@ -299,6 +301,7 @@ impl<'a> Parse<'a, Vec> for ItemTrait { } } +#[derive(Debug)] struct Constant<'a> { name: String, expr: &'a Expr, @@ -311,7 +314,7 @@ impl ToTokens for Constant<'_> { let expr = &self.expr; let docs = &self.docs; quote! { - (#name, #expr, &[#(#docs),*]) + (#name, &#expr, &[#(#docs),*]) } .to_tokens(tokens); } diff --git a/tests/src/integration/interface/interface.php b/tests/src/integration/interface/interface.php index 7d885696b..03622a176 100644 --- a/tests/src/integration/interface/interface.php +++ b/tests/src/integration/interface/interface.php @@ -30,3 +30,5 @@ public function refToLikeThisClass( assert(is_a($f, Throwable::class)); assert($f->nonStatic('Rust') === 'Rust - TEST'); assert($f->refToLikeThisClass('TEST', $f) === 'TEST - TEST | TEST - TEST'); +assert(ExtPhpRs\Interface\EmptyObjectInterface::STRING_CONST === 'STRING_CONST'); +assert(ExtPhpRs\Interface\EmptyObjectInterface::USIZE_CONST === 200); diff --git a/tests/src/integration/interface/mod.rs b/tests/src/integration/interface/mod.rs index 0018b668c..8152e8115 100644 --- a/tests/src/integration/interface/mod.rs +++ b/tests/src/integration/interface/mod.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use ext_php_rs::types::ZendClassObject; use ext_php_rs::zend::ce; use ext_php_rs::{php_class, php_impl, php_interface}; @@ -9,6 +7,10 @@ use ext_php_rs::{php_module, prelude::ModuleBuilder}; #[php(extends(ce = ce::throwable, stub = "\\Throwable"))] #[php(name = "ExtPhpRs\\Interface\\EmptyObjectInterface")] pub trait EmptyObjectTrait { + const STRING_CONST: &'static str = "STRING_CONST"; + + const USIZE_CONST: u64 = 200; + fn void(); fn non_static(&self, data: String) -> String; From 9c45dd8c6ed4be60213b38a5d2cb386aa6130686 Mon Sep 17 00:00:00 2001 From: norbytus Date: Mon, 4 Aug 2025 17:39:59 +0300 Subject: [PATCH 16/25] chore: Add test with default value --- tests/src/integration/interface/interface.php | 8 ++++++++ tests/src/integration/interface/mod.rs | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/src/integration/interface/interface.php b/tests/src/integration/interface/interface.php index 03622a176..9b343ea4d 100644 --- a/tests/src/integration/interface/interface.php +++ b/tests/src/integration/interface/interface.php @@ -9,6 +9,10 @@ final class Test extends Exception implements ExtPhpRs\Interface\EmptyObjectInterface { + public function __construct() + { + } + public static function void(): void { } @@ -24,6 +28,10 @@ public function refToLikeThisClass( ): string { return sprintf('%s | %s', $this->nonStatic($data), $other->nonStatic($data)); } + + public function setValue(?int $value = 0) { + + } } $f = new Test(); diff --git a/tests/src/integration/interface/mod.rs b/tests/src/integration/interface/mod.rs index 8152e8115..d04200b26 100644 --- a/tests/src/integration/interface/mod.rs +++ b/tests/src/integration/interface/mod.rs @@ -1,7 +1,7 @@ use ext_php_rs::types::ZendClassObject; use ext_php_rs::zend::ce; -use ext_php_rs::{php_class, php_impl, php_interface}; -use ext_php_rs::{php_module, prelude::ModuleBuilder}; +use ext_php_rs::php_interface; +use ext_php_rs::prelude::ModuleBuilder; #[php_interface] #[php(extends(ce = ce::throwable, stub = "\\Throwable"))] @@ -20,6 +20,9 @@ pub trait EmptyObjectTrait { data: String, other: &ZendClassObject, ) -> String; + + #[php(defaults(value = 0))] + fn set_value(&mut self, value: i32); } pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { From d354e48ed8707c0b4db37a0b93c666db3d3f547c Mon Sep 17 00:00:00 2001 From: norbytus Date: Mon, 4 Aug 2025 17:40:02 +0300 Subject: [PATCH 17/25] chore: Delete unnecessary impl generation --- crates/macros/src/interface.rs | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index 866f67c24..a5ccc1d56 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -88,7 +88,8 @@ impl ToTokens for InterfaceData<'_> { } fn constructor() -> Option<::ext_php_rs::class::ConstructorMeta> { - None + use ::ext_php_rs::internal::class::PhpClassImpl; + ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_constructor() } fn constants() -> &'static [( @@ -96,8 +97,7 @@ impl ToTokens for InterfaceData<'_> { &'static dyn ext_php_rs::convert::IntoZvalDyn, ext_php_rs::describe::DocComments, )] { - use ::ext_php_rs::internal::class::PhpClassImpl; - ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_constants() + &[#(#constants),*] } fn get_properties<'a>() -> ::std::collections::HashMap<&'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self>> { @@ -105,26 +105,6 @@ impl ToTokens for InterfaceData<'_> { } } - impl ::ext_php_rs::internal::class::PhpClassImpl<#path> for ::ext_php_rs::internal::class::PhpClassImplCollector<#path> { - fn get_methods(self) -> ::std::vec::Vec< - (::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags) - > { - panic!("Non supported for Interface"); - } - - fn get_method_props<'a>(self) -> ::std::collections::HashMap<&'static str, ::ext_php_rs::props::Property<'a, #path>> { - panic!("Non supported for Interface"); - } - - fn get_constructor(self) -> ::std::option::Option<::ext_php_rs::class::ConstructorMeta<#path>> { - None - } - - fn get_constants(self) -> &'static [(&'static str, &'static dyn ::ext_php_rs::convert::IntoZvalDyn, &'static [&'static str])] { - &[#(#constants),*] - } - } - impl<'a> ::ext_php_rs::convert::FromZendObject<'a> for &'a #interface_name { #[inline] fn from_zend_object( From e67f974c26c96ac43bb7e58e4d498adfff24e6b7 Mon Sep 17 00:00:00 2001 From: norbytus Date: Mon, 4 Aug 2025 17:40:04 +0300 Subject: [PATCH 18/25] feat: Define constructor --- crates/macros/src/interface.rs | 85 +++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index a5ccc1d56..17167e171 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -43,6 +43,7 @@ struct InterfaceData<'a> { name: String, path: Path, attrs: StructAttributes, + constructor: Option>, methods: Vec, constants: Vec>, } @@ -55,6 +56,12 @@ impl ToTokens for InterfaceData<'_> { let methods_sig = &self.methods; let path = &self.path; let constants = &self.constants; + + let constructor = self.constructor + .as_ref() + .map(|func| func.constructor_meta(&path)) + .option_tokens(); + quote! { pub struct #interface_name; @@ -88,8 +95,7 @@ impl ToTokens for InterfaceData<'_> { } fn constructor() -> Option<::ext_php_rs::class::ConstructorMeta> { - use ::ext_php_rs::internal::class::PhpClassImpl; - ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_constructor() + None } fn constants() -> &'static [( @@ -178,6 +184,7 @@ impl<'a> InterfaceData<'a> { name: String, path: Path, attrs: StructAttributes, + constructor: Option>, methods: Vec, constants: Vec>, ) -> Self { @@ -186,6 +193,7 @@ impl<'a> InterfaceData<'a> { name, path, attrs, + constructor, methods, constants, } @@ -201,11 +209,28 @@ impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait { let interface_name = format_ident!("PhpInterface{ident}"); let ts = quote! { #interface_name }; let path: Path = syn::parse2(ts)?; - let mut data = InterfaceData::new(ident, name, path, attrs, Vec::new(), Vec::new()); + let mut data = InterfaceData::new( + ident, + name, + path, + attrs, + None, + Vec::new(), + Vec::new() + ); for item in &mut self.items { match item { - TraitItem::Fn(f) => data.methods.push(f.parse()?), + TraitItem::Fn(f) => { + match f.parse()? { + MethodKind::Method(builder) => data.methods.push(builder), + MethodKind::Constructor(builder) => { + if data.constructor.replace(builder).is_some() { + bail!("Only one constructor can be provided per class."); + } + } + }; + }, TraitItem::Const(c) => data.constants.push(c.parse()?), _ => {} } @@ -229,20 +254,32 @@ pub struct PhpFunctionInterfaceAttribute { constructor: Flag, } -impl<'a> Parse<'a, FnBuilder> for TraitItemFn { - fn parse(&'a mut self) -> Result { - let php_attr = PhpFunctionInterfaceAttribute::from_attributes(&self.attrs)?; +enum MethodKind<'a> { + Method(FnBuilder), + Constructor(Function<'a>), +} + +impl<'a> Parse<'a, MethodKind<'a>> for TraitItemFn { + fn parse(&'a mut self) -> Result> { if self.default.is_some() { - bail!("Interface could not have default impl"); + bail!(self => "Interface could not have default impl"); } - let mut args = Args::parse_from_fnargs(self.sig.inputs.iter(), php_attr.defaults)?; - let docs = get_docs(&php_attr.attrs)?; - + let php_attr = PhpFunctionInterfaceAttribute::from_attributes( + &self.attrs + )?; self.attrs.clean_php(); + let mut args = Args::parse_from_fnargs( + self.sig.inputs.iter(), + php_attr.defaults + )?; + + let docs = get_docs(&php_attr.attrs)?; + let mut modifiers: HashSet = HashSet::new(); modifiers.insert(MethodModifier::Abstract); + if args.typed.first().is_some_and(|arg| arg.name == "self_") { args.typed.pop(); } else if args.receiver.is_none() { @@ -259,20 +296,26 @@ impl<'a> Parse<'a, FnBuilder> for TraitItemFn { docs, ); - Ok(FnBuilder { - builder: f.abstract_function_builder(), - vis: php_attr.vis.unwrap_or(Visibility::Public), - modifiers, - }) + if php_attr.constructor.is_present() { + Ok(MethodKind::Constructor(f)) + } else { + let builder = FnBuilder { + builder: f.abstract_function_builder(), + vis: php_attr.vis.unwrap_or(Visibility::Public), + modifiers, + }; + + Ok(MethodKind::Method(builder)) + } } } -impl<'a> Parse<'a, Vec> for ItemTrait { - fn parse(&'a mut self) -> Result> { +impl<'a> Parse<'a, Vec>> for ItemTrait { + fn parse(&'a mut self) -> Result>> { Ok(self .items .iter_mut() - .filter_map(|item: &mut TraitItem| match item { + .filter_map(|item| match item { TraitItem::Fn(f) => Some(f), _ => None, }) @@ -309,7 +352,7 @@ impl<'a> Constant<'a> { impl<'a> Parse<'a, Constant<'a>> for TraitItemConst { fn parse(&'a mut self) -> Result> { if self.default.is_none() { - bail!("Interface const could not be empty"); + bail!(self => "Interface const could not be empty"); } let attr = PhpConstAttribute::from_attributes(&self.attrs)?; @@ -327,7 +370,7 @@ impl<'a> Parse<'a, Vec>> for ItemTrait { Ok(self .items .iter_mut() - .filter_map(|item: &mut TraitItem| match item { + .filter_map(|item| match item { TraitItem::Const(c) => Some(c), _ => None, }) From b7d2273cd5966e5424a1a8ee854468bd591199bc Mon Sep 17 00:00:00 2001 From: norbytus Date: Mon, 4 Aug 2025 17:40:07 +0300 Subject: [PATCH 19/25] feat: Describe interface classes --- src/builders/class.rs | 4 ++++ src/describe/mod.rs | 12 +++++++++++- src/describe/stub.rs | 31 +++++++++++++++++++++++++++---- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/builders/class.rs b/src/builders/class.rs index afdb0057d..dba608cf5 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -58,6 +58,10 @@ impl ClassBuilder { } } + pub fn get_flags(&self) -> u32 { + self.ce.ce_flags + } + /// Sets the class builder to extend another class. /// /// # Parameters diff --git a/src/describe/mod.rs b/src/describe/mod.rs index 82238f0f5..2b40f71f3 100644 --- a/src/describe/mod.rs +++ b/src/describe/mod.rs @@ -7,7 +7,7 @@ use crate::builders::EnumBuilder; use crate::{ builders::{ClassBuilder, FunctionBuilder}, constant::IntoConst, - flags::{DataType, MethodFlags, PropertyFlags}, + flags::{ClassFlags, DataType, MethodFlags, PropertyFlags}, prelude::ModuleBuilder, }; use abi::{Option, RString, Str, Vec}; @@ -193,6 +193,8 @@ pub struct Class { pub methods: Vec, /// Constants of the class. pub constants: Vec, + /// Class flags + pub flags: u32, } #[cfg(feature = "closure")] @@ -225,15 +227,18 @@ impl Class { }), r#static: false, visibility: Visibility::Public, + r#abstract: false }] .into(), constants: StdVec::new().into(), + flags: 0, } } } impl From for Class { fn from(val: ClassBuilder) -> Self { + let flags = val.get_flags(); Self { name: val.name.into(), docs: DocBlock( @@ -269,6 +274,7 @@ impl From for Class { .map(Constant::from) .collect::>() .into(), + flags: flags } } } @@ -416,6 +422,8 @@ pub struct Method { pub r#static: bool, /// Visibility of the method. pub visibility: Visibility, + /// Not describe method body, if is abstract. + pub r#abstract: bool, } impl From<(FunctionBuilder<'_>, MethodFlags)> for Method { @@ -448,6 +456,7 @@ impl From<(FunctionBuilder<'_>, MethodFlags)> for Method { ty: flags.into(), r#static: flags.contains(MethodFlags::Static), visibility: flags.into(), + r#abstract: flags.contains(MethodFlags::Abstract), } } } @@ -685,6 +694,7 @@ mod tests { retval: Option::None, r#static: false, visibility: Visibility::Protected, + r#abstract: false } ); } diff --git a/src/describe/stub.rs b/src/describe/stub.rs index abfdba9d0..1737e2f82 100644 --- a/src/describe/stub.rs +++ b/src/describe/stub.rs @@ -16,7 +16,7 @@ use super::{ #[cfg(feature = "enum")] use crate::describe::{Enum, EnumCase}; -use crate::flags::DataType; +use crate::flags::{ClassFlags, DataType}; /// Implemented on types which can be converted into PHP stubs. pub trait ToStub { @@ -226,13 +226,20 @@ impl ToStub for Class { self.docs.fmt_stub(buf)?; let (_, name) = split_namespace(self.name.as_ref()); - write!(buf, "class {name} ")?; + let flags = ClassFlags::from_bits(self.flags).unwrap(); + let is_interface = flags.contains(ClassFlags::Interface); + + if is_interface { + write!(buf, "interface {name} ")?; + } else { + write!(buf, "class {name} ")?; + } if let Option::Some(extends) = &self.extends { write!(buf, "extends {extends} ")?; } - if !self.implements.is_empty() { + if !self.implements.is_empty() && !is_interface { write!( buf, "implements {} ", @@ -244,6 +251,18 @@ impl ToStub for Class { )?; } + if !self.implements.is_empty() && is_interface { + write!( + buf, + "extends {} ", + self.implements + .iter() + .map(RString::as_str) + .collect::>() + .join(", ") + )?; + } + writeln!(buf, "{{")?; buf.push_str( @@ -360,7 +379,11 @@ impl ToStub for Method { } } - writeln!(buf, " {{}}") + if !self.r#abstract { + writeln!(buf, " {{}}") + } else { + writeln!(buf, ";") + } } } From 473f628e6c125a16c4426c6d6e76220c508cce30 Mon Sep 17 00:00:00 2001 From: norbytus Date: Mon, 4 Aug 2025 17:40:09 +0300 Subject: [PATCH 20/25] feat: Separate interfaces from classes in module --- src/builders/module.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/builders/module.rs b/src/builders/module.rs index 2c7b20896..a7d70ac28 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -49,6 +49,7 @@ pub struct ModuleBuilder<'a> { pub(crate) functions: Vec>, pub(crate) constants: Vec<(String, Box, DocComments)>, pub(crate) classes: Vec ClassBuilder>, + pub(crate) interfaces: Vec ClassBuilder>, #[cfg(feature = "enum")] pub(crate) enums: Vec EnumBuilder>, startup_func: Option, @@ -199,7 +200,7 @@ impl ModuleBuilder<'_> { /// /// * Panics if a constant could not be registered. pub fn interface(mut self) -> Self { - self.classes.push(|| { + self.interfaces.push(|| { let mut builder = InterfaceBuilder::new(T::CLASS_NAME); for (method, flags) in T::method_builders() { builder = builder.method(method, flags); @@ -299,6 +300,7 @@ impl ModuleBuilder<'_> { pub struct ModuleStartup { constants: Vec<(String, Box)>, classes: Vec ClassBuilder>, + interfaces: Vec ClassBuilder>, #[cfg(feature = "enum")] enums: Vec EnumBuilder>, } @@ -323,6 +325,10 @@ impl ModuleStartup { c.register().expect("Failed to build class"); }); + self.interfaces.into_iter().map(|c| c()).for_each(|c| { + c.register().expect("Failed to build interface"); + }); + #[cfg(feature = "enum")] self.enums .into_iter() @@ -365,6 +371,7 @@ impl TryFrom> for (ModuleEntry, ModuleStartup) { .map(|(n, v, _)| (n, v)) .collect(), classes: builder.classes, + interfaces: builder.interfaces, #[cfg(feature = "enum")] enums: builder.enums, }; @@ -420,6 +427,7 @@ mod tests { assert!(builder.functions.is_empty()); assert!(builder.constants.is_empty()); assert!(builder.classes.is_empty()); + assert!(builder.interfaces.is_empty()); assert!(builder.startup_func.is_none()); assert!(builder.shutdown_func.is_none()); assert!(builder.request_startup_func.is_none()); From 855b49a891c00a068a905333ab2587977c12f0f7 Mon Sep 17 00:00:00 2001 From: norbytus Date: Sun, 10 Aug 2025 19:38:39 +0300 Subject: [PATCH 21/25] feat: Add doc about interface in guide --- guide/src/SUMMARY.md | 1 + guide/src/macros/index.md | 2 + guide/src/macros/interface.md | 73 +++++++++++++++++++ tests/src/integration/interface/interface.php | 4 - 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 guide/src/macros/interface.md diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index d5e18495c..b3fa7fc6b 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -27,6 +27,7 @@ - [Macros](./macros/index.md) - [Module](./macros/module.md) - [Function](./macros/function.md) + - [Interfaces](./macros/interface.md) - [Classes](./macros/classes.md) - [`impl`s](./macros/impl.md) - [Constants](./macros/constant.md) diff --git a/guide/src/macros/index.md b/guide/src/macros/index.md index 9fdcd56a9..1ca83c3bc 100644 --- a/guide/src/macros/index.md +++ b/guide/src/macros/index.md @@ -13,6 +13,7 @@ used from PHP without fiddling around with zvals. methods and constants. - [`php_const`] - Used to export a Rust constant to PHP as a global constant. - [`php_extern`] - Attribute used to annotate `extern` blocks which are deemed as +- [`php_interface`] - Attribute used to export Rust Trait to PHP interface PHP functions. - [`php`] - Used to modify the default behavior of the above macros. This is a generic attribute that can be used on most of the above macros. @@ -23,4 +24,5 @@ used from PHP without fiddling around with zvals. [`php_impl`]: ./impl.md [`php_const`]: ./constant.md [`php_extern`]: ./extern.md +[`php_interface`]: ./interface.md [`php`]: ./php.md diff --git a/guide/src/macros/interface.md b/guide/src/macros/interface.md new file mode 100644 index 000000000..d3e181d46 --- /dev/null +++ b/guide/src/macros/interface.md @@ -0,0 +1,73 @@ +# `#[php_interface]` Attribute + +You can export an entire `Trait` block to PHP. This exports all methods as well +as constants to PHP on the interface. Trait method SHOULD NOT contain default implementation + +## Options + +By default all constants are renamed to `UPPER_CASE` and all methods are renamed to +camelCase. This can be changed by passing the `change_method_case` and +`change_constant_case` as `#[php]` attributes on the `impl` block. The options are: + +- `#[php(change_method_case = "snake_case")]` - Renames the method to snake case. +- `#[php(change_constant_case = "snake_case")]` - Renames the constant to snake case. + +See the [`name` and `change_case`](./php.md#name-and-change_case) section for a list of all +available cases. + +## Methods + +See the [php_impl](./impl.md#) + +## Constants + +See the [php_impl](./impl.md#) + +## Example + +Define trait example with few methods and constant, and try implement this interface +in php + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::{prelude::*, types::ZendClassObject}; + + +#[php_interface] +#[php(name = "Rust\\TestInterface")] +trait Test { + const TEST: &'static str = "TEST"; + + fn co(); + + #[php(defaults(value = 0))] + fn set_value(&mut self, value: i32); +} + +#[php_module] +pub fn module(module: ModuleBuilder) -> ModuleBuilder { + module + .interface::() +} + +# fn main() {} +``` + +Using our newly created interface in PHP: + +```php + Date: Sun, 10 Aug 2025 19:58:10 +0300 Subject: [PATCH 22/25] chore: Delete unused interface.rs --- src/interface.rs | 5 ----- src/lib.rs | 1 - 2 files changed, 6 deletions(-) delete mode 100644 src/interface.rs diff --git a/src/interface.rs b/src/interface.rs deleted file mode 100644 index 8bd092295..000000000 --- a/src/interface.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::{convert::IntoZval, describe::DocComments}; - -pub trait RegisteredInterface { - fn constants() -> &'static [(&'static str, &'static impl IntoZval, DocComments)]; -} diff --git a/src/lib.rs b/src/lib.rs index 548992167..b7a533a73 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,7 +30,6 @@ pub mod describe; pub mod embed; #[cfg(feature = "enum")] pub mod enum_; -pub mod interface; #[doc(hidden)] pub mod internal; pub mod props; From 34580c9597356f1712c7eaed60967ed171c87245 Mon Sep 17 00:00:00 2001 From: norbytus Date: Sun, 10 Aug 2025 20:32:20 +0300 Subject: [PATCH 23/25] chore: Clean from duplicated code --- crates/macros/src/interface.rs | 26 ++++----------- src/builders/interface.rs | 46 -------------------------- src/builders/mod.rs | 1 - src/builders/module.rs | 11 +++--- src/describe/mod.rs | 4 +-- tests/src/integration/interface/mod.rs | 4 +-- 6 files changed, 15 insertions(+), 77 deletions(-) delete mode 100644 src/builders/interface.rs diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index 17167e171..b70a99709 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -54,12 +54,11 @@ impl ToTokens for InterfaceData<'_> { let name = &self.name; let implements = &self.attrs.extends; let methods_sig = &self.methods; - let path = &self.path; let constants = &self.constants; - let constructor = self.constructor + let _constructor = self.constructor .as_ref() - .map(|func| func.constructor_meta(&path)) + .map(|func| func.constructor_meta(&self.path)) .option_tokens(); quote! { @@ -209,15 +208,7 @@ impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait { let interface_name = format_ident!("PhpInterface{ident}"); let ts = quote! { #interface_name }; let path: Path = syn::parse2(ts)?; - let mut data = InterfaceData::new( - ident, - name, - path, - attrs, - None, - Vec::new(), - Vec::new() - ); + let mut data = InterfaceData::new(ident, name, path, attrs, None, Vec::new(), Vec::new()); for item in &mut self.items { match item { @@ -230,7 +221,7 @@ impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait { } } }; - }, + } TraitItem::Const(c) => data.constants.push(c.parse()?), _ => {} } @@ -265,15 +256,10 @@ impl<'a> Parse<'a, MethodKind<'a>> for TraitItemFn { bail!(self => "Interface could not have default impl"); } - let php_attr = PhpFunctionInterfaceAttribute::from_attributes( - &self.attrs - )?; + let php_attr = PhpFunctionInterfaceAttribute::from_attributes(&self.attrs)?; self.attrs.clean_php(); - let mut args = Args::parse_from_fnargs( - self.sig.inputs.iter(), - php_attr.defaults - )?; + let mut args = Args::parse_from_fnargs(self.sig.inputs.iter(), php_attr.defaults)?; let docs = get_docs(&php_attr.attrs)?; diff --git a/src/builders/interface.rs b/src/builders/interface.rs deleted file mode 100644 index b247e41ae..000000000 --- a/src/builders/interface.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::builders::FunctionBuilder; -use crate::error::Result; -use crate::flags::{ClassFlags, MethodFlags}; -use crate::{ - builders::ClassBuilder, class::ClassEntryInfo, convert::IntoZvalDyn, describe::DocComments, -}; - -pub struct InterfaceBuilder { - class_builder: ClassBuilder, -} - -impl InterfaceBuilder { - pub fn new>(name: T) -> Self { - Self { - class_builder: ClassBuilder::new(name), - } - } - - pub fn implements(mut self, interface: ClassEntryInfo) -> Self { - self.class_builder = self.class_builder.implements(interface); - - self - } - - pub fn method(mut self, func: FunctionBuilder<'static>, flags: MethodFlags) -> Self { - self.class_builder = self.class_builder.method(func, flags); - - self - } - - pub fn dyn_constant>( - mut self, - name: T, - value: &'static dyn IntoZvalDyn, - docs: DocComments, - ) -> Result { - self.class_builder = self.class_builder.dyn_constant(name, value, docs)?; - - Ok(self) - } - - pub fn builder(mut self) -> ClassBuilder { - self.class_builder = self.class_builder.flags(ClassFlags::Interface); - self.class_builder - } -} diff --git a/src/builders/mod.rs b/src/builders/mod.rs index 7920df93e..c439c8168 100644 --- a/src/builders/mod.rs +++ b/src/builders/mod.rs @@ -7,7 +7,6 @@ mod enum_builder; mod function; #[cfg(all(php82, feature = "embed"))] mod ini; -mod interface; mod module; #[cfg(feature = "embed")] mod sapi; diff --git a/src/builders/module.rs b/src/builders/module.rs index a7d70ac28..10a1d929d 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -4,12 +4,12 @@ use super::{ClassBuilder, FunctionBuilder}; #[cfg(feature = "enum")] use crate::{builders::enum_builder::EnumBuilder, enum_::RegisteredEnum}; use crate::{ - builders::interface::InterfaceBuilder, class::RegisteredClass, constant::IntoConst, describe::DocComments, error::Result, ffi::{ext_php_rs_php_build_id, ZEND_MODULE_API_NO}, + flags::ClassFlags, zend::{FunctionEntry, ModuleEntry}, PHP_DEBUG, PHP_ZTS, }; @@ -201,7 +201,7 @@ impl ModuleBuilder<'_> { /// * Panics if a constant could not be registered. pub fn interface(mut self) -> Self { self.interfaces.push(|| { - let mut builder = InterfaceBuilder::new(T::CLASS_NAME); + let mut builder = ClassBuilder::new(T::CLASS_NAME); for (method, flags) in T::method_builders() { builder = builder.method(method, flags); } @@ -214,13 +214,12 @@ impl ModuleBuilder<'_> { .expect("Failed to register constant"); } - let mut class_builder = builder.builder(); - if let Some(modifier) = T::BUILDER_MODIFIER { - class_builder = modifier(class_builder); + builder = modifier(builder); } - class_builder + builder = builder.flags(ClassFlags::Interface); + builder .object_override::() .registration(|ce| { T::get_metadata().set_ce(ce); diff --git a/src/describe/mod.rs b/src/describe/mod.rs index 2b40f71f3..29f6af758 100644 --- a/src/describe/mod.rs +++ b/src/describe/mod.rs @@ -227,7 +227,7 @@ impl Class { }), r#static: false, visibility: Visibility::Public, - r#abstract: false + r#abstract: false, }] .into(), constants: StdVec::new().into(), @@ -274,7 +274,7 @@ impl From for Class { .map(Constant::from) .collect::>() .into(), - flags: flags + flags: flags, } } } diff --git a/tests/src/integration/interface/mod.rs b/tests/src/integration/interface/mod.rs index d04200b26..232bc72d9 100644 --- a/tests/src/integration/interface/mod.rs +++ b/tests/src/integration/interface/mod.rs @@ -1,7 +1,7 @@ -use ext_php_rs::types::ZendClassObject; -use ext_php_rs::zend::ce; use ext_php_rs::php_interface; use ext_php_rs::prelude::ModuleBuilder; +use ext_php_rs::types::ZendClassObject; +use ext_php_rs::zend::ce; #[php_interface] #[php(extends(ce = ce::throwable, stub = "\\Throwable"))] From fa449e5521e503c8a90fb970bdf1d217394af59f Mon Sep 17 00:00:00 2001 From: norbytus Date: Sun, 10 Aug 2025 21:23:35 +0300 Subject: [PATCH 24/25] chore: Fix clippy and etc --- crates/macros/src/interface.rs | 4 +++- src/builders/class.rs | 2 ++ src/describe/mod.rs | 4 ++-- src/describe/stub.rs | 8 ++++---- tests/src/integration/interface/mod.rs | 1 + 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index b70a99709..13970543b 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -49,6 +49,7 @@ struct InterfaceData<'a> { } impl ToTokens for InterfaceData<'_> { + #[allow(clippy::too_many_lines)] fn to_tokens(&self, tokens: &mut TokenStream) { let interface_name = format_ident!("PhpInterface{}", self.ident); let name = &self.name; @@ -56,7 +57,8 @@ impl ToTokens for InterfaceData<'_> { let methods_sig = &self.methods; let constants = &self.constants; - let _constructor = self.constructor + let _constructor = self + .constructor .as_ref() .map(|func| func.constructor_meta(&self.path)) .option_tokens(); diff --git a/src/builders/class.rs b/src/builders/class.rs index dba608cf5..b705e81d9 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -58,6 +58,8 @@ impl ClassBuilder { } } + /// Return PHP class flags + #[must_use] pub fn get_flags(&self) -> u32 { self.ce.ce_flags } diff --git a/src/describe/mod.rs b/src/describe/mod.rs index 29f6af758..bb361d313 100644 --- a/src/describe/mod.rs +++ b/src/describe/mod.rs @@ -7,7 +7,7 @@ use crate::builders::EnumBuilder; use crate::{ builders::{ClassBuilder, FunctionBuilder}, constant::IntoConst, - flags::{ClassFlags, DataType, MethodFlags, PropertyFlags}, + flags::{DataType, MethodFlags, PropertyFlags}, prelude::ModuleBuilder, }; use abi::{Option, RString, Str, Vec}; @@ -274,7 +274,7 @@ impl From for Class { .map(Constant::from) .collect::>() .into(), - flags: flags, + flags, } } } diff --git a/src/describe/stub.rs b/src/describe/stub.rs index 1737e2f82..76650d546 100644 --- a/src/describe/stub.rs +++ b/src/describe/stub.rs @@ -226,7 +226,7 @@ impl ToStub for Class { self.docs.fmt_stub(buf)?; let (_, name) = split_namespace(self.name.as_ref()); - let flags = ClassFlags::from_bits(self.flags).unwrap(); + let flags = ClassFlags::from_bits(self.flags).unwrap_or(ClassFlags::empty()); let is_interface = flags.contains(ClassFlags::Interface); if is_interface { @@ -379,10 +379,10 @@ impl ToStub for Method { } } - if !self.r#abstract { - writeln!(buf, " {{}}") - } else { + if self.r#abstract { writeln!(buf, ";") + } else { + writeln!(buf, " {{}}") } } } diff --git a/tests/src/integration/interface/mod.rs b/tests/src/integration/interface/mod.rs index 232bc72d9..620e5ac4a 100644 --- a/tests/src/integration/interface/mod.rs +++ b/tests/src/integration/interface/mod.rs @@ -6,6 +6,7 @@ use ext_php_rs::zend::ce; #[php_interface] #[php(extends(ce = ce::throwable, stub = "\\Throwable"))] #[php(name = "ExtPhpRs\\Interface\\EmptyObjectInterface")] +#[allow(dead_code)] pub trait EmptyObjectTrait { const STRING_CONST: &'static str = "STRING_CONST"; From c0dcacdd2aec0f62a6c260cb69b03e1cba635117 Mon Sep 17 00:00:00 2001 From: norbytus Date: Mon, 11 Aug 2025 15:42:07 +0300 Subject: [PATCH 25/25] chore: Remove expand test --- crates/macros/src/interface.rs | 2 +- .../macros/tests/expand/interface.expanded.rs | 242 ------------------ crates/macros/tests/expand/interface.rs | 26 -- src/builders/class.rs | 14 +- 4 files changed, 13 insertions(+), 271 deletions(-) delete mode 100644 crates/macros/tests/expand/interface.expanded.rs delete mode 100644 crates/macros/tests/expand/interface.rs diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index 13970543b..242254622 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -60,7 +60,7 @@ impl ToTokens for InterfaceData<'_> { let _constructor = self .constructor .as_ref() - .map(|func| func.constructor_meta(&self.path)) + .map(|func| func.constructor_meta(&self.path, Some(&Visibility::Public))) .option_tokens(); quote! { diff --git a/crates/macros/tests/expand/interface.expanded.rs b/crates/macros/tests/expand/interface.expanded.rs deleted file mode 100644 index d691d20bc..000000000 --- a/crates/macros/tests/expand/interface.expanded.rs +++ /dev/null @@ -1,242 +0,0 @@ -#![feature(prelude_import)] -#[prelude_import] -use std::prelude::rust_2024::*; -#[macro_use] -extern crate std; -use ext_php_rs::types::ZendClassObject; -use ext_php_rs::php_interface; -use ext_php_rs::zend::ce; -pub trait EmptyObjectTrait { - const HELLO: &'static str = "HELLO"; - const ONE: u64 = 12; - fn void(); - fn non_static(&self, data: String) -> String; - fn ref_to_like_this_class( - &self, - data: String, - other: &ZendClassObject, - ) -> String; -} -pub struct PhpInterfaceEmptyObjectTrait; -impl PhpInterfaceEmptyObjectTrait { - pub const HELLO: &'static str = "HELLO"; - pub const ONE: u64 = 12; -} -impl ::ext_php_rs::class::RegisteredClass for PhpInterfaceEmptyObjectTrait { - const CLASS_NAME: &'static str = "ExtPhpRs\\Interface\\EmptyObjectInterface"; - const BUILDER_MODIFIER: Option< - fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder, - > = None; - const EXTENDS: Option<::ext_php_rs::class::ClassEntryInfo> = None; - const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::Interface; - const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[ - (ce::throwable, "\\Throwable"), - ]; - fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { - static METADATA: ::ext_php_rs::class::ClassMetadata< - PhpInterfaceEmptyObjectTrait, - > = ::ext_php_rs::class::ClassMetadata::new(); - &METADATA - } - fn method_builders() -> Vec< - ( - ::ext_php_rs::builders::FunctionBuilder<'static>, - ::ext_php_rs::flags::MethodFlags, - ), - > { - <[_]>::into_vec( - ::alloc::boxed::box_new([ - ( - ::ext_php_rs::builders::FunctionBuilder::new_abstract("void") - .not_required(), - ::ext_php_rs::flags::MethodFlags::Public - | ::ext_php_rs::flags::MethodFlags::Abstract - | ::ext_php_rs::flags::MethodFlags::Static, - ), - ( - ::ext_php_rs::builders::FunctionBuilder::new_abstract("nonStatic") - .arg( - ::ext_php_rs::args::Arg::new( - "data", - ::TYPE, - ), - ) - .not_required() - .returns( - ::TYPE, - false, - ::NULLABLE, - ), - ::ext_php_rs::flags::MethodFlags::Public - | ::ext_php_rs::flags::MethodFlags::Abstract, - ), - ( - ::ext_php_rs::builders::FunctionBuilder::new_abstract( - "refToLikeThisClass", - ) - .arg( - ::ext_php_rs::args::Arg::new( - "data", - ::TYPE, - ), - ) - .arg( - ::ext_php_rs::args::Arg::new( - "other", - <&ZendClassObject< - PhpInterfaceEmptyObjectTrait, - > as ::ext_php_rs::convert::FromZvalMut>::TYPE, - ), - ) - .not_required() - .returns( - ::TYPE, - false, - ::NULLABLE, - ), - ::ext_php_rs::flags::MethodFlags::Public - | ::ext_php_rs::flags::MethodFlags::Abstract, - ), - ]), - ) - } - fn constructor() -> Option<::ext_php_rs::class::ConstructorMeta> { - None - } - fn constants() -> &'static [( - &'static str, - &'static dyn ext_php_rs::convert::IntoZvalDyn, - ext_php_rs::describe::DocComments, - )] { - use ::ext_php_rs::internal::class::PhpClassImpl; - ::ext_php_rs::internal::class::PhpClassImplCollector::::default() - .get_constants() - } - fn get_properties<'a>() -> std::collections::HashMap< - &'static str, - ::ext_php_rs::internal::property::PropertyInfo<'a, Self>, - > { - HashMap::new() - } -} -impl ::ext_php_rs::internal::class::PhpClassImpl -for ::ext_php_rs::internal::class::PhpClassImplCollector { - fn get_methods( - self, - ) -> ::std::vec::Vec< - ( - ::ext_php_rs::builders::FunctionBuilder<'static>, - ::ext_php_rs::flags::MethodFlags, - ), - > { - ::alloc::vec::Vec::new() - } - fn get_method_props<'a>( - self, - ) -> ::std::collections::HashMap< - &'static str, - ::ext_php_rs::props::Property<'a, PhpInterfaceEmptyObjectTrait>, - > { - ::core::panicking::panic("not yet implemented") - } - fn get_constructor( - self, - ) -> ::std::option::Option< - ::ext_php_rs::class::ConstructorMeta, - > { - None - } - fn get_constants( - self, - ) -> &'static [( - &'static str, - &'static dyn ::ext_php_rs::convert::IntoZvalDyn, - &'static [&'static str], - )] { - &[ - ("HELLO", &PhpInterfaceEmptyObjectTrait::HELLO, &[]), - ("ONE", &PhpInterfaceEmptyObjectTrait::ONE, &[]), - ] - } -} -impl<'a> ::ext_php_rs::convert::FromZendObject<'a> for &'a PhpInterfaceEmptyObjectTrait { - #[inline] - fn from_zend_object( - obj: &'a ::ext_php_rs::types::ZendObject, - ) -> ::ext_php_rs::error::Result { - let obj = ::ext_php_rs::types::ZendClassObject::< - PhpInterfaceEmptyObjectTrait, - >::from_zend_obj(obj) - .ok_or(::ext_php_rs::error::Error::InvalidScope)?; - Ok(&**obj) - } -} -impl<'a> ::ext_php_rs::convert::FromZendObjectMut<'a> -for &'a mut PhpInterfaceEmptyObjectTrait { - #[inline] - fn from_zend_object_mut( - obj: &'a mut ::ext_php_rs::types::ZendObject, - ) -> ::ext_php_rs::error::Result { - let obj = ::ext_php_rs::types::ZendClassObject::< - PhpInterfaceEmptyObjectTrait, - >::from_zend_obj_mut(obj) - .ok_or(::ext_php_rs::error::Error::InvalidScope)?; - Ok(&mut **obj) - } -} -impl<'a> ::ext_php_rs::convert::FromZval<'a> for &'a PhpInterfaceEmptyObjectTrait { - const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object( - Some( - ::CLASS_NAME, - ), - ); - #[inline] - fn from_zval(zval: &'a ::ext_php_rs::types::Zval) -> ::std::option::Option { - ::from_zend_object(zval.object()?) - .ok() - } -} -impl<'a> ::ext_php_rs::convert::FromZvalMut<'a> -for &'a mut PhpInterfaceEmptyObjectTrait { - const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object( - Some( - ::CLASS_NAME, - ), - ); - #[inline] - fn from_zval_mut( - zval: &'a mut ::ext_php_rs::types::Zval, - ) -> ::std::option::Option { - ::from_zend_object_mut( - zval.object_mut()?, - ) - .ok() - } -} -impl ::ext_php_rs::convert::IntoZendObject for PhpInterfaceEmptyObjectTrait { - #[inline] - fn into_zend_object( - self, - ) -> ::ext_php_rs::error::Result< - ::ext_php_rs::boxed::ZBox<::ext_php_rs::types::ZendObject>, - > { - Ok(::ext_php_rs::types::ZendClassObject::new(self).into()) - } -} -impl ::ext_php_rs::convert::IntoZval for PhpInterfaceEmptyObjectTrait { - const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object( - Some( - ::CLASS_NAME, - ), - ); - const NULLABLE: bool = false; - #[inline] - fn set_zval( - self, - zv: &mut ::ext_php_rs::types::Zval, - persistent: bool, - ) -> ::ext_php_rs::error::Result<()> { - use ::ext_php_rs::convert::IntoZendObject; - self.into_zend_object()?.set_zval(zv, persistent) - } -} diff --git a/crates/macros/tests/expand/interface.rs b/crates/macros/tests/expand/interface.rs deleted file mode 100644 index f3b2b00c4..000000000 --- a/crates/macros/tests/expand/interface.rs +++ /dev/null @@ -1,26 +0,0 @@ -#[macro_use] -extern crate ext_php_rs_derive; - -use ext_php_rs::types::ZendClassObject; -use ext_php_rs::php_interface; -use ext_php_rs::zend::ce; - -#[php_interface] -#[php(extends(ce = ce::throwable, stub = "\\Throwable"))] -#[php(name = "ExtPhpRs\\Interface\\EmptyObjectInterface")] -pub trait EmptyObjectTrait -{ - const HELLO: &'static str = "HELLO"; - - const ONE: u64 = 12; - - fn void(); - - fn non_static(&self, data: String) -> String; - - fn ref_to_like_this_class( - &self, - data: String, - other: &ZendClassObject - ) -> String; -} diff --git a/src/builders/class.rs b/src/builders/class.rs index b705e81d9..1de17579e 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -244,16 +244,26 @@ impl ClassBuilder { "Class name in builder does not match class name in `impl RegisteredClass`." ); self.object_override = Some(create_object::); + let is_interface = T::FLAGS.contains(ClassFlags::Interface); let (func, visibility) = if let Some(ConstructorMeta { build_fn, flags, .. }) = T::constructor() { - let func = FunctionBuilder::new("__construct", constructor::); + let func = if is_interface { + FunctionBuilder::new_abstract("__construct") + } else { + FunctionBuilder::new("__construct", constructor::) + }; + (build_fn(func), flags.unwrap_or(MethodFlags::Public)) } else { ( - FunctionBuilder::new("__construct", constructor::), + if is_interface { + FunctionBuilder::new_abstract("__construct") + } else { + FunctionBuilder::new("__construct", constructor::) + }, MethodFlags::Public, ) };