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

[RFC] Property hooks #13455

Open
wants to merge 44 commits into
base: master
Choose a base branch
from
Open

[RFC] Property hooks #13455

wants to merge 44 commits into from

Conversation

iluuu1994
Copy link
Member

Zend/zend_vm_def.h Outdated Show resolved Hide resolved
Comment on lines +173 to 175
case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL:
/* The argument passing optimizations are valid for prototypes as well,
* as inheritance cannot change between ref <-> non-ref arguments. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since hooks never receive by ref, we may be able to optimize ops to their non-ref variants like we do for known functions. E.g. for this code:

    public mixed $x {
        set => parent::$x::set($value['x']);
    }

we generate this:

0000 CV0($value) = RECV 1
0001 INIT_PARENT_PROPERTY_HOOK_CALL 1 string("x") 1
0002 CHECK_FUNC_ARG 1
0003 V1 = FETCH_DIM_FUNC_ARG CV0($value) string("x")
0004 SEND_FUNC_ARG V1 1
0005 DO_FCALL
0006 RETURN null

and it should optimize to something like this once the optimizer knows that hooks do not receive by ref:

0000 CV0($value) = RECV 1
0001 INIT_PARENT_PROPERTY_HOOK_CALL 1 string("x") 1
0003 T1 = FETCH_DIM_R CV0($value) string("x")
0004 SEND_VAL T1 1
0005 DO_FCALL
0006 RETURN null

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. Parent hooks are not very optimized at this moment. I think they will be rare in practice. I'll have a look nonetheless. We might be switching to a different syntax though (parent::$prop and parent::$prop = $value), if possible.

Zend/zend_ast.h Outdated Show resolved Hide resolved
Zend/zend_vm_def.h Outdated Show resolved Hide resolved
Zend/zend_compile.c Show resolved Hide resolved
Zend/zend_inheritance.c Outdated Show resolved Hide resolved
Zend/zend_inheritance.c Show resolved Hide resolved
Zend/zend_vm_def.h Outdated Show resolved Hide resolved
Zend/zend_compile.c Outdated Show resolved Hide resolved
Zend/zend_object_handlers.h Outdated Show resolved Hide resolved
Zend/zend_vm_def.h Outdated Show resolved Hide resolved
Zend/zend_ast.c Outdated Show resolved Hide resolved
Zend/zend_compile.h Show resolved Hide resolved
@TRowbotham
Copy link
Contributor

Potential scoping issue when hooks are defined in traits. Not sure if this is the expected behavior or not.

https://3v4l.org/8NILP/rfc#vrfc.property-hooks

@iluuu1994 iluuu1994 self-assigned this Mar 27, 2024
@iluuu1994 iluuu1994 changed the title [RFC] Property hooks Trait bug - https://github.com/php/php-src/pull/13455#issuecomment-2017098635 Mar 27, 2024
@iluuu1994 iluuu1994 changed the title Trait bug - https://github.com/php/php-src/pull/13455#issuecomment-2017098635 [RFC] Property hooks Mar 27, 2024
@iluuu1994
Copy link
Member Author

@TRowbotham Thanks for letting me know. I added this to my todo list.

@TRowbotham
Copy link
Contributor

Here is another one: https://3v4l.org/7Al8b/rfc#vrfc.property-hooks

When hooks are defined in a class that implements the \Iterator interface the iterator returns the values of each property's get hook instead of the values that the iterator contains.

@iluuu1994
Copy link
Member Author

@TRowbotham Good catch! I'll have a look at that one as well.

@TRowbotham
Copy link
Contributor

Ran into a behavior difference between __get/__set and property hooks when a property hook's setter calls a function, which reads a property hook's value from a different scope. With __get/__set it works without error, however, the property hooks version reports a fatal error. I couldn't tell from the RFC if the difference in behavior was intentional.

(property hooks) https://3v4l.org/6W26c/rfc#vrfc.property-hooks
(__get/__set) https://3v4l.org/3pUtD/rfc#vrfc.property-hooks

@iluuu1994
Copy link
Member Author

@TRowbotham This is intended behavior. Inside functions called from a hook, $this->prop refers to the backing field, as it would in the hook. However, the compiler cannot infer that that a backing field must be added, since it isn't used directly in the hook. As such, the property is marked as virtual. When doSomething is called, it tries to access this backing field that doesn't exist.

An additional difference is that, for the __get/__set case, each magic method is "guarded" on it's own. Guarding in this context means that inside the magic method, it won't be called again, but the underlying property will be accessed. If you assign a variable, invoking __set, __get can still be invoked inside __set. The same is not true for hooks, once you're in the hook $this->prop always refers to the backing value. This is intentional, because otherwise read/write assignments such as ++ within set might unexpectedly call your getter. Furthermore, it allows for more flexibility in the future, where self::$prop::get() may still be used as an explicit way to invoke the other hook.

I hope that clears it up.

@TRowbotham
Copy link
Contributor

@iluuu1994 Thanks for the explanation. That makes it clear.

@rodrigoslayertech
Copy link

Congratulations!!! RFC passed! PHP 8.4 will be AWESOME!!!

@ramsey
Copy link
Member

ramsey commented Apr 16, 2024

@rodrigoslayertech The voting is open until 29 April. Let's not count our chickens before they hatch. ;-)

@MarienMupenda
Copy link

Awsome, this sounds like the dynamic properties we have in laravel models, voted.

@claudepache
Copy link
Contributor

claudepache commented Apr 29, 2024

Hi,

While playing with the feature on 3v4l.org, I found the following bug. Although I’ve not tested the PR in its current state, I suspect that the bug might be still here, because I didn’t find test against it.

Test case (https://3v4l.org/fuj6p/rfc#vrfcproperty-hooks):

abstract class A {
    abstract public $x { get; }
}

class C extends A {
    private $_x;
    public $x {
        get => $this->_x;
    }
}

// expected: bool(true); actual: bool(false)
var_dump((new ReflectionProperty(C::class, 'x'))->isVirtual());

// expected: Error: Property C::$x is read-only; actual: no error
$c = new C;
$c->x = 3;

The test case works as expected when I replace the abstract class with an interface (https://3v4l.org/ed1TG/rfc#vrfc.property-hooks).

(Background: I expect to use that pattern in order to approximate asymmetric visibility.)

@iluuu1994
Copy link
Member Author

@claudepache The 3v4l build is indeed outdated. In any case, I'll make sure to have a look at your example. Thanks!

* Hooks without guards

* Skip stack overflowing tests on asan

* Avoid re-entry for non-ref hooks

* VM_ENTER fixes

* Fix more bugs

* Fix guard check for other objects

* Cache simple getter calls

* Fix reflection

Instead of modifying the current call frame, we use the trampoline we already
use for parent calls, and push a new stack frame. Unfortunately, this requires
setting scope of the function, which modifies some error messages. But because
A::get() is a confusing method, we now also need to allocate a new string for
$prop::get(), so that the full name in the message becomes A::$prop::get().

* Fix stack overflow tests on windows

* Avoid trampoline for {get,set}RawValue() for unhooked props

* Simplify duplicate check

* Add fast failure for missing cache_slot

* Tweak ASAN skip message
Filter inaccessible properties when constructing the array. This avoids looping
to find a valid index.
This will simplify handling two iterators.
Possibly buggy, not tested well yet.
The argument is no longer optional. This was adjusted in the stub file but
forgotten here.
@iluuu1994
Copy link
Member Author

The implementation now solves all known issues, and CI (including nightly) passes. So I will be merging it in the coming days.

@iluuu1994 iluuu1994 marked this pull request as ready for review July 11, 2024 15:27
}

/* Get-only virtual property can never be written to. */
if ((prop->flags & ZEND_ACC_VIRTUAL) && !prop->hooks[ZEND_PROPERTY_HOOK_SET]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it the intention that ZEND_ACC_VIRTUAL implies the existence of hooks? If we allow to specify @virtual in the stub file in a follow-up to avoid allocating backing storage, then this check can potentially dereference a NULL pointer because prop->hooks isn't necessarily filled in.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know all the checks by heart but at least here, ZEND_ACC_VIRTUAL assumes hooks are present. I presume your intention is to use @virtual for existing virtual properties implemented using the existing object property handlers? Tweaking the logic where necessary to check for the existence of hooks would be ok with me, if these cannot immediately be migrated to hooks (in part because internal classes aren't supported yet). But I'd prefer doing that after this has merged.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I presume your intention is to use @virtual for existing virtual properties implemented using the existing object property handlers?

Indeed.

Tweaking the logic where necessary to check for the existence of hooks would be ok with me, if these cannot immediately be migrated to hooks (in part because internal classes aren't supported yet)

I don't know how internal hooks would look like so can't really comment on this (yet).

But I'd prefer doing that after this has merged.

Of course

Copy link
Member Author

@iluuu1994 iluuu1994 Jul 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how internal hooks would look like so can't really comment on this (yet).

My intention is to allow marking the property with { get; set; } (or possibly just annotations until we have PHP-Parser support), which will look for and link the corresponding function into zend_property_info.hooks. So, hopefully it will not be too complicated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, those would be zend_function's right? That's a bit of a pitty because it would require completely reworking the virtual properties in DOM/XMLReader with the additional penalty of having to deal with a stackframe, unless they can be frameless.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Good question. Avoiding the stack frame was not planned for now, but I'll see what's possible once I get to it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet