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
68 changes: 68 additions & 0 deletions Zend/zend_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -1846,6 +1846,74 @@ ZEND_API zend_result object_init_ex(zval *arg, zend_class_entry *class_type) /*
}
/* }}} */

ZEND_API zend_result object_init_with_constructor(zval *arg, zend_class_entry *class_type, uint32_t param_count, zval *params, HashTable *named_params) /* {{{ */
{
zend_result status = _object_and_properties_init(arg, class_type, NULL);
if (UNEXPECTED(status == FAILURE)) {
ZVAL_UNDEF(arg);
return FAILURE;
}
zend_object *obj = Z_OBJ_P(arg);
zend_function *constructor = obj->handlers->get_constructor(obj);
if (constructor == NULL) {
/* The constructor can be NULL for 2 different reasons:
* - It is not defined
* - We are not allowed to call the constructor (e.g. private, or internal opaque class)
* and an exception has been thrown
* in the former case, we are (mostly) done and the object is initialized,
* in the latter we need to destroy the object as initialization failed
*/
if (UNEXPECTED(EG(exception))) {
zval_ptr_dtor(arg);
ZVAL_UNDEF(arg);
return FAILURE;
}

/* Surprisingly, this is the only case where internal classes will allow to pass extra arguments
* However, if there are named arguments (and it is not empty),
* an Error must be thrown to be consistent with new ClassName() */
if (UNEXPECTED(named_params != NULL && zend_hash_num_elements(named_params) != 0)) {
/* 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);
zend_throw_error(NULL, "Unknown named parameter $%s", ZSTR_VAL(arg_name));
zend_string_release(arg_name);
/* Do not call destructor, free object, and set arg to IS_UNDEF */
zend_object_store_ctor_failed(obj);
zval_ptr_dtor(arg);
ZVAL_UNDEF(arg);
return FAILURE;
} else {
return SUCCESS;
}
}
/* A constructor should not return a value, however if an exception is thrown
* zend_call_known_function() will set the retval to IS_UNDEF */
zval retval;
zend_call_known_function(
constructor,
obj,
class_type,
&retval,
param_count,
params,
named_params
);
if (Z_TYPE(retval) == IS_UNDEF) {
/* Do not call destructor, free object, and set arg to IS_UNDEF */
zend_object_store_ctor_failed(obj);
zval_ptr_dtor(arg);
ZVAL_UNDEF(arg);
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

} else {
/* Unlikely, but user constructors may return any value they want */
zval_ptr_dtor(&retval);
return SUCCESS;
}
}
/* }}} */

ZEND_API void object_init(zval *arg) /* {{{ */
{
ZVAL_OBJ(arg, zend_objects_new(zend_standard_class_def));
Expand Down
1 change: 1 addition & 0 deletions Zend/zend_API.h
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ ZEND_API const char *zend_get_type_by_const(int type);
#define array_init_size(arg, size) ZVAL_ARR((arg), zend_new_array(size))
ZEND_API void object_init(zval *arg);
ZEND_API zend_result object_init_ex(zval *arg, zend_class_entry *ce);
ZEND_API zend_result object_init_with_constructor(zval *arg, zend_class_entry *class_type, uint32_t param_count, zval *params, HashTable *named_params);
ZEND_API zend_result object_and_properties_init(zval *arg, zend_class_entry *ce, HashTable *properties);
ZEND_API void object_properties_init(zend_object *object, zend_class_entry *class_type);
ZEND_API void object_properties_init_ex(zend_object *object, HashTable *properties);
Expand Down
23 changes: 23 additions & 0 deletions ext/zend_test/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,29 @@ static ZEND_FUNCTION(zend_call_method)
zend_call_method(obj, ce, NULL, ZSTR_VAL(method_name), ZSTR_LEN(method_name), return_value, argc - 2, arg1, arg2);
}

/* Instantiate a class and run the constructor via object_init_with_constructor */
static ZEND_FUNCTION(zend_object_init_with_constructor)
{
zend_class_entry *ce = NULL;
zval *args;
uint32_t num_args;
HashTable *named_args;

ZEND_PARSE_PARAMETERS_START(1, -1)
Z_PARAM_CLASS(ce)
Z_PARAM_VARIADIC_WITH_NAMED(args, num_args, named_args)
ZEND_PARSE_PARAMETERS_END();

zval obj;
/* We don't use return_value directly to check for memory leaks of the API on failure */
zend_result status = object_init_with_constructor(&obj, ce, num_args, args, named_args);
if (status == FAILURE) {
RETURN_THROWS();
}
ZEND_ASSERT(!EG(exception));
ZVAL_COPY_VALUE(return_value, &obj);
}

static ZEND_FUNCTION(zend_get_unit_enum)
{
ZEND_PARSE_PARAMETERS_NONE();
Expand Down
2 changes: 2 additions & 0 deletions ext/zend_test/test.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ function zend_get_current_func_name(): string {}

function zend_call_method(object|string $obj_or_class, string $method, mixed $arg1 = UNKNOWN, mixed $arg2 = UNKNOWN): mixed {}

function zend_object_init_with_constructor(string $class, mixed ...$args): mixed {}

function zend_test_zend_ini_parse_quantity(string $str): int {}
function zend_test_zend_ini_parse_uquantity(string $str): int {}

Expand Down
9 changes: 8 additions & 1 deletion ext/zend_test/test_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

165 changes: 165 additions & 0 deletions ext/zend_test/tests/zend_object_init_with_constructor.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
--TEST--
Zend: Test object_init_with_constructor() API
--EXTENSIONS--
zend_test
sysvmsg
--FILE--
<?php

class PrivateUser {
private function __construct() {
return new stdClass();
}
public function __destruct() {
echo 'Destructor for ', __CLASS__, PHP_EOL;
}
}

class ThrowingUser {
public function __construct() {
throw new Exception("Don't construct");
}
public function __destruct() {
echo 'Destructor for ', __CLASS__, PHP_EOL;
}
}

abstract class AbstractClass {
public function __construct() {
return new stdClass();
}
public function __destruct() {
echo 'Destructor for ', __CLASS__, PHP_EOL;
}
}

class TestUserWithConstructorArgs {
public function __construct(int $int_param, string $string_param) {
return new stdClass();
}
public function __destruct() {
echo 'Destructor for ', __CLASS__, PHP_EOL;
}
}

class TestUserWithConstructorNoParams {
public function __construct() {
return new stdClass();
}
public function __destruct() {
echo 'Destructor for ', __CLASS__, PHP_EOL;
}
}

echo "Testing impossible initializations\n";
try {
$o = zend_object_init_with_constructor("_ZendTestInterface");
var_dump($o);
unset($o);
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}
try {
$o = zend_object_init_with_constructor("_ZendTestTrait");
var_dump($o);
unset($o);
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}
try {
$o = zend_object_init_with_constructor("ZendTestUnitEnum");
var_dump($o);
unset($o);
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}
try {
$o = zend_object_init_with_constructor("AbstractClass");
var_dump($o);
unset($o);
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}
try {
$o = zend_object_init_with_constructor("SysvMessageQueue");
var_dump($o);
unset($o);
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}
try {
$o = zend_object_init_with_constructor("PrivateUser");
var_dump($o);
unset($o);
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}
try {
$o = zend_object_init_with_constructor("ThrowingUser");
var_dump($o);
unset($o);
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

echo "Testing param passing\n";
try {
$o = zend_object_init_with_constructor("TestUserWithConstructorArgs");
var_dump($o);
unset($o);
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}
try {
$o = zend_object_init_with_constructor("TestUserWithConstructorArgs", "str", 5);
var_dump($o);
unset($o);
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}
try {
$o = zend_object_init_with_constructor("TestUserWithConstructorArgs", 5, string_param: "str", unused_param: 15.3);
var_dump($o);
unset($o);
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

$o = zend_object_init_with_constructor("TestUserWithConstructorArgs", 5, string_param: "str");
var_dump($o);
unset($o);

echo "Passing too many args to constructor\n";
$o = zend_object_init_with_constructor("TestUserWithConstructorArgs", 5, "str", 'unused_param');
var_dump($o);
unset($o);

echo "Testing class with defined constructor and no params\n";
$o = zend_object_init_with_constructor("TestUserWithConstructorNoParams");
var_dump($o);
unset($o);
?>
--EXPECT--
Testing impossible initializations
Error: Cannot instantiate interface _ZendTestInterface
Error: Cannot instantiate trait _ZendTestTrait
Error: Cannot instantiate enum ZendTestUnitEnum
Error: Cannot instantiate abstract class AbstractClass
Error: Cannot directly construct SysvMessageQueue, use msg_get_queue() instead
Error: Call to private PrivateUser::__construct() from global scope
Exception: Don't construct
Testing param passing
ArgumentCountError: Too few arguments to function TestUserWithConstructorArgs::__construct(), 0 passed and exactly 2 expected
TypeError: TestUserWithConstructorArgs::__construct(): Argument #1 ($int_param) must be of type int, string given
Error: Unknown named parameter $unused_param
object(TestUserWithConstructorArgs)#1 (0) {
}
Destructor for TestUserWithConstructorArgs
Passing too many args to constructor
object(TestUserWithConstructorArgs)#1 (0) {
}
Destructor for TestUserWithConstructorArgs
Testing class with defined constructor and no params
object(TestUserWithConstructorNoParams)#1 (0) {
}
Destructor for TestUserWithConstructorNoParams
Loading
Loading