Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Constructors with subclasses #138

Open
joehoyle opened this issue May 8, 2022 · 10 comments
Open

Constructors with subclasses #138

joehoyle opened this issue May 8, 2022 · 10 comments

Comments

@joehoyle
Copy link
Collaborator

joehoyle commented May 8, 2022

I'm not sure it's possible for classes via the php_impl macro to be subclassed, as the constructor will return Self, instead of the subclassed class. I might be misunderstanding how subclassing (from user land PHP) should be working though!

@ju1ius
Copy link
Contributor

ju1ius commented Nov 9, 2022

Indeed, this is not working as of version 0.8.1:

#[php_class]
pub struct Greeter(String);

#[php_impl]
impl Greeter {
  pub fn __construct(who: String) -> Self {
    Self(who)
  }
  
  pub fn greet(&self) -> String {
    format!("Hello, {}!", self.0)
  }
}
<?php

var_dump((new Greeter('world'))->greet()); // => "Hello, world!"

$example = new class("it doesn't work") extends Greeter
{
  public function greet(): string
  {
     return "Hello, it works!";
  }
};
var_dump($example->greet()); // => "Hello, it doesn't work!"

@joehoyle
Copy link
Collaborator Author

@joelwurtz I wonder if you've run into this issue / found a solution to this issue?

@joehoyle
Copy link
Collaborator Author

or maybe @danog !

@joehoyle
Copy link
Collaborator Author

I think the issue is around here: https://github.com/davidcole1340/ext-php-rs/blob/dddc07f587cd5d40f3b3ff64b9a59fba8299d953/src/builders/class.rs#L164C58-L168 where PHP passes a reference to the ce (which would be the subclass), but ext-php-rs ignores the ce provided by PHP and instead takes the static reference for the ce from the registered class' metadata.

@ju1ius
Copy link
Contributor

ju1ius commented Oct 24, 2023

@joehoyle Yes that's the issue. The create_object handler should just forward the zend_class_entry pointer to internal_new (the Zend engine guarantees that it points to either your registered class entry or a subclass thereof).

Then this pointer should be passed to zend_object_std_init et al.

@joehoyle
Copy link
Collaborator Author

@ju1ius ok cool, I had a quick go at trying that, but then ran into panics in the free_obj handler and such so I wasn't sure if I was going in the right direction or not!

@ju1ius
Copy link
Contributor

ju1ius commented Oct 24, 2023

Yeah, I tried to fix it sometime ago but ran into so much fundamental issues that I ended up writing my own library from scratch. 🤣

@joehoyle
Copy link
Collaborator Author

@ju1ius ah I see! Any chance that code is open sourced?

@joehoyle
Copy link
Collaborator Author

Ok I think I got this working as it happens in #277. I wasn't that familiar with how the ce was linked to the Rust struct, so I wrote up a playthrough as I understood it. If that's valuable to anyone else in the future, I've pasted it below:


Class Entry

In this library, when a struct is marked as #[php_class], this implements the RegisteredClass trait on the struct. This trait adds methods:

  • get_metadata() : &'static ClassMetadata
  • get_properties() : HashMap<String, Property>

ClassMetadata stores the ClassEntry (ce) for the #[php_class] struct.

Classes are registered in PHP with the zend_register_internal_class_ex( ce ) function. This library calls this via the ClassBuilder. ClassBuilder is constructed as part of the #[php_startup] macro, which essentially does:

ClassBuilder::new("MyClass").object_override::<MyClass>().build();

object_override() sets the create_object handler for the object, which essentially sets a custom externed C function as the handler when the class is instantiated from PHP.

The build call will register the class via the aformentioned zend_register_internal_class_ex() and other simliar things like registering class constants, properties etc.

That's essentially everythign that happens to register the class with the PHP runtime ahead of time. All other things are invoked once the class is called / instantiated from PHP.

Instantiation

When PHP user-land code instantiates the object with new MyClass(), the custom create_object is called. This handler creates a
ZendClassObject. That's a struct that couples the Rust struct instance MyClass with the ZendObject. ZendObject is the PHP Object, being an instance of the registered PHP class.

So, when the create_object handler is called, a new ZendClassObject is created via ZendClassObject::new_uninit. "New uninit" because the ZendClassObject will have a ZendObject (.std) but not an initialized instance of the Rust struct MyClass yet (.obj).

ZendClassObject::new_internal has some pretty funky magic. To provide a way to get the Rust struct for the ZendObject it manually allocates the memory for the ZendClassObject which is essentially two references: The &ZendObject (std) followed by the &MyClass reference (object) directly after. It's then always possible to get the instance of MyClass my manual memory offsets from the ZendObject if you only have a reference to the ZendObject. That's done via the ZendClassEntry::_from_zend_obj() helper static method.

So, the ZendObject is allocated with zend_object_std_init() and the ce for the ZendObject is taken from MyClass::get_metadata() method.

A reference to the ZendObject is returned from the create_object handler back to the PHP runtime.

Instantiation of the Rust struct happens when the constructor() PHP function is called. ClassBuilder has some boilerplate for the wrapped constructor externed function, which looks up the instance of the ZendClassObject via the ExecuteData.This and sets the new Rust struct in the obj property. The ZendClassObject is now initted.

Method Calling

We haven't talked about how method calling is brided in to Rust. When the PHP class is registered via the ClassBuilder with zend_register_internal_class_ex( ce ), the ClassEntry.ce.info.internal.builtin_functions pointer is pointed to the list of methods (which are moxed) from the ClassBuilder. Each function is a FunctionEntry (zend_function_entry). FunctionEntrys are created via the FunctionBuilder similar to the ClassBuilder.

The #[php_impl] macro is responsible for collecting all methods on the Rust impl and generating a wrapper FunctionEntry for each method. The wrapper accepts the ExecuteData and retval as the function callback, the ZendObject (i.e. PHP $this) is looked up form the ExecuteData. The sketchy memory offset ZendClassEntry::_from_zend_obj() essentially is then looked up and the Rust function is called on the Rust struct instance. All the usual IntoZval conversions are done in this wrapper too.

Inheritance and Sub-Classing

Thoug this isn't currently supported here's what likely needs to happen to make it work. In PHP inheritance this is only a single ZendObject of a Sub-class, so for all intents and purposes, the Sub-class ClassEntry shoulld be swapped via the parent ClassEntry. PHP will already attach the parent ClassEntry to the sub-class's ClassEntry and method / ::parent resolving will be handled automatically.

Because the Sub-class will inherit the create_object overriden callback to the parent class, the same create_object must also handle subclasses. Fortunately the ClassEntry is provided to the create_object handler. It should be a case of instead initting the ZendObject via zend_object_std_init with the ce from create_object.

@ju1ius
Copy link
Contributor

ju1ius commented Oct 26, 2023

@ju1ius ah I see! Any chance that code is open sourced?

It is planned yes, but no clear ETA. Probably sometime after the PHP 8.3 release.

joehoyle added a commit to humanmade/ext-php-rs that referenced this issue Nov 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants