From 9a646466c0cac8de0f27a4d48ddaf8db221d006f Mon Sep 17 00:00:00 2001 From: Ilija Tovilo <ilija.tovilo@me.com> Date: Thu, 17 Oct 2024 00:32:21 +0200 Subject: [PATCH 1/2] Implement frameless static calls --- Zend/Optimizer/zend_dump.c | 5 +++- Zend/zend_compile.c | 24 +++++++++++++++++++ build/gen_stub.php | 18 ++++++++++---- ext/zend_test/test.c | 24 +++++++++++++++++++ ext/zend_test/test.stub.php | 3 +++ ext/zend_test/test_arginfo.h | 22 ++++++++++++++++- .../tests/frameless_static_methods.phpt | 12 ++++++++++ 7 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 ext/zend_test/tests/frameless_static_methods.phpt diff --git a/Zend/Optimizer/zend_dump.c b/Zend/Optimizer/zend_dump.c index 4e46b38a8eb5e..b171eaebd1890 100644 --- a/Zend/Optimizer/zend_dump.c +++ b/Zend/Optimizer/zend_dump.c @@ -479,7 +479,10 @@ ZEND_API void zend_dump_op(const zend_op_array *op_array, const zend_basic_block if (ZEND_OP_IS_FRAMELESS_ICALL(opline->opcode)) { zend_function *func = ZEND_FLF_FUNC(opline); - fprintf(stderr, "(%s)", ZSTR_VAL(func->common.function_name)); + fprintf(stderr, "(%s%s%s)", + func->common.scope ? ZSTR_VAL(func->common.scope->name) : "", + func->common.scope ? "::" : "", + ZSTR_VAL(func->common.function_name)); } if (ZEND_VM_EXT_NUM == (flags & ZEND_VM_EXT_MASK)) { diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index e33990c73bb67..1a402fd43c846 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -59,6 +59,13 @@ #define FC(member) (CG(file_context).member) +#define ZEND_OP1_LITERAL(opline) (op_array)->literals[(opline)->op1.constant] +#define ZEND_OP2_LITERAL(opline) (op_array)->literals[(opline)->op2.constant] +#define literal_dtor(zv) do { \ + zval_ptr_dtor_nogc(zv); \ + ZVAL_NULL(zv); \ + } while (0) + typedef struct _zend_loop_var { uint8_t opcode; uint8_t var_type; @@ -5336,6 +5343,7 @@ static void zend_compile_static_call(znode *result, zend_ast *ast, uint32_t type } } + uint32_t init_opnum = get_next_op_number(); opline = get_next_op(); opline->opcode = ZEND_INIT_STATIC_METHOD_CALL; @@ -5378,6 +5386,22 @@ static void zend_compile_static_call(znode *result, zend_ast *ast, uint32_t type } } + if (!(CG(compiler_options) & ZEND_COMPILE_NO_BUILTINS) + && fbc + && (fbc->type == ZEND_INTERNAL_FUNCTION) + && zend_ast_is_list(args_ast) + && !zend_args_contain_unpack_or_named(zend_ast_get_list(args_ast))) { + if (zend_compile_frameless_icall(result, zend_ast_get_list(args_ast), fbc, type) != (uint32_t)-1) { + /* Update opline in case it got invalidated. */ + zend_op_array *op_array = CG(active_op_array); + opline = &op_array->opcodes[init_opnum]; + literal_dtor(&ZEND_OP1_LITERAL(opline)); + literal_dtor(&ZEND_OP2_LITERAL(opline)); + MAKE_NOP(opline); + return; + } + } + zend_compile_call_common(result, args_ast, fbc, zend_ast_get_lineno(method_ast)); } /* }}} */ diff --git a/build/gen_stub.php b/build/gen_stub.php index f8e16064ee292..d4eff6a16ae57 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -1060,6 +1060,10 @@ public function getDeclarationClassName(): string { return implode('_', $this->className->getParts()); } + public function getDeclarationName(): string { + return "{$this->getDeclarationClassName()}_{$this->methodName}"; + } + public function getDeclaration(): string { return "ZEND_METHOD({$this->getDeclarationClassName()}, $this->methodName);\n"; } @@ -1068,6 +1072,10 @@ public function getArgInfoName(): string { return "arginfo_class_{$this->getDeclarationClassName()}_{$this->methodName}"; } + public function getFramelessFunctionInfosName(): string { + return "frameless_function_infos_{$this->className}_{$this->methodName}"; + } + public function getMethodSynopsisFilename(): string { $parts = [...$this->className->getParts(), ltrim($this->methodName, '_')]; @@ -1331,12 +1339,12 @@ public function getFramelessDeclaration(): ?string { } foreach ($this->framelessFunctionInfos as $framelessFunctionInfo) { - $code .= "ZEND_FRAMELESS_FUNCTION({$this->name->getFunctionName()}, {$framelessFunctionInfo->arity});\n"; + $code .= "ZEND_FRAMELESS_FUNCTION({$this->name->getDeclarationName()}, {$framelessFunctionInfo->arity});\n"; } $code .= 'static const zend_frameless_function_info ' . $this->getFramelessFunctionInfosName() . "[] = {\n"; foreach ($this->framelessFunctionInfos as $framelessFunctionInfo) { - $code .= "\t{ ZEND_FRAMELESS_FUNCTION_NAME({$this->name->getFunctionName()}, {$framelessFunctionInfo->arity}), {$framelessFunctionInfo->arity} },\n"; + $code .= "\t{ ZEND_FRAMELESS_FUNCTION_NAME({$this->name->getDeclarationName()}, {$framelessFunctionInfo->arity}), {$framelessFunctionInfo->arity} },\n"; } $code .= "\t{ 0 },\n"; $code .= "};\n"; @@ -1362,10 +1370,10 @@ public function getFunctionEntry(): string { $functionEntryCode = null; if (!empty($this->framelessFunctionInfos)) { - if ($this->isMethod()) { - throw new Exception('Frameless methods are not supported yet'); + if ($this->isMethod() && !($this->flags & Modifiers::STATIC)) { + throw new Exception('Frameless methods must be static'); } - if ($this->name->getNamespace()) { + if (!$this->isMethod() && $this->name->getNamespace()) { throw new Exception('Namespaced direct calls to frameless functions are not supported yet'); } if ($this->alias) { diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index e3f87ee1e1636..55d86896468e8 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -1169,6 +1169,30 @@ static ZEND_METHOD(_ZendTestClass, takesUnionType) RETURN_NULL(); } +static ZEND_METHOD(_ZendTestClass, framelessStaticMethod) +{ + zend_long lhs, rhs; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_LONG(lhs) + Z_PARAM_LONG(rhs) + ZEND_PARSE_PARAMETERS_END(); + + RETURN_LONG(lhs + rhs); +} + +ZEND_FRAMELESS_FUNCTION(_ZendTestClass_framelessStaticMethod, 2) +{ + zend_long lhs, rhs; + + Z_FLF_PARAM_LONG(1, lhs); + Z_FLF_PARAM_LONG(2, rhs); + + ZVAL_LONG(return_value, lhs + rhs); + +flf_clean:; +} + // Returns a newly allocated DNF type `Iterator|(Traversable&Countable)`. // // We need to generate it "manually" because gen_stubs.php does not support codegen for DNF types ATM. diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php index 59cb9661e4e43..bddffaa1accc5 100644 --- a/ext/zend_test/test.stub.php +++ b/ext/zend_test/test.stub.php @@ -69,6 +69,9 @@ public function returnsThrowable(): Throwable {} static public function variadicTest(string|Iterator ...$elements) : static {} public function takesUnionType(stdclass|Iterator $arg): void {} + + /** @frameless-function {"arity": 2} */ + public static function framelessStaticMethod(int $lhs, int $rhs): int {} } class _ZendTestMagicCall diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h index c558b58f65169..94e602a155810 100644 --- a/ext/zend_test/test_arginfo.h +++ b/ext/zend_test/test_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 3082e62e96d5f4383c98638513463c676a7c3a69 */ + * Stub hash: 5ea0e1aae76e9f10b6e5c17d4d55f51e353ebbf2 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_array_return, 0, 0, IS_ARRAY, 0) ZEND_END_ARG_INFO() @@ -203,6 +203,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class__ZendTestClass_takesUnionT ZEND_ARG_OBJ_TYPE_MASK(0, arg, stdclass|Iterator, 0, NULL) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class__ZendTestClass_framelessStaticMethod, 0, 2, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, lhs, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, rhs, IS_LONG, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class__ZendTestMagicCall___call, 0, 2, IS_MIXED, 0) ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, args, IS_ARRAY, 0) @@ -254,6 +259,15 @@ ZEND_END_ARG_INFO() #define arginfo_class_ZendTestNS2_ZendSubNS_Foo_method arginfo_zend_test_void_return + +#if (PHP_VERSION_ID >= 80400) +ZEND_FRAMELESS_FUNCTION(_ZendTestClass_framelessStaticMethod, 2); +static const zend_frameless_function_info frameless_function_infos__ZendTestClass_framelessStaticMethod[] = { + { ZEND_FRAMELESS_FUNCTION_NAME(_ZendTestClass_framelessStaticMethod, 2), 2 }, + { 0 }, +}; +#endif + static ZEND_FUNCTION(zend_test_array_return); static ZEND_FUNCTION(zend_test_nullable_array_return); static ZEND_FUNCTION(zend_test_void_return); @@ -313,6 +327,7 @@ static ZEND_METHOD(_ZendTestClass, returnsStatic); static ZEND_METHOD(_ZendTestClass, returnsThrowable); static ZEND_METHOD(_ZendTestClass, variadicTest); static ZEND_METHOD(_ZendTestClass, takesUnionType); +static ZEND_METHOD(_ZendTestClass, framelessStaticMethod); static ZEND_METHOD(_ZendTestMagicCall, __call); static ZEND_METHOD(_ZendTestMagicCallForward, __call); static ZEND_METHOD(_ZendTestChildClass, returnsThrowable); @@ -458,6 +473,11 @@ static const zend_function_entry class__ZendTestClass_methods[] = { ZEND_ME(_ZendTestClass, returnsThrowable, arginfo_class__ZendTestClass_returnsThrowable, ZEND_ACC_PUBLIC) ZEND_ME(_ZendTestClass, variadicTest, arginfo_class__ZendTestClass_variadicTest, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) ZEND_ME(_ZendTestClass, takesUnionType, arginfo_class__ZendTestClass_takesUnionType, ZEND_ACC_PUBLIC) +#if (PHP_VERSION_ID >= 80400) + ZEND_RAW_FENTRY("framelessStaticMethod", zim__ZendTestClass_framelessStaticMethod, arginfo_class__ZendTestClass_framelessStaticMethod, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, frameless_function_infos__ZendTestClass_framelessStaticMethod, NULL) +#else + ZEND_RAW_FENTRY("framelessStaticMethod", zim__ZendTestClass_framelessStaticMethod, arginfo_class__ZendTestClass_framelessStaticMethod, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) +#endif ZEND_FE_END }; diff --git a/ext/zend_test/tests/frameless_static_methods.phpt b/ext/zend_test/tests/frameless_static_methods.phpt new file mode 100644 index 0000000000000..39a1bbbf68fd5 --- /dev/null +++ b/ext/zend_test/tests/frameless_static_methods.phpt @@ -0,0 +1,12 @@ +--TEST-- +Frameless static methods +--EXTENSIONS-- +zend_test +--FILE-- +<?php + +var_dump(_ZendTestClass::framelessStaticMethod(42, 69)); + +?> +--EXPECT-- +int(111) From ba9092a84a5858c3895fc445adee5c222aa45816 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo <ilija.tovilo@me.com> Date: Wed, 27 Nov 2024 16:00:56 +0100 Subject: [PATCH 2/2] Compile static frameless calls before emitting opcodes Co-authored-by: Dmitry Stogov <dmitry@zend.com> --- Zend/zend_compile.c | 80 ++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 44 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 1a402fd43c846..7eadef4644c41 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -59,13 +59,6 @@ #define FC(member) (CG(file_context).member) -#define ZEND_OP1_LITERAL(opline) (op_array)->literals[(opline)->op1.constant] -#define ZEND_OP2_LITERAL(opline) (op_array)->literals[(opline)->op2.constant] -#define literal_dtor(zv) do { \ - zval_ptr_dtor_nogc(zv); \ - ZVAL_NULL(zv); \ - } while (0) - typedef struct _zend_loop_var { uint8_t opcode; uint8_t var_type; @@ -5343,29 +5336,11 @@ static void zend_compile_static_call(znode *result, zend_ast *ast, uint32_t type } } - uint32_t init_opnum = get_next_op_number(); - opline = get_next_op(); - opline->opcode = ZEND_INIT_STATIC_METHOD_CALL; - - zend_set_class_name_op1(opline, &class_node); - - if (method_node.op_type == IS_CONST) { - opline->op2_type = IS_CONST; - opline->op2.constant = zend_add_func_name_literal( - Z_STR(method_node.u.constant)); - opline->result.num = zend_alloc_cache_slots(2); - } else { - if (opline->op1_type == IS_CONST) { - opline->result.num = zend_alloc_cache_slot(); - } - SET_NODE(opline->op2, &method_node); - } - /* Check if we already know which method we're calling */ - if (opline->op2_type == IS_CONST) { + if (method_node.op_type == IS_CONST) { zend_class_entry *ce = NULL; - if (opline->op1_type == IS_CONST) { - zend_string *lcname = Z_STR_P(CT_CONSTANT(opline->op1) + 1); + if (class_node.op_type == IS_CONST) { + zend_string *lcname = zend_string_tolower(Z_STR(class_node.u.constant)); ce = zend_hash_find_ptr(CG(class_table), lcname); if (ce) { if (zend_compile_ignore_class(ce, CG(active_op_array)->filename)) { @@ -5375,31 +5350,48 @@ static void zend_compile_static_call(znode *result, zend_ast *ast, uint32_t type && zend_string_equals_ci(CG(active_class_entry)->name, lcname)) { ce = CG(active_class_entry); } - } else if (opline->op1_type == IS_UNUSED - && (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF + zend_string_release(lcname); + } else if (class_node.op_type == IS_UNUSED + && (class_node.u.op.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF && zend_is_scope_known()) { ce = CG(active_class_entry); } if (ce) { - zend_string *lcname = Z_STR_P(CT_CONSTANT(opline->op2) + 1); + zend_string *lcname = zend_string_tolower(Z_STR(method_node.u.constant)); fbc = zend_get_compatible_func_or_null(ce, lcname); + zend_string_release(lcname); + + if (fbc + && !(CG(compiler_options) & ZEND_COMPILE_NO_BUILTINS) + && (fbc->type == ZEND_INTERNAL_FUNCTION) + && zend_ast_is_list(args_ast) + && !zend_args_contain_unpack_or_named(zend_ast_get_list(args_ast))) { + if (zend_compile_frameless_icall(result, zend_ast_get_list(args_ast), fbc, type) != (uint32_t)-1) { + zval_ptr_dtor(&method_node.u.constant); + if (class_node.op_type == IS_CONST) { + zval_ptr_dtor(&class_node.u.constant); + } + return; + } + } } } - if (!(CG(compiler_options) & ZEND_COMPILE_NO_BUILTINS) - && fbc - && (fbc->type == ZEND_INTERNAL_FUNCTION) - && zend_ast_is_list(args_ast) - && !zend_args_contain_unpack_or_named(zend_ast_get_list(args_ast))) { - if (zend_compile_frameless_icall(result, zend_ast_get_list(args_ast), fbc, type) != (uint32_t)-1) { - /* Update opline in case it got invalidated. */ - zend_op_array *op_array = CG(active_op_array); - opline = &op_array->opcodes[init_opnum]; - literal_dtor(&ZEND_OP1_LITERAL(opline)); - literal_dtor(&ZEND_OP2_LITERAL(opline)); - MAKE_NOP(opline); - return; + opline = get_next_op(); + opline->opcode = ZEND_INIT_STATIC_METHOD_CALL; + + zend_set_class_name_op1(opline, &class_node); + + if (method_node.op_type == IS_CONST) { + opline->op2_type = IS_CONST; + opline->op2.constant = zend_add_func_name_literal( + Z_STR(method_node.u.constant)); + opline->result.num = zend_alloc_cache_slots(2); + } else { + if (opline->op1_type == IS_CONST) { + opline->result.num = zend_alloc_cache_slot(); } + SET_NODE(opline->op2, &method_node); } zend_compile_call_common(result, args_ast, fbc, zend_ast_get_lineno(method_ast));