Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Zend/zend.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,21 @@ struct _zend_class_entry {
zend_function *__callstatic;
zend_function *__tostring;
zend_function *__debugInfo;

/* magic functions for operator overloading */
zend_function *__add;
zend_function *__sub;
zend_function *__mul;
zend_function *__pow;
zend_function *__div;
zend_function *__concat;
zend_function *__mod;
zend_function *__sl;
zend_function *__sr;
zend_function *__or;
zend_function *__and;
zend_function *__xor;
Copy link

Choose a reason for hiding this comment

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

My two bits for better developer experience:

I suggest to use full names of methods, this is just not readable. __add, __subtract, __multiply etc. is much clearer.

Also, on* method naming convention came to my mind (__onAddition, __onMultiplication etc.) You can think about the operator as an event emitter ($a + $b) and the magic method as a handler. Not sure if it is a good idea, but I guess it is more explicit about when the method is run actually.


zend_function *serialize_func;
zend_function *unserialize_func;

Expand Down
48 changes: 48 additions & 0 deletions Zend/zend_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -1967,6 +1967,54 @@ ZEND_API void zend_check_magic_method_implementation(const zend_class_entry *ce,
} else if (name_len == sizeof(ZEND_DEBUGINFO_FUNC_NAME) - 1 &&
!memcmp(lcname, ZEND_DEBUGINFO_FUNC_NAME, sizeof(ZEND_DEBUGINFO_FUNC_NAME)-1) && fptr->common.num_args != 0) {
zend_error(error_type, "Method %s::%s() cannot take arguments", ZSTR_VAL(ce->name), ZEND_DEBUGINFO_FUNC_NAME);
} else if (name_len == sizeof(ZEND_ADD_FUNC_NAME) - 1 && !memcmp(lcname, ZEND_ADD_FUNC_NAME, sizeof(ZEND_ADD_FUNC_NAME) - 1)) {
if (fptr->common.num_args != 2) {
zend_error(error_type, "Method %s::%s() must take exactly 2 arguments", ZSTR_VAL(ce->name), ZEND_ADD_FUNC_NAME);
}
} else if (name_len == sizeof(ZEND_SUB_FUNC_NAME) - 1 && !memcmp(lcname, ZEND_SUB_FUNC_NAME, sizeof(ZEND_SUB_FUNC_NAME) - 1)) {
if (fptr->common.num_args != 2) {
zend_error(error_type, "Method %s::%s() must take exactly 2 arguments", ZSTR_VAL(ce->name), ZEND_SUB_FUNC_NAME);
}
} else if (name_len == sizeof(ZEND_MUL_FUNC_NAME) - 1 && !memcmp(lcname, ZEND_MUL_FUNC_NAME, sizeof(ZEND_MUL_FUNC_NAME) - 1)) {
if (fptr->common.num_args != 2) {
zend_error(error_type, "Method %s::%s() must take exactly 2 arguments", ZSTR_VAL(ce->name), ZEND_MUL_FUNC_NAME);
}
} else if (name_len == sizeof(ZEND_DIV_FUNC_NAME) - 1 && !memcmp(lcname, ZEND_DIV_FUNC_NAME, sizeof(ZEND_DIV_FUNC_NAME) - 1)) {
if (fptr->common.num_args != 2) {
zend_error(error_type, "Method %s::%s() must take exactly 2 arguments", ZSTR_VAL(ce->name), ZEND_DIV_FUNC_NAME);
}
} else if (name_len == sizeof(ZEND_POW_FUNC_NAME) - 1 && !memcmp(lcname, ZEND_POW_FUNC_NAME, sizeof(ZEND_POW_FUNC_NAME) - 1)) {
if (fptr->common.num_args != 2) {
zend_error(error_type, "Method %s::%s() must take exactly 2 arguments", ZSTR_VAL(ce->name), ZEND_POW_FUNC_NAME);
}
} else if (name_len == sizeof(ZEND_MOD_FUNC_NAME) - 1 && !memcmp(lcname, ZEND_MOD_FUNC_NAME, sizeof(ZEND_MOD_FUNC_NAME) - 1)) {
if (fptr->common.num_args != 2) {
zend_error(error_type, "Method %s::%s() must take exactly 2 arguments", ZSTR_VAL(ce->name), ZEND_MOD_FUNC_NAME);
}
} else if (name_len == sizeof(ZEND_CONCAT_FUNC_NAME) - 1 && !memcmp(lcname, ZEND_CONCAT_FUNC_NAME, sizeof(ZEND_CONCAT_FUNC_NAME) - 1)) {
if (fptr->common.num_args != 2) {
zend_error(error_type, "Method %s::%s() must take exactly 2 arguments", ZSTR_VAL(ce->name), ZEND_CONCAT_FUNC_NAME);
}
} else if (name_len == sizeof(ZEND_SHIFT_LEFT_FUNC_NAME) - 1 && !memcmp(lcname, ZEND_SHIFT_LEFT_FUNC_NAME, sizeof(ZEND_SHIFT_LEFT_FUNC_NAME) - 1)) {
if (fptr->common.num_args != 2) {
zend_error(error_type, "Method %s::%s() must take exactly 2 arguments", ZSTR_VAL(ce->name), ZEND_SHIFT_LEFT_FUNC_NAME);
}
} else if (name_len == sizeof(ZEND_SHIFT_RIGHT_FUNC_NAME) - 1 && !memcmp(lcname, ZEND_SHIFT_RIGHT_FUNC_NAME, sizeof(ZEND_SHIFT_RIGHT_FUNC_NAME) - 1)) {
if (fptr->common.num_args != 2) {
zend_error(error_type, "Method %s::%s() must take exactly 2 arguments", ZSTR_VAL(ce->name), ZEND_SHIFT_RIGHT_FUNC_NAME);
}
} else if (name_len == sizeof(ZEND_OR_FUNC_NAME) - 1 && !memcmp(lcname, ZEND_OR_FUNC_NAME, sizeof(ZEND_OR_FUNC_NAME) - 1)) {
if (fptr->common.num_args != 2) {
zend_error(error_type, "Method %s::%s() must take exactly 2 arguments", ZSTR_VAL(ce->name), ZEND_OR_FUNC_NAME);
}
} else if (name_len == sizeof(ZEND_AND_FUNC_NAME) - 1 && !memcmp(lcname, ZEND_AND_FUNC_NAME, sizeof(ZEND_AND_FUNC_NAME) - 1)) {
if (fptr->common.num_args != 2) {
zend_error(error_type, "Method %s::%s() must take exactly 2 arguments", ZSTR_VAL(ce->name), ZEND_AND_FUNC_NAME);
}
} else if (name_len == sizeof(ZEND_XOR_FUNC_NAME) - 1 && !memcmp(lcname, ZEND_XOR_FUNC_NAME, sizeof(ZEND_XOR_FUNC_NAME) - 1)) {
if (fptr->common.num_args != 2) {
zend_error(error_type, "Method %s::%s() must take exactly 2 arguments", ZSTR_VAL(ce->name), ZEND_XOR_FUNC_NAME);
}
}
}
/* }}} */
Expand Down
36 changes: 36 additions & 0 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -6092,6 +6092,42 @@ void zend_begin_method_decl(zend_op_array *op_array, zend_string *name, zend_boo
} else if (zend_string_equals_literal(lcname, ZEND_DEBUGINFO_FUNC_NAME)) {
zend_check_magic_method_attr(fn_flags, "__debugInfo", 0);
ce->__debugInfo = (zend_function *) op_array;
} else if (zend_string_equals_literal(lcname, ZEND_ADD_FUNC_NAME)) {
zend_check_magic_method_attr(fn_flags, "__add", 1);
ce->__add = (zend_function *) op_array;
} else if (zend_string_equals_literal(lcname, ZEND_SUB_FUNC_NAME)) {
zend_check_magic_method_attr(fn_flags, "__sub", 1);
ce->__sub = (zend_function *) op_array;
} else if (zend_string_equals_literal(lcname, ZEND_MUL_FUNC_NAME)) {
zend_check_magic_method_attr(fn_flags, "__mul", 1);
ce->__mul = (zend_function *) op_array;
} else if (zend_string_equals_literal(lcname, ZEND_POW_FUNC_NAME)) {
zend_check_magic_method_attr(fn_flags, "__pow", 1);
ce->__pow = (zend_function *) op_array;
} else if (zend_string_equals_literal(lcname, ZEND_DIV_FUNC_NAME)) {
zend_check_magic_method_attr(fn_flags, "__div", 1);
ce->__div = (zend_function *) op_array;
} else if (zend_string_equals_literal(lcname, ZEND_CONCAT_FUNC_NAME)) {
zend_check_magic_method_attr(fn_flags, "__concat", 1);
ce->__concat = (zend_function *) op_array;
} else if (zend_string_equals_literal(lcname, ZEND_MOD_FUNC_NAME)) {
zend_check_magic_method_attr(fn_flags, "__mod", 1);
ce->__mod = (zend_function *) op_array;
} else if (zend_string_equals_literal(lcname, ZEND_SHIFT_LEFT_FUNC_NAME)) {
zend_check_magic_method_attr(fn_flags, "__sl", 1);
ce->__sl = (zend_function *) op_array;
} else if (zend_string_equals_literal(lcname, ZEND_SHIFT_RIGHT_FUNC_NAME)) {
zend_check_magic_method_attr(fn_flags, "__sr", 1);
ce->__sr = (zend_function *) op_array;
} else if (zend_string_equals_literal(lcname, ZEND_OR_FUNC_NAME)) {
zend_check_magic_method_attr(fn_flags, "__or", 1);
ce->__or = (zend_function *) op_array;
} else if (zend_string_equals_literal(lcname, ZEND_AND_FUNC_NAME)) {
zend_check_magic_method_attr(fn_flags, "__and", 1);
ce->__and = (zend_function *) op_array;
} else if (zend_string_equals_literal(lcname, ZEND_XOR_FUNC_NAME)) {
zend_check_magic_method_attr(fn_flags, "__xor", 1);
ce->__xor = (zend_function *) op_array;
}

zend_string_release_ex(lcname, 0);
Expand Down
15 changes: 15 additions & 0 deletions Zend/zend_compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,21 @@ END_EXTERN_C()
#define ZEND_INVOKE_FUNC_NAME "__invoke"
#define ZEND_DEBUGINFO_FUNC_NAME "__debuginfo"


#define ZEND_ADD_FUNC_NAME "__add"
#define ZEND_SUB_FUNC_NAME "__sub"
#define ZEND_MUL_FUNC_NAME "__mul"
#define ZEND_DIV_FUNC_NAME "__div"
#define ZEND_POW_FUNC_NAME "__pow"
#define ZEND_CONCAT_FUNC_NAME "__concat"
#define ZEND_MOD_FUNC_NAME "__mod"
#define ZEND_SHIFT_LEFT_FUNC_NAME "__sl"
#define ZEND_SHIFT_RIGHT_FUNC_NAME "__sr"
#define ZEND_OR_FUNC_NAME "__or"
#define ZEND_AND_FUNC_NAME "__and"
#define ZEND_XOR_FUNC_NAME "__xor"


/* The following constants may be combined in CG(compiler_options)
* to change the default compiler behavior */

Expand Down
131 changes: 130 additions & 1 deletion Zend/zend_object_handlers.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include "zend_closures.h"
#include "zend_compile.h"
#include "zend_hash.h"
#include "zend_vm_opcodes.h"

