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/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 b808e2b22..aa31c51cd 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -131,6 +131,40 @@ 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/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/impl_.rs b/crates/macros/src/impl_.rs index 039122621..1e8ea6c7e 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,13 @@ 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> ParsedImpl<'a> { diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs new file mode 100644 index 000000000..13970543b --- /dev/null +++ b/crates/macros/src/interface.rs @@ -0,0 +1,368 @@ +use std::collections::{HashMap, HashSet}; + +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, ToTokens}; +use syn::{Expr, Ident, ItemTrait, Path, TraitItem, TraitItemConst, TraitItemFn}; + +use crate::impl_::{FnBuilder, MethodModifier}; +use crate::parsing::{PhpRename, RenameRule, Visibility}; +use crate::prelude::*; + +#[derive(FromAttributes, Debug, Default)] +#[darling(attributes(php), forward_attrs(doc), default)] +pub struct StructAttributes { + #[darling(flatten)] + rename: PhpRename, + #[darling(multiple)] + extends: Vec, +} + +pub fn parser(mut input: ItemTrait) -> Result { + let interface_data: InterfaceData = input.parse()?; + let interface_tokens = quote! { #interface_data }; + + Ok(quote! { + #input + + #interface_tokens + }) +} + +trait Parse<'a, T> { + fn parse(&'a mut self) -> Result; +} + +struct InterfaceData<'a> { + ident: &'a Ident, + name: String, + path: Path, + attrs: StructAttributes, + constructor: Option>, + methods: Vec, + constants: Vec>, +} + +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; + let implements = &self.attrs.extends; + let methods_sig = &self.methods; + let constants = &self.constants; + + let _constructor = self + .constructor + .as_ref() + .map(|func| func.constructor_meta(&self.path)) + .option_tokens(); + + quote! { + 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] = &[ + #(#implements,)* + ]; + + 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![#(#methods_sig),*] + } + + 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, + )] { + &[#(#constants),*] + } + + fn get_properties<'a>() -> ::std::collections::HashMap<&'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self>> { + panic!("Non supported for Interface"); + } + } + + 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) + } + } + }.to_tokens(tokens); + } +} + +impl<'a> InterfaceData<'a> { + fn new( + ident: &'a Ident, + name: String, + path: Path, + attrs: StructAttributes, + constructor: Option>, + methods: Vec, + constants: Vec>, + ) -> Self { + Self { + ident, + name, + path, + attrs, + constructor, + methods, + constants, + } + } +} + +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, None, Vec::new(), Vec::new()); + + for item in &mut self.items { + match item { + 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()?), + _ => {} + } + } + + Ok(data) + } +} + +#[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, +} + +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!(self => "Interface could not have default impl"); + } + + 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() { + 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, + ); + + 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>> { + Ok(self + .items + .iter_mut() + .filter_map(|item| match item { + TraitItem::Fn(f) => Some(f), + _ => None, + }) + .flat_map(Parse::parse) + .collect()) + } +} + +#[derive(Debug)] +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!(self => "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(); + + let (_, expr) = self.default.as_ref().unwrap(); + Ok(Constant::new(name, expr, docs)) + } +} + +impl<'a> Parse<'a, Vec>> for ItemTrait { + fn parse(&'a mut self) -> Result>> { + Ok(self + .items + .iter_mut() + .filter_map(|item| match item { + TraitItem::Const(c) => Some(c), + _ => None, + }) + .flat_map(Parse::parse) + .collect()) + } +} diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 8b7cdbee0..875258199 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -7,6 +7,7 @@ mod fastcall; mod function; mod helpers; mod impl_; +mod interface; mod module; mod parsing; mod syn_ext; @@ -14,7 +15,9 @@ mod zval; use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; -use syn::{DeriveInput, ItemConst, ItemEnum, ItemFn, ItemForeignMod, ItemImpl, ItemStruct}; +use syn::{ + DeriveInput, ItemConst, ItemEnum, ItemFn, ItemForeignMod, ItemImpl, ItemStruct, ItemTrait, +}; extern crate proc_macro; @@ -339,6 +342,57 @@ 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() +} + +fn php_interface_internal(_args: TokenStream2, input: TokenStream2) -> TokenStream2 { + let input = parse_macro_input2!(input as ItemTrait); + + interface::parser(input).unwrap_or_else(|e| e.to_compile_error()) +} + // BEGIN DOCS FROM function.md /// # `#[php_function]` Attribute /// @@ -1252,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, @@ -1259,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/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/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 + u32 { + self.ce.ce_flags + } + /// Sets the class builder to extend another class. /// /// # Parameters @@ -238,9 +244,15 @@ impl ClassBuilder { "Class name in builder does not match class name in `impl RegisteredClass`." ); self.object_override = Some(create_object::); + + let mut func = if T::FLAGS.contains(ClassFlags::Interface) { + FunctionBuilder::new_abstract("__construct") + } else { + FunctionBuilder::new("__construct", constructor::) + }; + self.method( { - let mut func = FunctionBuilder::new("__construct", constructor::); if let Some(ConstructorMeta { build_fn, .. }) = T::constructor() { func = build_fn(func); } @@ -301,16 +313,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(&raw 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 @@ -462,6 +482,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"]); diff --git a/src/builders/module.rs b/src/builders/module.rs index 1eea007e5..10a1d929d 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -9,6 +9,7 @@ use crate::{ describe::DocComments, error::Result, ffi::{ext_php_rs_php_build_id, ZEND_MODULE_API_NO}, + flags::ClassFlags, zend::{FunctionEntry, ModuleEntry}, PHP_DEBUG, PHP_ZTS, }; @@ -48,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, @@ -192,6 +194,41 @@ 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.interfaces.push(|| { + let mut builder = ClassBuilder::new(T::CLASS_NAME); + 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"); + } + + if let Some(modifier) = T::BUILDER_MODIFIER { + builder = modifier(builder); + } + + builder = builder.flags(ClassFlags::Interface); + builder + .object_override::() + .registration(|ce| { + T::get_metadata().set_ce(ce); + }) + .docs(T::DOC_COMMENTS) + }); + self + } + /// Adds a class to the extension. /// /// # Panics @@ -262,6 +299,7 @@ impl ModuleBuilder<'_> { pub struct ModuleStartup { constants: Vec<(String, Box)>, classes: Vec ClassBuilder>, + interfaces: Vec ClassBuilder>, #[cfg(feature = "enum")] enums: Vec EnumBuilder>, } @@ -286,6 +324,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() @@ -328,6 +370,7 @@ impl TryFrom> for (ModuleEntry, ModuleStartup) { .map(|(n, v, _)| (n, v)) .collect(), classes: builder.classes, + interfaces: builder.interfaces, #[cfg(feature = "enum")] enums: builder.enums, }; @@ -383,6 +426,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()); diff --git a/src/describe/mod.rs b/src/describe/mod.rs index 82238f0f5..bb361d313 100644 --- a/src/describe/mod.rs +++ b/src/describe/mod.rs @@ -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, } } } @@ -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..76650d546 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_or(ClassFlags::empty()); + 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, " {{}}") + } } } 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, }; diff --git a/tests/src/integration/interface/interface.php b/tests/src/integration/interface/interface.php new file mode 100644 index 000000000..1266bc4e9 --- /dev/null +++ b/tests/src/integration/interface/interface.php @@ -0,0 +1,38 @@ +nonStatic($data), $other->nonStatic($data)); + } + + public function setValue(?int $value = 0) { + + } +} +$f = new Test(); + +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 new file mode 100644 index 000000000..620e5ac4a --- /dev/null +++ b/tests/src/integration/interface/mod.rs @@ -0,0 +1,39 @@ +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"))] +#[php(name = "ExtPhpRs\\Interface\\EmptyObjectInterface")] +#[allow(dead_code)] +pub trait EmptyObjectTrait { + const STRING_CONST: &'static str = "STRING_CONST"; + + const USIZE_CONST: u64 = 200; + + fn void(); + + fn non_static(&self, data: String) -> String; + + fn ref_to_like_this_class( + &self, + data: String, + other: &ZendClassObject, + ) -> String; + + #[php(defaults(value = 0))] + fn set_value(&mut self, value: i32); +} + +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/integration/mod.rs b/tests/src/integration/mod.rs index cc18e5108..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; 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 }