Skip to content

Zend: Add object_init_with_constructor() API #14440

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

Merged
merged 14 commits into from
Jun 6, 2024

Conversation

Girgias
Copy link
Member

@Girgias Girgias commented Jun 2, 2024

This will instantiate the object and execute its constructor with the given parameters.

As it came up in #14418

Edit: I probably also should add support for a named argument HashTable.

@Girgias Girgias force-pushed the object_init_constructor_api branch from f5c8ecb to 05ae5ed Compare June 3, 2024 00:21
@Girgias Girgias requested a review from kocsismate as a code owner June 3, 2024 00:21
named_params
);
if (Z_TYPE(retval) == IS_UNDEF) {
return FAILURE;
Copy link
Member

@arnaud-lb arnaud-lb Jun 3, 2024

Choose a reason for hiding this comment

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

We should set the IS_OBJ_DESTRUCTOR_CALLED flag to match to behavior or new and ReflectionClass::newInstance().

Should we also release the object, or let the caller release it?

Currently the caller can not tell if arg is initialized or not when object_init_with_constructor() returns FAILURE.

Copy link
Member Author

Choose a reason for hiding this comment

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

TIL about zend_object_store_ctor_failed, I think it indeed makes sense to call it.

I was thinking I had a bug in not releasing the object, but I couldn't produce it with a test so I left it, will try and see if I can come up with something and add some destructors to the tests.

Copy link
Member

Choose a reason for hiding this comment

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

The object is useless because it's incompletely initialized, so the caller can't do much with it. I think I'd prefer it it were released and not returned at all.

Copy link
Member

Choose a reason for hiding this comment

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

This discussion reminded me of my PR #14161 (“Add zend_get_attribute_object()”), which also automatically destructs the object on constructor failure.

For the #[\Deprecated] PR, I've made a follow-up to also ZVAL_UNDEF() the zval then, making it safe to unconditionally zval_ptr_dtor() the zval no matter if the constructor failed or not:

4211f4d

I would suggest the same here: If the constructor fails, the object is destroyed and the zval UNDEF'd.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah this is what I am doing

Zend/zend_API.c Outdated
zend_object *obj = Z_OBJ_P(arg);
zend_function *constructor = obj->handlers->get_constructor(obj);
if (UNEXPECTED(constructor == NULL)) {
return FAILURE;
Copy link
Member

Choose a reason for hiding this comment

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

Depending on the use-case of this function, I think that returning SUCCESS here when no arguments were passed, would be a better API. Otherwise we can not use this function as an internal equivalent of new without checking the existence of a constructor before that.

ReflectionClass::newInstance() does that, and throws Class %s does not have a constructor, so you cannot pass any constructor arguments when arguments where passed.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is actually a good point, really wondering if the SPL API that was exposed was not completely broken in reality now...

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 am wondering, should we throw an exception, or just allow it to initialize like a new call with too many arguments? As this is not currently an error in PHP: https://3v4l.org/CHps3, and it would be inconsistent with passing too many parameters which would initialize it.

Copy link
Member

@arnaud-lb arnaud-lb Jun 3, 2024

Choose a reason for hiding this comment

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

In this case I would prefer to follow the behavior of new, then. Because if this is used by user-facing APIs, users would expect that behavior.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually I think we need to handle internal and userland classes differently.

Need to add a test for this when I come back to work on it.

@Girgias
Copy link
Member Author

Girgias commented Jun 4, 2024

So it turns out, initializing an object by calling the constructor and have the behaviour be identical to new is really fricking hard, because the semantics are all over the place.

I must say, I'm not sure the get_constructor object handler is that useful, as it is only overloaded by opaque objects to throw an Error to indicate that it cannot be initialized and another extension function must be called.

Every other object uses the standard object hook provided by Zend.

I can see a couple of ways to improve the semantics here:

  1. For classes that do not define an explicit constructor, have zend_std_get_constructor return the internal pass function zend_pass_function
  2. Make zend_pass_function the default for the constructor field of CEs
  3. Have opaque objects implement a public constructor that throws the same Error as it currently does in their get_constructor handler
  4. Have NULL for the constructor field of a CE mean the object cannot be initialized traditionally, this would mean foregoing the custom Error exception messages

I must say, it is also slightly surprising that internal classes that don't define a constructor can take arbitrary amount of positional arguments, this feels like it goes against what one would normally expect of internal objects/functions

Zend/zend_API.c Outdated
/* Throw standard Error */
zend_string *arg_name = NULL;
zend_hash_get_current_key(named_params, &arg_name, /* num_index */ NULL);
ZEND_ASSERT(arg_name != NULL);
Copy link
Member

Choose a reason for hiding this comment

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

I think it's actually allowed to pass an empty hash table to the underlying zend_call_known_function function.
And API users might expect the same to hold for this function.
However, in that case this line will fail.

Copy link
Member Author

Choose a reason for hiding this comment

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

Indeed, fixed this.

Copy link
Member

@iluuu1994 iluuu1994 left a comment

Choose a reason for hiding this comment

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

Can this new function now be used somewhere? That would prove its correctness better than the test.

@Girgias
Copy link
Member Author

Girgias commented Jun 6, 2024

Can this new function now be used somewhere? That would prove its correctness better than the test.

I would use it in the places in #14418 and @TimWolla has told me it would be convenient for the Deprecated Attribute.

@TimWolla
Copy link
Member

TimWolla commented Jun 6, 2024

has told me it would be convenient for the Deprecated Attribute.

More specifically for error handling in zend_get_attribute_object, because it currently leaves a broken object in case the constructor fails, instead of setting the zval to UNDEF.

@Girgias Girgias force-pushed the object_init_constructor_api branch from 73c3cb7 to 9ad870a Compare June 6, 2024 15:13
Copy link
Member

@iluuu1994 iluuu1994 left a comment

Choose a reason for hiding this comment

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

LGTM. I find the control flow a bit confusing, I would prefer a common failure path:
https://gist.github.com/iluuu1994/8e2ae4715ef29176c2b97c37443d0e2f

But I'll leave it up to you if that's something you want to do.

@Girgias Girgias merged commit 51379d6 into php:master Jun 6, 2024
11 checks passed
@Girgias Girgias deleted the object_init_constructor_api branch June 6, 2024 20:21
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.

5 participants