#define DEBUG_OBJECT_HANDLERS 0

Expand Down Expand Up @@ -175,6 +176,134 @@ ZEND_API HashTable *zend_std_get_debug_info(zend_object *object, int *is_temp) /
}
/* }}} */


static int zend_std_call_op_handler(zend_uchar opcode, zval *result, zval *op1, zval *op2) /* {{{ */
{
zend_class_entry *ce;
zend_object *zobj;
if(Z_TYPE_P(op1) == IS_OBJECT) {
zobj = Z_OBJ_P(op1);
ce = Z_OBJCE_P(op1);
} else if(Z_TYPE_P(op2) == IS_OBJECT) {
zobj = Z_OBJ_P(op2);
ce = Z_OBJCE_P(op2);
} else {
return FAILURE;
}

zend_class_entry *orig_fake_scope = EG(fake_scope);
zend_fcall_info fci;
zend_fcall_info_cache fcic;
zval params[2] = {*op1, *op2};

EG(fake_scope) = NULL;

/* op handlers like __add are called with two operands op1, op2 */
fci.size = sizeof(fci);
fci.retval = result;
fci.param_count = 2;
fci.params = params;
fci.no_separation = 1;
//ZVAL_UNDEF(&fci.function_name); /* Unused */

do
{
fci.object = zobj;

/* Determine what handler should be used, based on opcode */
switch (opcode)
{
case ZEND_ADD:
fcic.function_handler = ce->__add;
ZVAL_STRING(&fci.function_name, ZEND_ADD_FUNC_NAME);
Copy link
Member

Choose a reason for hiding this comment

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

Is this actually needed? If the ce->__add etc are properly pre-initialized, then it should not be necessary to set the function name here.

Copy link
Author

Choose a reason for hiding this comment

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

The idea behind this, was to save the name of magic function called, so it can be for the usage notice below.
Maybe I should move this to the zend_error(E_NOTICE, ..) line, so the var is only created if really needed.

break;
case ZEND_SUB:
fcic.function_handler = ce->__sub;
ZVAL_STRING(&fci.function_name, ZEND_SUB_FUNC_NAME);
break;
case ZEND_MUL:
fcic.function_handler = ce->__mul;
ZVAL_STRING(&fci.function_name, ZEND_MUL_FUNC_NAME);
break;
case ZEND_DIV:
fcic.function_handler = ce->__div;
ZVAL_STRING(&fci.function_name, ZEND_DIV_FUNC_NAME);
break;
case ZEND_POW:
fcic.function_handler = ce->__pow;
ZVAL_STRING(&fci.function_name, ZEND_POW_FUNC_NAME);
break;
case ZEND_MOD:
fcic.function_handler = ce->__mod;
ZVAL_STRING(&fci.function_name, ZEND_MOD_FUNC_NAME);
break;
case ZEND_CONCAT:
fcic.function_handler = ce->__concat;
ZVAL_STRING(&fci.function_name, ZEND_CONCAT_FUNC_NAME);
break;
case ZEND_SL:
fcic.function_handler = ce->__sl;
ZVAL_STRING(&fci.function_name, ZEND_SHIFT_LEFT_FUNC_NAME);
break;
case ZEND_SR:
fcic.function_handler = ce->__sr;
ZVAL_STRING(&fci.function_name, ZEND_SHIFT_RIGHT_FUNC_NAME);
break;
case ZEND_BW_OR:
fcic.function_handler = ce->__or;
ZVAL_STRING(&fci.function_name, ZEND_OR_FUNC_NAME);
break;
case ZEND_BW_AND:
fcic.function_handler = ce->__and;
ZVAL_STRING(&fci.function_name, ZEND_AND_FUNC_NAME);
break;
case ZEND_BW_XOR:
fcic.function_handler = ce->__xor;
ZVAL_STRING(&fci.function_name, ZEND_XOR_FUNC_NAME);
break;
default:
return FAILURE;
break;
}

/* Check if function exists, check on other operand if possible */
if (fcic.function_handler == NULL)
{
if(zobj == Z_OBJ_P(op1) && Z_TYPE_P(op2) == IS_OBJECT) {
zobj = Z_OBJ_P(op2);
ce = Z_OBJCE_P(op2);
zval_ptr_dtor(&fci.function_name);
/* Retry checking if other operand has method */
continue;
}

zend_error(E_NOTICE, "You have to implement the %s function in class %s to use this operator with an object!",
Z_STRVAL(fci.function_name), ZSTR_VAL(ce->name));
zval_ptr_dtor(&fci.function_name);
return FAILURE;
}

fcic.called_scope = ce;
fcic.object = zobj;

int tmp = zend_call_function(&fci, &fcic);

if (!EG(exception) && (Z_TYPE_P(result) == IS_UNDEF || Z_TYPE_P(result) == IS_NULL))
{
zend_error(E_ERROR, "Method %s::%s must return a non-null value", ZSTR_VAL(ce->name), Z_STRVAL(fci.function_name));
Copy link
Member

Choose a reason for hiding this comment

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

This should use zend_type_error().

Copy link
Member

Choose a reason for hiding this comment

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

Though I'm wondering: Might it make sense to allow a null return value to indicate that this operand combination is not supported? In particular, I'm thinking that if you have $a * $b and A::__mul($a, $b) returns null, then B::__mul($a, $b) would be tried.

So if some library that provides A implements __mul but does not know about B, then B still has a chance to implement a __mul behavior if it knows about A.

Copy link
Author

Choose a reason for hiding this comment

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

This should use zend_type_error().

Thank you for the hint.

Might it make sense to allow a null return value to indicate that this operand combination is not supported?

I thought about this too. I think some mechanisms like this is highly useful, but i wonder if returning null value is the most intuitive way for that (returning null looks like the operator would return null). Maybe introducing a special Exception/Throwable, like OperandsNotSupportedException, that will be thrown in that cases and signal to use the other operands handler.

Choose a reason for hiding this comment

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

Throwing does carry overhead, perhaps a better option is a separate ce that can only be constructed via a call to an internal constant or method?

It could then return that, instead of throwing.

`
class Foo {
public static function __add(Foo $x, $y): Foo|ArithmeticOperationUnsupported {
if (!some_condition($y)) {
return Arithmetic::OPERATION_UNSUPPORTED;
}

return new Foo(...);
}
}
`

zval_ptr_dtor(&fci.function_name);
return FAILURE;
}

EG(fake_scope) = orig_fake_scope;

zval_ptr_dtor(&fci.function_name);

return tmp;
} while (1);
}
/* }}} */

static void zend_std_call_getter(zend_object *zobj, zend_string *prop_name, zval *retval) /* {{{ */
{
zend_class_entry *ce = zobj->ce;
Expand Down Expand Up @@ -1904,7 +2033,7 @@ ZEND_API const zend_object_handlers std_object_handlers = {
zend_std_get_debug_info, /* get_debug_info */
zend_std_get_closure, /* get_closure */
zend_std_get_gc, /* get_gc */
NULL, /* do_operation */
zend_std_call_op_handler, /* do_operation */
zend_std_compare_objects, /* compare */
NULL, /* get_properties_for */
};