Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion crates/cli/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@ 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
// to access the describe function. Missing here is the lifetime on `Symbol<'a, fn() ->
// Module>` where `ext_lib: 'a`.
#[allow(dead_code)]
ext_lib: Library,
#[cfg(unix)]
describe_fn: Symbol<extern "C" fn() -> Description>,
#[cfg(windows)]
describe_fn: Symbol<'static, extern "C" fn() -> Description>,
}

impl Ext {
Expand All @@ -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")
Expand Down
46 changes: 34 additions & 12 deletions crates/cli/tests/snapshots/stubs_snapshot__hello_world_stubs.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
}
95 changes: 90 additions & 5 deletions crates/macros/src/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` 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::*;

Expand Down Expand Up @@ -192,6 +246,7 @@ fn parse_fields<'a>(fields: impl Iterator<Item = &'a mut syn::Field>) -> Result<

result.push(Property {
ident,
ty: &field.ty,
name,
attr,
docs,
Expand All @@ -205,6 +260,7 @@ fn parse_fields<'a>(fields: impl Iterator<Item = &'a mut syn::Field>) -> 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<String>,
Expand Down Expand Up @@ -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
Expand All @@ -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<T>)
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,
})
}
});
Expand All @@ -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
Expand All @@ -277,11 +349,23 @@ fn generate_registered_class_impl(
quote! { ::std::option::Option::None }
};

// Determine if the property is nullable (type is Option<T>)
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)
}
});

Expand Down Expand Up @@ -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
}

Expand Down
5 changes: 5 additions & 0 deletions crates/macros/src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
);
}
Expand Down
7 changes: 7 additions & 0 deletions crates/macros/tests/expand/class.expanded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}
Expand Down
Loading
Loading