diff --git a/crates/cli/src/ext.rs b/crates/cli/src/ext.rs index f639a92d75..78b9bee760 100644 --- a/crates/cli/src/ext.rs +++ b/crates/cli/src/ext.rs @@ -2,8 +2,13 @@ use std::path::PathBuf; use anyhow::{Context, Result}; use ext_php_rs::describe::Description; + +#[cfg(unix)] use libloading::os::unix::{Library, RTLD_LAZY, RTLD_LOCAL, Symbol}; +#[cfg(windows)] +use libloading::{Library, Symbol}; + #[allow(improper_ctypes_definitions)] pub struct Ext { // These need to be here to keep the libraries alive. The extension library needs to be alive @@ -11,7 +16,10 @@ pub struct Ext { // Module>` where `ext_lib: 'a`. #[allow(dead_code)] ext_lib: Library, + #[cfg(unix)] describe_fn: Symbol Description>, + #[cfg(windows)] + describe_fn: Symbol<'static, extern "C" fn() -> Description>, } impl Ext { @@ -25,10 +33,15 @@ impl Ext { .with_context(|| "Failed to load extension library")?; // On other Unix platforms, RTLD_LAZY | RTLD_LOCAL is sufficient - #[cfg(not(target_os = "macos"))] + #[cfg(all(unix, not(target_os = "macos")))] let ext_lib = unsafe { Library::open(Some(ext_path), RTLD_LAZY | RTLD_LOCAL) } .with_context(|| "Failed to load extension library")?; + // On Windows, use the standard Library::new + #[cfg(windows)] + let ext_lib = unsafe { Library::new(ext_path) } + .with_context(|| "Failed to load extension library")?; + let describe_fn = unsafe { ext_lib .get(b"ext_php_rs_describe_module") diff --git a/crates/cli/tests/snapshots/stubs_snapshot__hello_world_stubs.snap b/crates/cli/tests/snapshots/stubs_snapshot__hello_world_stubs.snap index c84a9be64e..052fed0c2b 100644 --- a/crates/cli/tests/snapshots/stubs_snapshot__hello_world_stubs.snap +++ b/crates/cli/tests/snapshots/stubs_snapshot__hello_world_stubs.snap @@ -12,52 +12,74 @@ namespace { const HELLO_WORLD = 100; + /** + * A simple test class demonstrating ext-php-rs features. + * + * This class showcases property definitions, constants, and various + * method types including constructors and static methods. + */ class TestClass { const NEW_CONSTANT_NAME = 5; const SOME_OTHER_STR = 'Hello, world!'; - public $a; + public int $a; - public $b; + public int $b; /** - * @param int $a - * @param int $b + * Creates a new `TestClass` instance. + * + * Both values are incremented by 10 before being stored. + * + * @param int $a First value to store + * @param int $b Second value to store */ public function __construct(int $a, int $b) {} /** - * @return \TestClass + * Demonstrates the builder pattern by returning self. + * + * @return \TestClass Returns the same instance for method chaining. */ public function builderPattern(): \TestClass {} /** - * @param int $a - * @param int $test + * Tests camelCase conversion and default parameter values. + * + * @param int $a First parameter with default value 5 + * @param int $test Second parameter with default value 100 * @return void */ public function testCamelCase(int $a = 5, int $test = 100): void {} /** - * @return int + * Returns a static value. + * + * @return int Always returns 5. */ public static function x(): int {} } /** - * @param object $z - * @return int + * Demonstrates `ZvalConvert` derive macro usage. + * + * @param object $z An object that will be converted from a PHP value + * @return int Always returns 5. */ function get_zval_convert(object $z): int {} /** - * @return string + * Returns a friendly greeting. + * + * @return string The string "Hello, world!". */ function hello_world(): string {} /** - * @return \TestClass + * Creates a new `TestClass` instance with default values. + * + * @return \TestClass A `TestClass` with a=1 and b=2. */ function new_class(): \TestClass {} } diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index 8c15537334..11cbb134ed 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -2,9 +2,63 @@ use darling::util::Flag; use darling::{FromAttributes, FromMeta, ToTokens}; use proc_macro2::TokenStream; use quote::{TokenStreamExt, quote}; -use syn::{Attribute, Expr, Fields, ItemStruct}; +use syn::{Attribute, Expr, Fields, GenericArgument, ItemStruct, PathArguments, Type}; use crate::helpers::get_docs; + +/// Check if a type is `Option` and return the inner type if so. +fn is_option_type(ty: &Type) -> Option<&Type> { + let Type::Path(type_path) = ty else { + return None; + }; + if type_path.qself.is_some() { + return None; + } + let segments = &type_path.path.segments; + if segments.len() != 1 { + return None; + } + let segment = &segments[0]; + if segment.ident != "Option" { + return None; + } + let PathArguments::AngleBracketed(args) = &segment.arguments else { + return None; + }; + if args.args.len() != 1 { + return None; + } + if let GenericArgument::Type(inner) = &args.args[0] { + return Some(inner); + } + None +} + +/// Convert an expression to a PHP-compatible default string for stub generation. +fn expr_to_php_default_string(expr: &Expr) -> String { + // For simple literals, we can convert them directly + // For complex expressions, we use a string representation + match expr { + Expr::Lit(lit) => match &lit.lit { + syn::Lit::Str(s) => format!("'{}'", s.value().replace('\'', "\\'")), + syn::Lit::Int(i) => i.to_string(), + syn::Lit::Float(f) => f.to_string(), + syn::Lit::Bool(b) => if b.value { "true" } else { "false" }.to_string(), + _ => expr.to_token_stream().to_string(), + }, + Expr::Array(_) => "[]".to_string(), + Expr::Path(path) => { + // Handle constants like `None`, `true`, `false` + let path_str = path.to_token_stream().to_string(); + if path_str == "None" { + "null".to_string() + } else { + path_str + } + } + _ => expr.to_token_stream().to_string(), + } +} use crate::parsing::{PhpNameContext, PhpRename, RenameRule, ident_to_php_name, validate_php_name}; use crate::prelude::*; @@ -192,6 +246,7 @@ fn parse_fields<'a>(fields: impl Iterator) -> Result< result.push(Property { ident, + ty: &field.ty, name, attr, docs, @@ -205,6 +260,7 @@ fn parse_fields<'a>(fields: impl Iterator) -> Result< #[derive(Debug)] struct Property<'a> { pub ident: &'a syn::Ident, + pub ty: &'a syn::Type, pub name: String, pub attr: PropAttributes, pub docs: Vec, @@ -240,6 +296,7 @@ fn generate_registered_class_impl( let instance_fields = instance_props.iter().map(|prop| { let name = &prop.name; let field_ident = prop.ident; + let field_ty = prop.ty; let flags = prop .attr .flags @@ -248,11 +305,25 @@ fn generate_registered_class_impl( .unwrap_or(quote! { ::ext_php_rs::flags::PropertyFlags::Public }); let docs = &prop.docs; + // Determine if the property is nullable (type is Option) + let nullable = is_option_type(field_ty).is_some(); + + // Get the default value as a PHP-compatible string for stub generation + let default_str = if let Some(default_expr) = &prop.attr.default { + let s = expr_to_php_default_string(default_expr); + quote! { ::std::option::Option::Some(#s) } + } else { + quote! { ::std::option::Option::None } + }; + quote! { (#name, ::ext_php_rs::internal::property::PropertyInfo { prop: ::ext_php_rs::props::Property::field(|this: &mut Self| &mut this.#field_ident), flags: #flags, - docs: &[#(#docs,)*] + docs: &[#(#docs,)*], + ty: ::std::option::Option::Some(<#field_ty as ::ext_php_rs::convert::IntoZval>::TYPE), + nullable: #nullable, + default: #default_str, }) } }); @@ -262,6 +333,7 @@ fn generate_registered_class_impl( // const let static_fields = static_props.iter().map(|prop| { let name = &prop.name; + let field_ty = prop.ty; let base_flags = prop .attr .flags @@ -277,11 +349,23 @@ fn generate_registered_class_impl( quote! { ::std::option::Option::None } }; + // Determine if the property is nullable (type is Option) + let nullable = is_option_type(field_ty).is_some(); + + // Get the default value as a PHP-compatible string for stub generation + let default_str = if let Some(default_expr) = &prop.attr.default { + let s = expr_to_php_default_string(default_expr); + quote! { ::std::option::Option::Some(#s) } + } else { + quote! { ::std::option::Option::None } + }; + // Use from_bits_retain to combine flags in a const context + // Tuple: (name, flags, default_value, docs, type, nullable, default_str) quote! { (#name, ::ext_php_rs::flags::PropertyFlags::from_bits_retain( (#base_flags).bits() | ::ext_php_rs::flags::PropertyFlags::Static.bits() - ), #default_value, &[#(#docs,)*] as &[&str]) + ), #default_value, &[#(#docs,)*] as &[&str], ::std::option::Option::Some(<#field_ty as ::ext_php_rs::convert::IntoZval>::TYPE), #nullable, #default_str) } }); @@ -391,8 +475,9 @@ fn generate_registered_class_impl( } #[must_use] - fn static_properties() -> &'static [(&'static str, ::ext_php_rs::flags::PropertyFlags, ::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>, &'static [&'static str])] { - static STATIC_PROPS: &[(&str, ::ext_php_rs::flags::PropertyFlags, ::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>, &[&str])] = &[#(#static_fields,)*]; + #[allow(clippy::type_complexity)] + fn static_properties() -> &'static [(&'static str, ::ext_php_rs::flags::PropertyFlags, ::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>, &'static [&'static str], ::std::option::Option<::ext_php_rs::flags::DataType>, bool, ::std::option::Option<&'static str>)] { + static STATIC_PROPS: &[(&str, ::ext_php_rs::flags::PropertyFlags, ::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>, &[&str], ::std::option::Option<::ext_php_rs::flags::DataType>, bool, ::std::option::Option<&'static str>)] = &[#(#static_fields,)*]; STATIC_PROPS } diff --git a/crates/macros/src/impl_.rs b/crates/macros/src/impl_.rs index 178e2768d4..995d5c86a2 100644 --- a/crates/macros/src/impl_.rs +++ b/crates/macros/src/impl_.rs @@ -479,6 +479,11 @@ impl<'a> ParsedImpl<'a> { prop: #prop_expr, flags: #flags, docs: &[#(#docs),*], + // Type info not available for getter/setter-based properties + ty: ::std::option::Option::None, + // Nullable and default not available for getter/setter-based properties + nullable: false, + default: ::std::option::Option::None, } ); } diff --git a/crates/macros/tests/expand/class.expanded.rs b/crates/macros/tests/expand/class.expanded.rs index 401b2853dc..1f04b343b9 100644 --- a/crates/macros/tests/expand/class.expanded.rs +++ b/crates/macros/tests/expand/class.expanded.rs @@ -37,11 +37,15 @@ impl ::ext_php_rs::class::RegisteredClass for MyClass { props } #[must_use] + #[allow(clippy::type_complexity)] fn static_properties() -> &'static [( &'static str, ::ext_php_rs::flags::PropertyFlags, ::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>, &'static [&'static str], + ::std::option::Option<::ext_php_rs::flags::DataType>, + bool, + ::std::option::Option<&'static str>, )] { static STATIC_PROPS: &[( &str, @@ -50,6 +54,9 @@ impl ::ext_php_rs::class::RegisteredClass for MyClass { &'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync), >, &[&str], + ::std::option::Option<::ext_php_rs::flags::DataType>, + bool, + ::std::option::Option<&'static str>, )] = &[]; STATIC_PROPS } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index bf41699165..044bf4ef37 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -2,6 +2,10 @@ #![cfg_attr(windows, feature(abi_vectorcall))] use ext_php_rs::{constant::IntoConst, prelude::*, types::ZendClassObject}; +/// A simple test class demonstrating ext-php-rs features. +/// +/// This class showcases property definitions, constants, and various +/// method types including constructors and static methods. #[derive(Debug)] #[php_class] pub struct TestClass { @@ -17,6 +21,14 @@ impl TestClass { pub const SOME_CONSTANT: i32 = 5; pub const SOME_OTHER_STR: &'static str = "Hello, world!"; + /// Creates a new `TestClass` instance. + /// + /// Both values are incremented by 10 before being stored. + /// + /// # Arguments + /// + /// * `a` - First value to store + /// * `b` - Second value to store pub fn __construct(a: i32, b: i32) -> Self { Self { a: a + 10, @@ -24,15 +36,31 @@ impl TestClass { } } + /// Tests camelCase conversion and default parameter values. + /// + /// # Arguments + /// + /// * `a` - First parameter with default value 5 + /// * `test` - Second parameter with default value 100 #[php(defaults(a = 5, test = 100))] pub fn test_camel_case(&self, a: i32, test: i32) { println!("a: {a} test: {test}"); } + /// Returns a static value. + /// + /// # Returns + /// + /// Always returns 5. fn x() -> i32 { 5 } + /// Demonstrates the builder pattern by returning self. + /// + /// # Returns + /// + /// Returns the same instance for method chaining. pub fn builder_pattern( self_: &mut ZendClassObject, ) -> &mut ZendClassObject { @@ -40,11 +68,21 @@ impl TestClass { } } +/// Creates a new `TestClass` instance with default values. +/// +/// # Returns +/// +/// A `TestClass` with a=1 and b=2. #[php_function] pub fn new_class() -> TestClass { TestClass { a: 1, b: 2 } } +/// Returns a friendly greeting. +/// +/// # Returns +/// +/// The string "Hello, world!". #[php_function] pub fn hello_world() -> &'static str { "Hello, world!" @@ -65,6 +103,15 @@ pub struct TestZvalConvert<'a> { c: &'a str, } +/// Demonstrates `ZvalConvert` derive macro usage. +/// +/// # Arguments +/// +/// * `z` - An object that will be converted from a PHP value +/// +/// # Returns +/// +/// Always returns 5. #[php_function] pub fn get_zval_convert(z: TestZvalConvert) -> i32 { dbg!(z); diff --git a/src/builders/class.rs b/src/builders/class.rs index 975a6d32e4..b80c02479e 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -17,6 +17,8 @@ use crate::{ zend_fastcall, }; +use crate::flags::DataType; + /// A constant entry: (name, `value_closure`, docs, `stub_value`) type ConstantEntry = ( String, @@ -25,6 +27,16 @@ type ConstantEntry = ( String, ); type PropertyDefault = Option Result>>; +/// A property entry: (name, flags, default, docs, type, nullable, `default_str`). +type PropertyEntry = ( + String, + PropertyFlags, + PropertyDefault, + DocComments, + Option, + bool, + Option<&'static str>, +); /// Builder for registering a class in PHP. #[must_use] @@ -35,7 +47,7 @@ pub struct ClassBuilder { pub(crate) interfaces: Vec, pub(crate) methods: Vec<(FunctionBuilder<'static>, MethodFlags)>, object_override: Option *mut ZendObject>, - pub(crate) properties: Vec<(String, PropertyFlags, PropertyDefault, DocComments)>, + pub(crate) properties: Vec, pub(crate) constants: Vec, register: Option, pub(crate) docs: DocComments, @@ -114,14 +126,22 @@ impl ClassBuilder { /// * `flags` - Flags relating to the property. See [`PropertyFlags`]. /// * `default` - Optional default value for the property. /// * `docs` - Documentation comments for the property. + /// * `ty` - Optional type for stub generation. + /// * `nullable` - Whether the property is nullable. + /// * `default_str` - Default value as PHP string for stub generation. + #[allow(clippy::too_many_arguments)] pub fn property>( mut self, name: T, flags: PropertyFlags, default: PropertyDefault, docs: DocComments, + ty: Option, + nullable: bool, + default_str: Option<&'static str>, ) -> Self { - self.properties.push((name.into(), flags, default, docs)); + self.properties + .push((name.into(), flags, default, docs, ty, nullable, default_str)); self } @@ -396,7 +416,7 @@ impl ClassBuilder { unsafe { zend_do_implement_interface(class, ptr::from_ref(interface).cast_mut()) }; } - for (name, flags, default, _) in self.properties { + for (name, flags, default, _, _ty, _nullable, _default_str) in self.properties { let mut default_zval = match default { Some(f) => f()?, None => Zval::new(), @@ -482,13 +502,23 @@ mod tests { #[test] fn test_property() { - let class = - ClassBuilder::new("Foo").property("bar", PropertyFlags::Public, None, &["Doc 1"]); + let class = ClassBuilder::new("Foo").property( + "bar", + PropertyFlags::Public, + None, + &["Doc 1"], + None, + false, + None, + ); assert_eq!(class.properties.len(), 1); assert_eq!(class.properties[0].0, "bar"); assert_eq!(class.properties[0].1, PropertyFlags::Public); assert!(class.properties[0].2.is_none()); assert_eq!(class.properties[0].3, &["Doc 1"] as DocComments); + assert!(class.properties[0].4.is_none()); + assert!(!class.properties[0].5); // nullable + assert!(class.properties[0].6.is_none()); // default_str } #[test] diff --git a/src/builders/module.rs b/src/builders/module.rs index 2f7a6f9e09..f7c975ccdf 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -418,14 +418,30 @@ impl ModuleBuilder<'_> { .expect("Failed to register constant"); } for (name, prop_info) in T::get_properties() { - builder = builder.property(name, prop_info.flags, None, prop_info.docs); + builder = builder.property( + name, + prop_info.flags, + None, + prop_info.docs, + prop_info.ty, + prop_info.nullable, + prop_info.default, + ); } - for (name, flags, default, docs) in T::static_properties() { + for (name, flags, default, docs, ty, nullable, default_str) in T::static_properties() { let default_fn = default.map(|v| { Box::new(move || v.as_zval(true)) as Box crate::error::Result> }); - builder = builder.property(*name, *flags, default_fn, docs); + builder = builder.property( + *name, + *flags, + default_fn, + docs, + *ty, + *nullable, + *default_str, + ); } if let Some(modifier) = T::BUILDER_MODIFIER { builder = modifier(builder); diff --git a/src/class.rs b/src/class.rs index dec8ff2db8..69941acec1 100644 --- a/src/class.rs +++ b/src/class.rs @@ -76,14 +76,22 @@ pub trait RegisteredClass: Sized + 'static { /// Returns the static properties provided by the class. /// /// Static properties are declared at the class level and managed by PHP, - /// not by Rust handlers. Each tuple contains (name, flags, default, docs). - /// The default value is optional - `None` means null default. + /// not by Rust handlers. Each tuple contains: + /// (name, flags, `default_value`, docs, type, nullable, `default_str`). + /// - `default_value` is the actual PHP default value (None means null) + /// - `type` is for stub generation + /// - `nullable` indicates if the property accepts null + /// - `default_str` is the PHP-compatible default string for stubs #[must_use] + #[allow(clippy::type_complexity)] fn static_properties() -> &'static [( &'static str, PropertyFlags, Option<&'static (dyn IntoZvalDyn + Sync)>, DocComments, + Option, + bool, + Option<&'static str>, )] { &[] } diff --git a/src/describe/mod.rs b/src/describe/mod.rs index 9e65feeccf..7679112732 100644 --- a/src/describe/mod.rs +++ b/src/describe/mod.rs @@ -385,17 +385,37 @@ pub struct Property { pub default: Option, } -impl From<(String, PropertyFlags, D, DocComments)> for Property { - fn from(value: (String, PropertyFlags, D, DocComments)) -> Self { - let (name, flags, _default, docs) = value; +impl + From<( + String, + PropertyFlags, + D, + DocComments, + std::option::Option, + bool, + std::option::Option<&'static str>, + )> for Property +{ + fn from( + value: ( + String, + PropertyFlags, + D, + DocComments, + std::option::Option, + bool, + std::option::Option<&'static str>, + ), + ) -> Self { + let (name, flags, _default, docs, prop_type, prop_nullable, prop_default_str) = value; let static_ = flags.contains(PropertyFlags::Static); let vis = Visibility::from(flags); - // TODO: Implement ty #376 - let ty = Option::None; - // TODO: Implement default #376 - let default = Option::::None; - // TODO: Implement nullable #376 - let nullable = false; + // Use the type from macro if provided (#184) + let ty: Option = prop_type.into(); + // Use default string from macro if provided (#376) + let default: Option = prop_default_str.map(Into::into).into(); + // Use nullable from macro (#376) + let nullable = prop_nullable; let docs = docs.into(); Self { @@ -662,7 +682,15 @@ mod tests { .extends((|| todo!(), "BaseClass")) .implements((|| todo!(), "Interface1")) .implements((|| todo!(), "Interface2")) - .property("prop1", PropertyFlags::Public, None, &["doc1"]) + .property( + "prop1", + PropertyFlags::Public, + None, + &["doc1"], + Some(DataType::String), + false, + None, + ) .method( FunctionBuilder::new("test_function", test_function), MethodFlags::Protected, @@ -682,7 +710,7 @@ mod tests { Property { name: "prop1".into(), docs: DocBlock(vec!["doc1".into()].into()), - ty: Option::None, + ty: Option::Some(DataType::String), vis: Visibility::Public, static_: false, nullable: false, @@ -713,6 +741,9 @@ mod tests { PropertyFlags::Protected, (), docs, + Some(DataType::Long), + false, // nullable + Some("'default'"), // default_str ) .into(); assert_eq!(property.name, "test_property".into()); @@ -720,6 +751,25 @@ mod tests { assert_eq!(property.vis, Visibility::Protected); assert!(!property.static_); assert!(!property.nullable); + assert_eq!(property.ty, Option::Some(DataType::Long)); + assert_eq!(property.default, Option::Some("'default'".into())); + } + + #[test] + fn test_property_nullable() { + let docs: &'static [&'static str] = &[]; + let property: Property = ( + "nullable_prop".to_string(), + PropertyFlags::Public, + (), + docs, + Some(DataType::String), + true, // nullable + Some("null"), // default_str + ) + .into(); + assert!(property.nullable); + assert_eq!(property.default, Option::Some("null".into())); } #[test] diff --git a/src/describe/stub.rs b/src/describe/stub.rs index 31c799a98c..4b5d21eb4a 100644 --- a/src/describe/stub.rs +++ b/src/describe/stub.rs @@ -736,6 +736,7 @@ impl ToStub for Property { } if let Option::Some(ty) = &self.ty { ty.fmt_stub(buf)?; + write!(buf, " ")?; } write!(buf, "${}", self.name)?; if let Option::Some(default) = &self.default { diff --git a/src/internal/property.rs b/src/internal/property.rs index 2bc90dc7a9..947e7abd39 100644 --- a/src/internal/property.rs +++ b/src/internal/property.rs @@ -1,7 +1,17 @@ -use crate::{describe::DocComments, flags::PropertyFlags, props::Property}; +use crate::{ + describe::DocComments, + flags::{DataType, PropertyFlags}, + props::Property, +}; pub struct PropertyInfo<'a, T> { pub prop: Property<'a, T>, pub flags: PropertyFlags, pub docs: DocComments, + /// The PHP type of the property (for stub generation). + pub ty: Option, + /// Whether the property is nullable (for stub generation). + pub nullable: bool, + /// Default value as a PHP-compatible string (for stub generation). + pub default: Option<&'static str>, }