Skip to content

Zend: add new zend_object_init{_with_constructor}() API - #24135

Open
Girgias wants to merge 17 commits into
php:masterfrom
Girgias:2026-09-zend_object_init_api
Open

Girgias wants to merge 17 commits into
php:masterfrom
Girgias:2026-09-zend_object_init_api

Conversation

@Girgias

@Girgias Girgias commented Oct 5, 2026

Copy link
Copy Markdown
Member

Commits should be reviewed individually.

The motivation is that creating an object shouldn't require providing a zval backing storage, as often one only cares about the zend_object*. The common case where we do have a backing storage is within userland functions where return_value is passed, this is IMHO suboptimal as it leaks an implementation detail that is usually hidden by the use of the RETURN/RETVAL macros, moreover it can make determining what the return value of userland function is confusing if return_value is initialized and a plain return statement is used to return early.

The commits follow the rough idea:

  • Minor preparatory refactorings
  • Introduction of new API
  • Partial migration to the new API

Obviously not all cases have been migrated, sometimes it makes more sense to use the existing object_init_ex API, or moving to the new API is desirable but various other refactorings need to be made at an extension level for it to make sense.

Some future refactoring I am thinking of doing with the new APIs are:

  • reflection
  • intl
  • SPL

Comment thread Zend/zend_API.h Outdated
zval_ptr_dtor(&errors_array);

zend_throw_exception_object(&ex);
zend_throw_exception_internal(ex);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seeing this change, I wonder if the instanceof check should become an assertion in zend_throw_exception_internal.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm guessing you mean in zend_throw_exception_object()? And it cannot as it is used by the ZEND_THROW opcode.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No. I meant zend_throw_exception_internal(). Given that more users switched to zend_throw_exception_internal() directly, the instanceof check here in zend_throw_exception_object() could also become an assertion in zend_throw_exception_internal(), since the type system can only guarantee it's an object, but not that it's a throwable.

Comment thread Zend/zend_API.c Outdated
Comment thread Zend/zend_API.c Outdated
Comment thread Zend/zend_API.c Outdated
Comment thread Zend/zend_API.c
zval_ptr_dtor(arg);
ZVAL_UNDEF(arg);
return FAILURE;
zend_object_store_ctor_failed(obj);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this change fixing a pre-existing bug?

Copy link
Copy Markdown
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 think it does, as that branch is tested via the ext/zend_test/tests/zend_object_init_with_constructor.phpt and the destructor doesn't seem to be called already. So I'm slightly confused. But this whole logic will go away when I get back round to remove the annoying get_constructor object handler.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree this looks like a sus behaviour change

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be testable with a non-final class that throws with a destructor. I don't think we have a class already that can show this difference

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Isn't that case already covered by the ThrowingUser class in ext/zend_test/tests/zend_object_init_with_constructor.phpt?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No, you're looking for a class that throws in get_constructor without implicitly already calling zend_object_store_ctor_failed, and then subclass that with a destructor

@Girgias
Girgias force-pushed the 2026-09-zend_object_init_api branch from c5a38d1 to ada1544 Compare October 13, 2026 13:59
@Girgias
Girgias requested a review from TimWolla October 13, 2026 17:30

@TimWolla TimWolla left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Didn't check all extensions, I trust you with those. Some additional smaller nits, overall LGTM.

Comment thread Zend/zend_API.h Outdated
Comment thread Zend/zend_execute.c Outdated
Comment thread Zend/zend_execute.c Outdated
Comment thread Zend/zend_closures.c Outdated
@Girgias
Girgias force-pushed the 2026-09-zend_object_init_api branch from ada1544 to 425e7ae Compare October 14, 2026 18:00
@Girgias
Girgias requested a review from TimWolla October 14, 2026 18:00
@Girgias
Girgias force-pushed the 2026-09-zend_object_init_api branch from 425e7ae to 203213a Compare October 14, 2026 20:34

@TimWolla TimWolla left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Still couldn't be bothered to check all extensions. Changes LGTM now. Further comments are optional, either way no further review round necessary for them.

Comment thread Zend/zend_exceptions.c
#endif /* HAVE_DTRACE */

if (exception != NULL) {
#ifdef ZEND_DEBUG

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Explicit ZEND_DEBUG check should not be necessary for ZEND_ASSERT() to be elided including its calls (but perhaps check the assembly to verify).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That's false. That requires the compiler to know the function is pure, which it doesn't at this point. The same ifdef pattern is used in the VM for a similar check.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should also be #if, not #ifdef, because the macro is always defined: it's just either 0 or 1

Comment thread Zend/zend_execute.c

/* Extract the $message property. */
z = zend_read_property_ex(zend_ce_nodiscard, Z_OBJ_P(&obj), ZSTR_KNOWN(ZEND_STR_MESSAGE), false, NULL);
zval *z = zend_read_property_ex(zend_ce_nodiscard, obj, ZSTR_KNOWN(ZEND_STR_MESSAGE), false, NULL);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could also declare that separately for consistency with Deprecated.

Comment thread main/streams/userspace.c
add_property_resource(&object, "context", context->res);
zval tmp;
ZVAL_RES(&tmp, context->res);
zend_update_property(ce, object, ZEND_STRL("context"), &tmp);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This causes a refcount leak because add_property_resource dtors the input, so you need to remove the GC_ADDREF on line 262

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also this new code has a behaviour change: it will behave differently wrt private/protected properties due to the fake scope.

Comment thread Zend/zend_API.c
zval_ptr_dtor(arg);
ZVAL_UNDEF(arg);
return FAILURE;
zend_object_store_ctor_failed(obj);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree this looks like a sus behaviour change

Comment thread Zend/zend_API.c
zval_ptr_dtor(arg);
ZVAL_UNDEF(arg);
return FAILURE;
zend_object_store_ctor_failed(obj);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be testable with a non-final class that throws with a destructor. I don't think we have a class already that can show this difference

Comment thread ext/ftp/php_ftp.c
ftp->use_ssl = false;
#endif

object_init_ex(return_value, php_ftp_ce);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I disagree that the new code is better, the old code was more compact and return_value is open-coded plenty of times in the code base.

Comment thread Zend/zend_lazy_objects.c
object_init_ex(&zv, old_obj->ce);
GC_ADD_FLAGS(Z_OBJ(zv), IS_OBJ_DESTRUCTOR_CALLED);
return Z_OBJ(zv);
zend_object *obj = zend_object_init(old_obj->ce);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This on the other hand is a code change where the new coed looks better, same with the code above

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.

3 participants