diff --git a/NEWS b/NEWS index 4fbc7e89eb11..adeaf98c29bb 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,11 @@ PHP NEWS - Core: . Added first-class callable cache to share instances for the duration of the request. (ilutov) + . Closures declared in constant expressions (anonymous closures and + first-class callables) can now be serialized as references to their + declaration site. Added Closure::fromConstExpr(), + ReflectionFunction::getConstExprId() and getConstExprClass(). + (nicolas-grekas) . It is now possible to use reference assign on WeakMap without the key needing to be present beforehand. (ndossche) . Added `clamp()`. (kylekatarnls, thinkverse) diff --git a/UPGRADING b/UPGRADING index 2ec6ce92aa52..8429629cbf11 100644 --- a/UPGRADING +++ b/UPGRADING @@ -181,6 +181,22 @@ PHP 8.6 UPGRADE NOTES needing to be present beforehand. . It is now possible to define the `__debugInfo()` magic method on enums. RFC: https://wiki.php.net/rfc/debugable-enums + . Closures declared in constant expressions of a class member (anonymous + closures and first-class callables, of any visibility, in attribute + arguments and parameter default values) can now be serialized and + unserialized. The payload contains no code: it is a reference to the + declaration site (class name plus an element-scoped "@" id; + the key is the closure's rank within the declaring element for anonymous + closures, checked against the closure's line relative to the class, and + the name of the referenced callable, e.g. "$prop@Foo::bar", for + first-class callables), resolved against the loaded class. The payload is + a tagged union ([, [, ]]) so the format can + grow further reference kinds. Closures created at runtime, bound to + an object, rebound to another scope, or declared in class constant + values, property defaults or outside a class still refuse to serialize. + The unserialize() allowed_classes filter applies to Closure as to any + other class. Added Closure::fromConstExpr($class, $id) plus + ReflectionFunction::getConstExprId()/getConstExprClass() for exporters. - Fileinfo: . finfo_file() now works with remote streams. diff --git a/Zend/tests/closures/closure_const_expr/serialization_allowed_classes.phpt b/Zend/tests/closures/closure_const_expr/serialization_allowed_classes.phpt new file mode 100644 index 000000000000..a3300a8a3d1a --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_allowed_classes.phpt @@ -0,0 +1,47 @@ +--TEST-- +Serializable closures are gated by the unserialize() allowed_classes filter +--FILE-- +getAttributes(); +$payloads = [ + 'anonymous' => serialize($attrs[0]->getArguments()[0]), + 'fcc site' => serialize($attrs[1]->getArguments()[0]), +]; + +// The recommended safe-unserialize practice (allowed_classes => false) blocks +// every Closure payload: it becomes __PHP_Incomplete_Class and __unserialize() +// is never invoked, exactly like any other object-injection gadget. +foreach ($payloads as $name => $payload) { + $r = unserialize($payload, ['allowed_classes' => false]); + var_dump($name, $r instanceof __PHP_Incomplete_Class); +} + +// A list that does not contain Closure also blocks it. +$r = unserialize($payloads['fcc site'], ['allowed_classes' => ['stdClass']]); +var_dump($r instanceof __PHP_Incomplete_Class); + +// Closure must be explicitly opted in. +$r = unserialize($payloads['fcc site'], ['allowed_classes' => ['Closure']]); +var_dump($r instanceof Closure, $r('test')); + +?> +--EXPECT-- +string(9) "anonymous" +bool(true) +string(8) "fcc site" +bool(true) +bool(true) +bool(true) +int(4) diff --git a/Zend/tests/closures/closure_const_expr/serialization_basic.phpt b/Zend/tests/closures/closure_const_expr/serialization_basic.phpt new file mode 100644 index 000000000000..962d537ec459 --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_basic.phpt @@ -0,0 +1,84 @@ +--TEST-- +Closures in constant expressions are serializable as declaration-site references +--FILE-- + (new ReflectionClass(Demo::class))->getAttributes()[0]->getArguments()[0], + 'const' => (new ReflectionClassConstant(Demo::class, 'FOO'))->getAttributes()[0]->getArguments()[0], + 'prop-1' => (new ReflectionProperty(Demo::class, 'name'))->getAttributes()[0]->getArguments()['cb'][0], + 'prop-2' => (new ReflectionProperty(Demo::class, 'name'))->getAttributes()[0]->getArguments()['cb'][1], + 'method' => (new ReflectionMethod(Demo::class, 'm'))->getAttributes()[0]->getArguments()[0], + 'param' => (new ReflectionParameter([Demo::class, 'm'], 'x'))->getAttributes()[0]->getArguments()[0], + 'case' => (new ReflectionClassConstant(E::class, 'X'))->getAttributes()[0]->getArguments()[0], +]; + +foreach ($closures as $expected => $closure) { + $r = new ReflectionFunction($closure); + $id = $r->getConstExprId(); + $scope = $r->getClosureScopeClass()->name; + + $unserialized = unserialize(serialize($closure)); + $recreated = Closure::fromConstExpr($scope, $id); + + var_dump($expected === $closure() && $expected === $unserialized() && $expected === $recreated()); +} + +// Ids are assigned in canonical walk order: class attributes first, then +// constants, then properties, then methods (including parameters). +$ids = array_map( + static fn ($c) => (new ReflectionFunction($c))->getConstExprId(), + $closures +); +var_dump($ids); + +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +array(7) { + ["class"]=> + string(2) "@0" + ["const"]=> + string(5) "FOO@0" + ["prop-1"]=> + string(7) "$name@0" + ["prop-2"]=> + string(7) "$name@1" + ["method"]=> + string(5) "m()@0" + ["param"]=> + string(5) "m()@1" + ["case"]=> + string(3) "X@0" +} diff --git a/Zend/tests/closures/closure_const_expr/serialization_graph.phpt b/Zend/tests/closures/closure_const_expr/serialization_graph.phpt new file mode 100644 index 000000000000..d57e5ca567c0 --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_graph.phpt @@ -0,0 +1,39 @@ +--TEST-- +Const-expr closures inside serialized object graphs +--FILE-- +c)(), "\n"; + } +} + +$h = new Holder(); +$h->c = (new ReflectionProperty(Demo::class, 'p'))->getAttributes()[0]->getArguments()[0]; + +$u = unserialize(serialize([$h, $h->c, [$h->c]])); + +echo "after: ", ($u[1])(), "\n"; +// Shared instances are preserved within the graph. +var_dump($u[0]->c === $u[1], $u[1] === $u[2][0]); + +?> +--EXPECT-- +wakeup sees: ok +after: ok +bool(true) +bool(true) diff --git a/Zend/tests/closures/closure_const_expr/serialization_inheritance.phpt b/Zend/tests/closures/closure_const_expr/serialization_inheritance.phpt new file mode 100644 index 000000000000..ff3b3ed6683c --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_inheritance.phpt @@ -0,0 +1,50 @@ +--TEST-- +Serialization of const-expr closures with inheritance and traits +--FILE-- +getAttributes()[0]->getArguments()[0]; +var_dump((new ReflectionFunction($c))->getClosureScopeClass()->name); +$payload = serialize($c); +var_dump(str_contains($payload, '"Base"')); +var_dump(unserialize($payload)()); + +// Attribute on a parameter of a trait method: the copied method is scoped +// to the using class and the reference resolves through it. +$c = (new ReflectionParameter([UsesTrait::class, 'm'], 'x'))->getAttributes()[0]->getArguments()[0]; +var_dump((new ReflectionFunction($c))->getClosureScopeClass()->name); +$u = unserialize(serialize($c)); +var_dump($u()); + +?> +--EXPECT-- +string(4) "Base" +bool(true) +string(4) "base" +string(9) "UsesTrait" +string(5) "trait" diff --git a/Zend/tests/closures/closure_const_expr/serialization_invalid_payload.phpt b/Zend/tests/closures/closure_const_expr/serialization_invalid_payload.phpt new file mode 100644 index 000000000000..3d43273141a5 --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_invalid_payload.phpt @@ -0,0 +1,78 @@ +--TEST-- +Unserializing invalid or stale Closure declaration-site references +--FILE-- +getAttributes()[0]->getArguments()[0]; +$id = (new ReflectionFunction($closure))->getConstExprId(); +// The stored line is relative to the declaring class. +$rel = (new ReflectionFunction($closure))->getStartLine() - (new ReflectionClass(Demo::class))->getStartLine(); + +// Serialize an array and rebrand it as a Closure object payload, so we can +// feed __unserialize() arbitrary tagged-union shapes. +$obj = static fn (array $data): string => 'O:7:"Closure":' . substr(serialize($data), 2); + +// Tagged-union payload: [ [], ["const-expr", [class, id, line?]] ]. +$mk = static function (string $class, string $id, ?int $line) use ($obj): string { + $payload = $line === null ? [$class, $id] : [$class, $id, $line]; + return $obj([[], ['const-expr', $payload]]); +}; + +// Sanity check: a valid reference works. +var_dump(unserialize($mk('Demo', $id, $rel))()); + +$payloads = [ + 'empty data' => $obj([]), + 'one element' => $obj([[]]), + 'unknown tag' => $obj([[], ['whatever', ['Demo', $id, $rel]]]), + 'tag not list' => $obj([[], 'const-expr']), + 'wrong id type' => $obj([[], ['const-expr', ['Demo', 0, $rel]]]), + 'unknown class' => $mk('NoSuchClass', $id, $rel), + 'internal class' => $mk('stdClass', $id, $rel), + 'unknown site' => $mk('Demo', '$nope@0', $rel), + 'malformed id' => $mk('Demo', 'no-at-sign', $rel), + 'anon missing line' => $mk('Demo', $id, null), + 'stale line' => $mk('Demo', $id, $rel + 1), +]; + +foreach ($payloads as $name => $payload) { + try { + unserialize($payload); + echo "$name: unserialized!?\n"; + } catch (Exception $e) { + echo "$name: {$e->getMessage()}\n"; + } +} + +// __unserialize() cannot be used to reinitialize a live closure. +try { + $closure->__unserialize([[], ['const-expr', ['Demo', $id, $rel]]]); +} catch (Exception $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +string(2) "ok" +empty data: Invalid serialization data for Closure object +one element: Invalid serialization data for Closure object +unknown tag: Invalid serialization data for Closure object +tag not list: Invalid serialization data for Closure object +wrong id type: Invalid serialization data for Closure object +unknown class: Invalid serialization data for Closure object (cannot load class "NoSuchClass") +internal class: Invalid serialization data for Closure object (cannot load class "stdClass") +unknown site: Invalid serialization data for Closure object (constant-expression closure "$nope@0" of class Demo not found) +malformed id: Invalid serialization data for Closure object (constant-expression closure "no-at-sign" of class Demo not found) +anon missing line: Invalid serialization data for Closure object +stale line: Invalid serialization data for Closure object (constant-expression closure "$p@0" of class Demo not found) +Cannot unserialize an already initialized Closure diff --git a/Zend/tests/closures/closure_const_expr/serialization_misc.phpt b/Zend/tests/closures/closure_const_expr/serialization_misc.phpt new file mode 100644 index 000000000000..60818108b069 --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_misc.phpt @@ -0,0 +1,61 @@ +--TEST-- +Misc behaviors of serializable const-expr closures: static vars, identity, scope binding, fromConstExpr() errors +--FILE-- +getAttributes(); +$counter = $attributes[0]->getArguments()[0]; +$scoped = $attributes[1]->getArguments()[0]; + +// A reference does not carry the state of static variables: unserializing +// produces the closure as if it was freshly evaluated. +var_dump($counter(), $counter()); +$u = unserialize(serialize($counter)); +var_dump($u()); + +// Unserialized closures are new instances. +var_dump($u === $counter); + +// Scope binding is restored: private members of the class are accessible. +var_dump(unserialize(serialize($scoped))()); + +// fromConstExpr() error cases +try { + Closure::fromConstExpr('NoSuchClass', 0); +} catch (Error $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +try { + Closure::fromConstExpr('stdClass', 0); +} catch (ValueError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +try { + Closure::fromConstExpr('Demo', 999); +} catch (ValueError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +int(1) +int(2) +int(1) +bool(false) +string(6) "secret" +Error: Class "NoSuchClass" not found +ValueError: Closure::fromConstExpr(): Argument #2 ($id) does not refer to a constant-expression closure of class stdClass +ValueError: Closure::fromConstExpr(): Argument #2 ($id) does not refer to a constant-expression closure of class Demo diff --git a/Zend/tests/closures/closure_const_expr/serialization_named.phpt b/Zend/tests/closures/closure_const_expr/serialization_named.phpt new file mode 100644 index 000000000000..3eefb0a1fa21 --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_named.phpt @@ -0,0 +1,115 @@ +--TEST-- +First-class callable references serialize as name-keyed declaration sites +--FILE-- +getAttributes(); +foreach ($attrs as $i => $attr) { + $closure = $attr->getArguments()[0]; + $payload = serialize($closure); + $u = unserialize($payload); + $args = $i === 4 ? ['abcd'] : []; + // The id is "@Called::method" / "@function", the payload has + // no line field, and the recreated closure behaves identically + // (including static:: binding) and re-serializes byte-identically. + var_dump( + (new ReflectionFunction($closure))->getConstExprId() + . ' | ' . var_export($u(...$args) === $closure(...$args), true) + . ' | ' . var_export(serialize($u) === $payload, true) + ); +} +echo serialize($attrs[0]->getArguments()[0]), "\n"; + +// parent:: and self:: forms resolve their distinct static:: bindings. +var_dump(unserialize(serialize($attrs[1]->getArguments()[0]))()); +var_dump(unserialize(serialize($attrs[2]->getArguments()[0]))()); + +// No rank is consumed by first-class callable references. +$qattrs = (new ReflectionProperty(Demo::class, 'q'))->getAttributes(); +var_dump((new ReflectionFunction($qattrs[1]->getArguments()[0]))->getConstExprId()); + +// Runtime-created named closures are not declaration sites and refuse, +// even when an identical reference exists in an attribute. +foreach ([strlen(...), Validators::check(...), Closure::fromCallable('strlen')] as $closure) { + try { + serialize($closure); + echo "serialized!?\n"; + } catch (Exception $e) { + echo $e->getMessage(), "\n"; + } +} + +// The name is the address, the declaration is the guard: keys that no +// element declares do not resolve, whatever they name. +foreach (['$p@system', '$p@Demo::nope', '$q@Validators::check', '$p@0'] as $id) { + try { + Closure::fromConstExpr('Demo', $id); + echo "resolved!?\n"; + } catch (ValueError $e) { + echo $id, ": ", $e->getMessage(), "\n"; + } +} +try { + unserialize('O:7:"Closure":2:' . substr(serialize([[], ['const-expr', ['Demo', '$p@system']]]), 4)); +} catch (Exception $e) { + echo $e->getMessage(), "\n"; +} + +} +?> +--EXPECT-- +string(27) "$p@Demo::priv | true | true" +string(27) "$p@Demo::prot | true | true" +string(27) "$p@Base::prot | true | true" +string(34) "$p@Validators::check | true | true" +string(23) "$p@strlen | true | true" +string(27) "$p@App\helper | true | true" +O:7:"Closure":2:{i:0;a:0:{}i:1;a:2:{i:0;s:10:"const-expr";i:1;a:2:{i:0;s:4:"Demo";i:1;s:13:"$p@Demo::priv";}}} +string(9) "prot-Demo" +string(9) "prot-Base" +string(4) "$q@0" +Serialization of 'Closure' is not allowed +Serialization of 'Closure' is not allowed +Serialization of 'Closure' is not allowed +$p@system: Closure::fromConstExpr(): Argument #2 ($id) does not refer to a constant-expression closure of class Demo +$p@Demo::nope: Closure::fromConstExpr(): Argument #2 ($id) does not refer to a constant-expression closure of class Demo +$q@Validators::check: Closure::fromConstExpr(): Argument #2 ($id) does not refer to a constant-expression closure of class Demo +$p@0: Closure::fromConstExpr(): Argument #2 ($id) does not refer to a constant-expression closure of class Demo +Invalid serialization data for Closure object (constant-expression closure "$p@system" of class Demo not found) diff --git a/Zend/tests/closures/closure_const_expr/serialization_nested.phpt b/Zend/tests/closures/closure_const_expr/serialization_nested.phpt new file mode 100644 index 000000000000..1cf3c8615311 --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_nested.phpt @@ -0,0 +1,66 @@ +--TEST-- +Serialization of const-expr closures in parameter defaults, hooks and nested functions +--FILE-- + $this->v; + } + + public function withDefault($cb = static function () { return 'default'; }) { + return $cb; + } + + public function makeClosure() { + return #[A(static function () { return 'inner'; })] static function () {}; + } +} + +class Nested { + #[A(static function ( + #[A(static function () { return 'deep'; })] $x = null, + ) { return 'outer'; })] + public int $p = 0; +} + +$roundtrip = static function (Closure $c) { + $u = unserialize(serialize($c)); + var_dump($u(), (new ReflectionFunction($c))->getConstExprId()); +}; + +// Attribute on a property hook +$hook = (new ReflectionProperty(Demo::class, 'v'))->getHook(PropertyHookType::Get); +$roundtrip($hook->getAttributes()[0]->getArguments()[0]); + +// Parameter default value +$roundtrip((new Demo)->withDefault()); + +// Attribute on a runtime closure declared in a method body +$runtime = (new Demo)->makeClosure(); +$roundtrip((new ReflectionFunction($runtime))->getAttributes()[0]->getArguments()[0]); + +// Const-expr closure nested in the parameter attribute of another one +$outer = (new ReflectionProperty(Nested::class, 'p'))->getAttributes()[0]->getArguments()[0]; +$inner = (new ReflectionFunction($outer))->getParameters()[0]->getAttributes()[0]->getArguments()[0]; +$roundtrip($outer); +$roundtrip($inner); + +?> +--EXPECT-- +string(8) "get-hook" +string(11) "$v::get()@0" +string(7) "default" +string(15) "withDefault()@0" +string(5) "inner" +string(15) "makeClosure()@0" +string(5) "outer" +string(4) "$p@0" +string(4) "deep" +string(4) "$p@1" diff --git a/Zend/tests/closures/closure_const_expr/serialization_not_allowed.phpt b/Zend/tests/closures/closure_const_expr/serialization_not_allowed.phpt new file mode 100644 index 000000000000..fbcef956cf1c --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_not_allowed.phpt @@ -0,0 +1,86 @@ +--TEST-- +Closures that are not addressable by a declaration-site reference still refuse to serialize +--FILE-- +getAttributes()[0]->getArguments()[0]; + +$cases = [ + 'runtime closure' => function () {}, + 'static runtime closure' => static function () {}, + 'arrow function' => fn () => 1, + 'runtime function callable' => strlen(...), + 'runtime method callable' => Demo::privateFcc(...), + 'bound method callable' => (new Demo)->boundMethod(...), + 'private method callable' => Demo::privateFcc(), + '__callStatic trampoline' => Demo::someUndefinedMethod(...), + 'class constant value' => Demo::BAD, + 'property default' => (new Demo)->bad, + 'rebound scope' => Closure::bind($attrClosure, null, A::class), + 'free function attribute' => (new ReflectionFunction('freeFunction'))->getAttributes()[0]->getArguments()[0], + 'anonymous class attribute' => (new ReflectionClass($anon))->getAttributes()[0]->getArguments()[0], +]; + +foreach ($cases as $name => $closure) { + try { + serialize($closure); + echo "$name: serialized!?\n"; + } catch (Exception $e) { + echo "$name: {$e->getMessage()}\n"; + } + var_dump((new ReflectionFunction($closure))->getConstExprId()); +} + +?> +--EXPECT-- +runtime closure: Serialization of 'Closure' is not allowed +NULL +static runtime closure: Serialization of 'Closure' is not allowed +NULL +arrow function: Serialization of 'Closure' is not allowed +NULL +runtime function callable: Serialization of 'Closure' is not allowed +NULL +runtime method callable: Serialization of 'Closure' is not allowed +NULL +bound method callable: Serialization of 'Closure' is not allowed +NULL +private method callable: Serialization of 'Closure' is not allowed +NULL +__callStatic trampoline: Serialization of 'Closure' is not allowed +NULL +class constant value: Serialization of 'Closure' is not allowed +NULL +property default: Serialization of 'Closure' is not allowed +NULL +rebound scope: Serialization of 'Closure' is not allowed +NULL +free function attribute: Serialization of 'Closure' is not allowed +NULL +anonymous class attribute: Serialization of 'Closure' is not allowed +NULL diff --git a/Zend/tests/closures/closure_const_expr/serialization_tagged_format.phpt b/Zend/tests/closures/closure_const_expr/serialization_tagged_format.phpt new file mode 100644 index 000000000000..0e7e178c30b1 --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_tagged_format.phpt @@ -0,0 +1,82 @@ +--TEST-- +Closure serialization payload is a tagged union with a class-relative line +--FILE-- +getAttributes(); +$anon = $attrs[0]->getArguments()[0]; +$fcc = $attrs[1]->getArguments()[0]; + +// The payload is [ , [ , ] ]. The +// properties slot is empty, the tag is "const-expr". An anonymous closure +// carries [class, id, line]; a first-class callable carries just [class, id]. +var_dump($anon->__serialize()); +var_dump($fcc->__serialize()); + +// The line is relative to the declaring class, so it equals the offset of the +// closure from the class declaration, not an absolute line number. +$relLine = $anon->__serialize()[1][1][2]; +$expected = (new ReflectionFunction($anon))->getStartLine() - (new ReflectionClass(Demo::class))->getStartLine(); +var_dump($relLine === $expected); + +// Because the line is class-relative, the same class declared at a different +// offset in the file (here, five blank lines earlier) produces the identical +// reference: an edit above the class does not invalidate stored payloads. +eval("\n\n\n\n\n" . 'class Shifted { + #[A(static function () { return "anon"; })] + public int $p = 0; +}'); +$shifted = (new ReflectionProperty('Shifted', 'p'))->getAttributes()[0]->getArguments()[0]; +var_dump($shifted->__serialize()[1][1][2] === $relLine); + +?> +--EXPECT-- +array(2) { + [0]=> + array(0) { + } + [1]=> + array(2) { + [0]=> + string(10) "const-expr" + [1]=> + array(3) { + [0]=> + string(4) "Demo" + [1]=> + string(4) "$p@0" + [2]=> + int(1) + } + } +} +array(2) { + [0]=> + array(0) { + } + [1]=> + array(2) { + [0]=> + string(10) "const-expr" + [1]=> + array(2) { + [0]=> + string(4) "Demo" + [1]=> + string(9) "$p@strlen" + } + } +} +bool(true) +bool(true) diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index a7e26711cd17..4788a7b69b3d 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -1244,6 +1244,12 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner( zend_create_fake_closure(result, fptr, fptr->common.scope, called_scope, NULL); + if (scope) { + /* Remember the declaration site, so that the closure can be + * serialized as a reference to it. */ + zend_closure_mark_as_constexpr_fcc(result, scope); + } + return SUCCESS; } case ZEND_AST_OP_ARRAY: diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c index 840d2dbe32e1..0fa2adf8103e 100644 --- a/Zend/zend_closures.c +++ b/Zend/zend_closures.c @@ -19,8 +19,11 @@ #include "zend.h" #include "zend_API.h" +#include "zend_ast.h" +#include "zend_attributes.h" #include "zend_closures.h" #include "zend_exceptions.h" +#include "zend_execute.h" #include "zend_interfaces.h" #include "zend_objects.h" #include "zend_objects_API.h" @@ -33,6 +36,10 @@ typedef struct _zend_closure { zval this_ptr; zend_class_entry *called_scope; zif_handler orig_internal_handler; + /* The class whose constant expressions declared this closure, when it was + * created by evaluating a first-class callable in one of them + * (ZEND_ACC2_CONSTEXPR_FCC). */ + zend_class_entry *constexpr_site; } zend_closure; /* non-static since it needs to be referenced */ @@ -40,6 +47,8 @@ ZEND_API zend_class_entry *zend_ce_closure; static zend_object_handlers closure_handlers; static zend_result zend_closure_get_closure(zend_object *obj, zend_class_entry **ce_ptr, zend_function **fptr_ptr, zend_object **obj_ptr, bool check_only); +static ZEND_COLD ZEND_NAMED_FUNCTION(zend_closure_uninitialized_handler); +static void zend_closure_init_ex(zend_closure *closure, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr, bool is_fake); ZEND_METHOD(Closure, __invoke) /* {{{ */ { @@ -448,6 +457,885 @@ ZEND_METHOD(Closure, getCurrent) RETURN_OBJ_COPY(obj); } +/* {{{ Closures declared in constant expressions + * + * Closures declared in constant expressions are static, capture no variables + * and are fully described by their compiled op_array. This makes them + * addressable by (class, id), where the id is the position of the closure in + * a canonical walk over all constant expression ASTs that are reachable from + * the class and that are not subject to in-place evaluation: attribute + * arguments and parameter default values, including those of nested + * functions. Class constant values and property default values are evaluated + * (and their AST freed) in place, so closures declared there are not + * addressable this way. + * + * The walk visits candidates in a deterministic order that only depends on + * the compiled class, not on runtime evaluation state, so ids computed by one + * process can be resolved by another process running the same code. + */ + +typedef struct { + /* Rank of the next closure WITHIN the current reflection element; reset to + * 0 at each element boundary by zend_constexpr_closure_walk_class. A + * reference is (element site, local rank), not a single class-global rank, + * so adding/removing/reordering closures in one element does not renumber + * closures in other elements. */ + uint32_t next_id; + /* When by_id is true, search for target_id (a LOCAL rank among the + * anonymous closures of a single element; the caller has already + * positioned the walk on that element). When target_key is set, search + * for the first-class callable reference matching that "Called::method" + * / "function" key within the element. Otherwise search for the op_array + * of an anonymous closure identified by its opcodes, or for a first-class + * callable reference matching the fcc closure. */ + bool by_id; + uint32_t target_id; + const char *target_key; + size_t target_key_len; + const zend_op *opcodes; + const zend_closure *fcc; + /* The class being walked; used to resolve self/parent references. */ + zend_class_entry *site_class; + /* The reflection element currently being walked, kept as a cheap + * (kind, borrowed name, hook) descriptor rather than a built string, so no + * allocation happens for the elements the walk merely passes through. The + * "@" string is materialized once, from found_*, on a match. */ + uint8_t cur_kind; + zend_string *cur_name; + uint8_t cur_hook; + /* The found site: either a ZEND_AST_OP_ARRAY node, or a ZEND_AST_CALL / + * ZEND_AST_STATIC_CALL node whose argument list is a first-class callable + * placeholder. */ + zend_ast *found; + uint32_t found_id; + uint8_t found_kind; + zend_string *found_name; + uint8_t found_hook; +} zend_constexpr_closure_walk; + +/* Reflection-element kinds for the site descriptor. */ +#define ZEND_CEXPR_SITE_CLASS 0 +#define ZEND_CEXPR_SITE_CONST 1 +#define ZEND_CEXPR_SITE_PROP 2 +#define ZEND_CEXPR_SITE_HOOK 3 +#define ZEND_CEXPR_SITE_METHOD 4 + +static bool zend_constexpr_closure_walk_op_array(zend_constexpr_closure_walk *w, zend_op_array *op_array); + +/* Whether the named function resolved from a first-class callable site is the + * one the closure was created from. */ +static bool zend_closure_func_matches(const zend_closure *closure, const zend_function *resolved) +{ + if (resolved->type != closure->func.type) { + return false; + } + if (resolved->type == ZEND_USER_FUNCTION) { + return resolved->op_array.opcodes == closure->func.op_array.opcodes; + } + /* The handler of internal fake closures is wrapped, the original one is + * saved in orig_internal_handler. This also rejects __call/__callstatic + * trampolines, whose handler is not the resolved function. */ + return resolved->internal_function.handler == closure->orig_internal_handler; +} + +/* Whether the first-class callable declared by the given site resolves to the + * function the closure was created from. Resolution is done by name and + * pointer comparisons only: no autoloading, no user code, no exceptions. */ +static bool zend_constexpr_fcc_matches(const zend_constexpr_closure_walk *w, const zend_ast *ast) +{ + const zend_closure *closure = w->fcc; + const zend_function *resolved = NULL; + zend_string *lcname; + + if (!closure) { + return false; + } + + if (ast->kind == ZEND_AST_STATIC_CALL) { + const zend_ast *class_ast = ast->child[0]; + zend_class_entry *ref_ce; + + if (!closure->func.common.scope || !closure->called_scope) { + return false; + } + + switch (class_ast->attr >> ZEND_CONST_EXPR_NEW_FETCH_TYPE_SHIFT) { + case ZEND_FETCH_CLASS_SELF: + ref_ce = w->site_class; + break; + case ZEND_FETCH_CLASS_PARENT: + ref_ce = w->site_class->parent; + break; + default: + ref_ce = zend_string_equals_ci(zend_ast_get_str((zend_ast *) class_ast), closure->called_scope->name) + ? closure->called_scope : NULL; + break; + } + if (!ref_ce || ref_ce != closure->called_scope) { + return false; + } + + lcname = zend_string_tolower(zend_ast_get_str(ast->child[1])); + resolved = zend_hash_find_ptr(&ref_ce->function_table, lcname); + zend_string_release_ex(lcname, 0); + } else { + ZEND_ASSERT(ast->kind == ZEND_AST_CALL); + + if (closure->func.common.scope || closure->called_scope) { + return false; + } + + lcname = zend_string_tolower(zend_ast_get_str(ast->child[0])); + resolved = zend_hash_find_ptr(EG(function_table), lcname); + if (!resolved && ast->child[0]->attr != ZEND_NAME_FQ) { + const char *backslash = zend_memrchr(ZSTR_VAL(lcname), '\\', ZSTR_LEN(lcname)); + if (backslash) { + resolved = zend_hash_str_find_ptr(EG(function_table), + backslash + 1, ZSTR_LEN(lcname) - (backslash - ZSTR_VAL(lcname) + 1)); + } + } + zend_string_release_ex(lcname, 0); + } + + return resolved && zend_closure_func_matches(closure, resolved); +} + +/* Whether the first-class callable declared by the given site matches a + * "Called::method" / "function" key. Pure string comparisons: class + * references are compile-time resolved names ('self'/'parent' resolve + * against the walked class), so no class or function is ever loaded. */ +static bool zend_constexpr_fcc_key_matches(const zend_constexpr_closure_walk *w, const zend_ast *ast) +{ + const char *key = w->target_key; + size_t key_len = w->target_key_len; + const char *sep = NULL; + + for (size_t i = 0; i + 1 < key_len; i++) { + if (key[i] == ':' && key[i + 1] == ':') { + sep = key + i; + break; + } + } + + if (ast->kind == ZEND_AST_STATIC_CALL) { + const zend_ast *class_ast = ast->child[0]; + zend_string *method = zend_ast_get_str(ast->child[1]); + zend_string *ref_name; + + if (!sep) { + return false; + } + + switch (class_ast->attr >> ZEND_CONST_EXPR_NEW_FETCH_TYPE_SHIFT) { + case ZEND_FETCH_CLASS_SELF: + ref_name = w->site_class->name; + break; + case ZEND_FETCH_CLASS_PARENT: + ref_name = w->site_class->parent ? w->site_class->parent->name : NULL; + break; + default: + ref_name = zend_ast_get_str((zend_ast *) class_ast); + break; + } + + return ref_name + && ZSTR_LEN(ref_name) == (size_t) (sep - key) + && zend_binary_strcasecmp(ZSTR_VAL(ref_name), ZSTR_LEN(ref_name), key, sep - key) == 0 + && ZSTR_LEN(method) == key_len - (sep - key) - 2 + && zend_binary_strcasecmp(ZSTR_VAL(method), ZSTR_LEN(method), sep + 2, ZSTR_LEN(method)) == 0; + } + + ZEND_ASSERT(ast->kind == ZEND_AST_CALL); + + if (sep) { + return false; + } + + zend_string *fname = zend_ast_get_str(ast->child[0]); + if (ZSTR_LEN(fname) == key_len + && zend_binary_strcasecmp(ZSTR_VAL(fname), ZSTR_LEN(fname), key, key_len) == 0) { + return true; + } + /* Non-fully-qualified names in namespaced code may resolve to the global + * function, whose name is what serialization emits as the key. */ + if (ast->child[0]->attr != ZEND_NAME_FQ) { + const char *backslash = zend_memrchr(ZSTR_VAL(fname), '\\', ZSTR_LEN(fname)); + if (backslash) { + size_t tail_len = ZSTR_LEN(fname) - (backslash + 1 - ZSTR_VAL(fname)); + return tail_len == key_len + && zend_binary_strcasecmp(backslash + 1, tail_len, key, key_len) == 0; + } + } + return false; +} + +static bool zend_constexpr_closure_visit_op_array(zend_constexpr_closure_walk *w, zend_ast *ast) +{ + zend_op_array *op_array = zend_ast_get_op_array(ast)->op_array; + uint32_t id = w->next_id++; + + if (w->by_id ? w->target_id == id : (w->opcodes && w->opcodes == op_array->opcodes)) { + w->found = ast; + w->found_id = id; + w->found_kind = w->cur_kind; + w->found_name = w->cur_name; + w->found_hook = w->cur_hook; + return true; + } + + /* The closure may itself declare closures in its attributes or in its + * parameter default values. */ + return zend_constexpr_closure_walk_op_array(w, op_array); +} + +static bool zend_constexpr_closure_walk_ast(zend_constexpr_closure_walk *w, zend_ast *ast) +{ + if (!ast) { + return false; + } + + switch (ast->kind) { + case ZEND_AST_OP_ARRAY: + return zend_constexpr_closure_visit_op_array(w, ast); + case ZEND_AST_ZVAL: + case ZEND_AST_CONSTANT: + return false; + case ZEND_AST_CALL: + case ZEND_AST_STATIC_CALL: { + zend_ast *args = ast->kind == ZEND_AST_CALL ? ast->child[1] : ast->child[2]; + + /* In constant expressions, calls only exist in their first-class + * callable form: each one is a closure declaration site, keyed by + * the name of its target. It does not consume a rank: adding or + * removing a callable reference never renumbers the anonymous + * closures of the element. */ + if (args && args->kind == ZEND_AST_CALLABLE_CONVERT) { + if (w->target_key + ? zend_constexpr_fcc_key_matches(w, ast) + : (!w->by_id && zend_constexpr_fcc_matches(w, ast))) { + w->found = ast; + w->found_kind = w->cur_kind; + w->found_name = w->cur_name; + w->found_hook = w->cur_hook; + return true; + } + return false; + } + break; + } + case ZEND_AST_CALLABLE_CONVERT: + /* Visited through its enclosing call node. */ + return false; + default: + break; + } + + if (zend_ast_is_list(ast)) { + zend_ast_list *list = zend_ast_get_list(ast); + for (uint32_t i = 0; i < list->children; i++) { + if (zend_constexpr_closure_walk_ast(w, list->child[i])) { + return true; + } + } + return false; + } + + if (zend_ast_is_decl(ast)) { + /* Closure declarations in constant expressions are compiled to + * ZEND_AST_OP_ARRAY nodes, so declarations cannot appear here. */ + ZEND_UNREACHABLE(); + return false; + } + + uint32_t children = zend_ast_get_num_children(ast); + for (uint32_t i = 0; i < children; i++) { + if (zend_constexpr_closure_walk_ast(w, ast->child[i])) { + return true; + } + } + return false; +} + +static bool zend_constexpr_closure_walk_zval(zend_constexpr_closure_walk *w, zval *zv) +{ + if (Z_TYPE_P(zv) == IS_CONSTANT_AST) { + return zend_constexpr_closure_walk_ast(w, Z_ASTVAL_P(zv)); + } + return false; +} + +static bool zend_constexpr_closure_walk_attributes(zend_constexpr_closure_walk *w, HashTable *attributes) +{ + zend_attribute *attr; + + if (!attributes) { + return false; + } + + ZEND_HASH_FOREACH_PTR(attributes, attr) { + for (uint32_t i = 0; i < attr->argc; i++) { + if (zend_constexpr_closure_walk_zval(w, &attr->args[i].value)) { + return true; + } + } + } ZEND_HASH_FOREACH_END(); + + return false; +} + +static bool zend_constexpr_closure_walk_op_array(zend_constexpr_closure_walk *w, zend_op_array *op_array) +{ + if (op_array->type != ZEND_USER_FUNCTION) { + return false; + } + + if (zend_constexpr_closure_walk_attributes(w, op_array->attributes)) { + return true; + } + + /* Parameter default values are IS_CONSTANT_AST literals used by RECV_INIT. */ + for (uint32_t i = 0; i < op_array->last_literal; i++) { + if (zend_constexpr_closure_walk_zval(w, &op_array->literals[i])) { + return true; + } + } + + for (uint32_t i = 0; i < op_array->num_dynamic_func_defs; i++) { + if (zend_constexpr_closure_walk_op_array(w, op_array->dynamic_func_defs[i])) { + return true; + } + } + + return false; +} + +/* Enter a reflection element: record its (kind, borrowed name, hook) descriptor + * and restart the local rank. No allocation for elements merely passed through; + * the "@" string is built once from found_* on a match. */ +#define WALK_ELEMENT(kind_, name_, hook_) \ + (w->cur_kind = (kind_), w->cur_name = (name_), w->cur_hook = (hook_), w->next_id = 0) + +static bool zend_constexpr_closure_walk_class(zend_constexpr_closure_walk *w, zend_class_entry *ce) +{ + zend_string *cname, *pname; + zend_class_constant *c; + zend_property_info *prop_info; + zend_function *func; + + if (ce->type != ZEND_USER_CLASS) { + return false; + } + + WALK_ELEMENT(ZEND_CEXPR_SITE_CLASS, NULL, 0); + if (zend_constexpr_closure_walk_attributes(w, ce->attributes)) { + return true; + } + + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&ce->constants_table, cname, c) { + WALK_ELEMENT(ZEND_CEXPR_SITE_CONST, cname, 0); + if (zend_constexpr_closure_walk_attributes(w, c->attributes)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&ce->properties_info, pname, prop_info) { + WALK_ELEMENT(ZEND_CEXPR_SITE_PROP, pname, 0); + if (zend_constexpr_closure_walk_attributes(w, prop_info->attributes)) { + return true; + } + if (prop_info->hooks) { + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + if (prop_info->hooks[i]) { + WALK_ELEMENT(ZEND_CEXPR_SITE_HOOK, pname, (uint8_t) i); + if (zend_constexpr_closure_walk_op_array(w, &prop_info->hooks[i]->op_array)) { + return true; + } + } + } + } + } ZEND_HASH_FOREACH_END(); + + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, func) { + WALK_ELEMENT(ZEND_CEXPR_SITE_METHOD, func->common.function_name, 0); + if (zend_constexpr_closure_walk_op_array(w, &func->op_array)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + + return false; +} + +#undef WALK_ELEMENT + +/* Materialize the opaque "@" reference from a found descriptor. + * The key is the local rank for an anonymous closure, and the name of the + * referenced callable ("Called::method" or "function") for a first-class + * callable, built from the resolved closure so that it carries canonical + * names regardless of how the source spells the reference. */ +static zend_string *zend_constexpr_closure_build_ref(const zend_constexpr_closure_walk *w) +{ + zend_string *key, *ref; + + if (w->found->kind == ZEND_AST_OP_ARRAY) { + key = zend_long_to_str((zend_long) w->found_id); + } else if (w->fcc->func.common.scope) { + zend_string *called = w->fcc->called_scope->name; + zend_string *method = w->fcc->func.common.function_name; + key = zend_string_concat3( + ZSTR_VAL(called), ZSTR_LEN(called), "::", 2, ZSTR_VAL(method), ZSTR_LEN(method)); + } else { + key = zend_string_copy(w->fcc->func.common.function_name); + } + + switch (w->found_kind) { + case ZEND_CEXPR_SITE_CONST: + ref = zend_strpprintf(0, "%s@%s", ZSTR_VAL(w->found_name), ZSTR_VAL(key)); + break; + case ZEND_CEXPR_SITE_PROP: + ref = zend_strpprintf(0, "$%s@%s", ZSTR_VAL(w->found_name), ZSTR_VAL(key)); + break; + case ZEND_CEXPR_SITE_HOOK: + ref = zend_strpprintf(0, "$%s::%s()@%s", ZSTR_VAL(w->found_name), + w->found_hook == ZEND_PROPERTY_HOOK_GET ? "get" : "set", ZSTR_VAL(key)); + break; + case ZEND_CEXPR_SITE_METHOD: + ref = zend_strpprintf(0, "%s()@%s", ZSTR_VAL(w->found_name), ZSTR_VAL(key)); + break; + case ZEND_CEXPR_SITE_CLASS: + default: + ref = zend_strpprintf(0, "@%s", ZSTR_VAL(key)); + break; + } + + zend_string_release_ex(key, 0); + return ref; +} + +/* Resolve (element site, local rank) to a declaration-site AST: locate the one + * named element by a direct hash lookup, then walk only it. This is where + * element-scoping pays off on decode: instead of a class-global scan to the + * Nth site, resolution is bounded by a single element's const-expr surface. */ +static zend_ast *zend_constexpr_closure_site_by_element( + zend_class_entry *ce, const char *site, size_t site_len, + uint32_t rank, const char *key, size_t key_len) +{ + zend_constexpr_closure_walk w; + + if (ce->type != ZEND_USER_CLASS) { + return NULL; + } + + memset(&w, 0, sizeof(w)); + if (key) { + w.target_key = key; + w.target_key_len = key_len; + } else { + w.by_id = true; + w.target_id = rank; + } + w.site_class = ce; + + if (site_len == 0) { + /* class attributes */ + zend_constexpr_closure_walk_attributes(&w, ce->attributes); + return w.found; + } + + if (site[0] == '$') { + /* property "$name" or hook "$name::get()" / "$name::set()" */ + const char *sep = NULL; + for (size_t i = 1; i + 1 < site_len; i++) { + if (site[i] == ':' && site[i + 1] == ':') { + sep = site + i; + break; + } + } + size_t name_len = (sep ? (size_t) (sep - site) : site_len) - 1; + zend_property_info *prop = zend_hash_str_find_ptr(&ce->properties_info, site + 1, name_len); + if (!prop) { + return NULL; + } + if (!sep) { + zend_constexpr_closure_walk_attributes(&w, prop->attributes); + return w.found; + } + const char *spec = sep + 2; + size_t spec_len = site_len - (spec - site); + uint32_t hook_kind; + if (spec_len == 5 && memcmp(spec, "get()", 5) == 0) { + hook_kind = ZEND_PROPERTY_HOOK_GET; + } else if (spec_len == 5 && memcmp(spec, "set()", 5) == 0) { + hook_kind = ZEND_PROPERTY_HOOK_SET; + } else { + return NULL; + } + if (!prop->hooks || !prop->hooks[hook_kind] + || prop->hooks[hook_kind]->type != ZEND_USER_FUNCTION) { + return NULL; + } + zend_constexpr_closure_walk_op_array(&w, &prop->hooks[hook_kind]->op_array); + return w.found; + } + + if (site_len >= 2 && site[site_len - 2] == '(' && site[site_len - 1] == ')') { + /* method "name()" */ + zend_function *func = zend_hash_str_find_ptr_lc(&ce->function_table, site, site_len - 2); + if (!func || func->type != ZEND_USER_FUNCTION) { + return NULL; + } + zend_constexpr_closure_walk_op_array(&w, &func->op_array); + return w.found; + } + + /* class constant / enum case "NAME" */ + zend_class_constant *c = zend_hash_str_find_ptr(&ce->constants_table, site, site_len); + if (!c) { + return NULL; + } + zend_constexpr_closure_walk_attributes(&w, c->attributes); + return w.found; +} + +/* Parse the opaque "@" reference and resolve it. An all-digits + * key is the local rank of an anonymous closure; anything else is the name + * of a first-class callable ("Called::method" or "function"; names cannot + * start with a digit, so the two forms cannot collide). */ +static zend_ast *zend_constexpr_closure_site_by_ref(zend_class_entry *ce, zend_string *id) +{ + const char *s = ZSTR_VAL(id); + size_t len = ZSTR_LEN(id); + const char *at = zend_memrchr(s, '@', len); + if (!at) { + return NULL; + } + size_t site_len = at - s; + const char *key = at + 1; + size_t key_len = len - site_len - 1; + if (key_len == 0) { + return NULL; + } + + bool is_rank = true; + uint64_t rank = 0; + for (size_t i = 0; i < key_len; i++) { + if (key[i] < '0' || key[i] > '9') { + is_rank = false; + break; + } + rank = rank * 10 + (key[i] - '0'); + if (rank > UINT32_MAX) { + return NULL; + } + } + if (is_rank) { + if (key_len > 1 && key[0] == '0') { + return NULL; + } + return zend_constexpr_closure_site_by_element(ce, s, site_len, (uint32_t) rank, NULL, 0); + } + + return zend_constexpr_closure_site_by_element(ce, s, site_len, 0, key, key_len); +} + +static uint32_t zend_constexpr_closure_site_lineno(const zend_ast *site) +{ + if (site->kind == ZEND_AST_OP_ARRAY) { + return zend_ast_get_op_array((zend_ast *) site)->op_array->line_start; + } + return zend_ast_get_lineno((zend_ast *) site); +} + +/* The staleness tripwire is the closure's line relative to its declaring + * class, not an absolute line: an edit above the class (a new use import, + * another declaration) shifts the class and the closure together and leaves + * the offset intact, so only edits between the class line and the closure + * trip it. The declaring class line is the one anchor every consumer can + * recompute, including userland polyfills through ReflectionClass. */ +static zend_long zend_constexpr_closure_rel_lineno(const zend_class_entry *ce, const zend_ast *site) +{ + return (zend_long) zend_constexpr_closure_site_lineno(site) - (zend_long) ce->info.user.line_start; +} + +ZEND_API zend_result zend_constexpr_closure_ref(zend_object *closure_obj, zend_class_entry **ce, zend_string **id, zend_long *lineno, bool *has_line) +{ + const zend_closure *closure = (const zend_closure *) closure_obj; + zend_constexpr_closure_walk w; + zend_class_entry *site_class; + + memset(&w, 0, sizeof(w)); + + if (closure->func.type == ZEND_USER_FUNCTION + && (closure->func.common.fn_flags2 & ZEND_ACC2_CONSTEXPR_CLOSURE) + && !(closure->func.common.fn_flags & ZEND_ACC_FAKE_CLOSURE)) { + /* Anonymous closure declared in a constant expression: identified by + * its op_array, found in the class it is scoped to. */ + site_class = closure->func.common.scope; + w.opcodes = closure->func.op_array.opcodes; + } else if ((closure->func.common.fn_flags2 & ZEND_ACC2_CONSTEXPR_FCC) + && (closure->func.common.fn_flags & ZEND_ACC_FAKE_CLOSURE) + && Z_TYPE(closure->this_ptr) == IS_UNDEF) { + /* First-class callable evaluated in a constant expression: identified + * by its target, found in the class that declared the reference. */ + site_class = closure->constexpr_site; + w.fcc = closure; + } else { + return FAILURE; + } + + if (!site_class || site_class->type != ZEND_USER_CLASS || (site_class->ce_flags & ZEND_ACC_ANON_CLASS)) { + return FAILURE; + } + + w.site_class = site_class; + + if (!zend_constexpr_closure_walk_class(&w, site_class)) { + return FAILURE; + } + + *ce = site_class; + /* Opaque element-scoped reference "@": site names the declaring + * reflection element, rank is the closure's position within just that + * element. Callers treat it as an opaque token (pass it to + * Closure::fromConstExpr); the grammar is an engine detail. */ + *id = zend_constexpr_closure_build_ref(&w); + /* Anonymous closures are keyed by rank and carry a relative-line tripwire; + * first-class callable references are keyed by name and need none, since a + * name cannot drift silently. */ + if (has_line) { + *has_line = w.found->kind == ZEND_AST_OP_ARRAY; + } + if (lineno) { + *lineno = w.found->kind == ZEND_AST_OP_ARRAY + ? zend_constexpr_closure_rel_lineno(site_class, w.found) : 0; + } + return SUCCESS; +} + +ZEND_API void zend_closure_mark_as_constexpr_fcc(zval *closure_zv, zend_class_entry *site_class) +{ + zend_closure *closure = (zend_closure *) Z_OBJ_P(closure_zv); + + ZEND_ASSERT(closure->func.common.fn_flags & ZEND_ACC_FAKE_CLOSURE); + closure->func.common.fn_flags2 |= ZEND_ACC2_CONSTEXPR_FCC; + closure->constexpr_site = site_class; +} +/* }}} */ + +/* {{{ Serialize a closure as a reference to the declaration site of the + constant expression that declared it: either an anonymous closure + declaration, or a first-class callable reference */ +ZEND_METHOD(Closure, __serialize) +{ + zend_class_entry *ce; + zend_string *id; + zend_long lineno; + bool has_line; + zval payload, tagged; + + ZEND_PARSE_PARAMETERS_NONE(); + + if (zend_constexpr_closure_ref(Z_OBJ_P(ZEND_THIS), &ce, &id, &lineno, &has_line) == FAILURE) { + zend_throw_exception(NULL, "Serialization of 'Closure' is not allowed", 0); + RETURN_THROWS(); + } + + /* Tagged-union payload: [ , [ , ] ]. + * A Closure has no properties, so the first slot is an empty array; the + * tag names the reference kind so the format can grow new kinds without + * ambiguity. The "const-expr" payload is [class, id] for a first-class + * callable reference (keyed by name) and [class, id, relative-line] for + * an anonymous closure (keyed by rank, line-checked). */ + array_init_size(&payload, has_line ? 3 : 2); + add_next_index_str(&payload, zend_string_copy(ce->name)); + add_next_index_str(&payload, id); + if (has_line) { + add_next_index_long(&payload, lineno); + } + + array_init_size(&tagged, 2); + add_next_index_string(&tagged, "const-expr"); + add_next_index_zval(&tagged, &payload); + + array_init_size(return_value, 2); + add_next_index_array(return_value, zend_new_array(0)); + add_next_index_zval(return_value, &tagged); +} +/* }}} */ + +/* {{{ Recreate a closure from a reference to its declaration site */ +ZEND_METHOD(Closure, __unserialize) +{ + zend_closure *closure = (zend_closure *) Z_OBJ_P(ZEND_THIS); + HashTable *data; + zval *z_class, *z_id, *z_line; + zend_class_entry *ce; + zend_ast *site; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(data) + ZEND_PARSE_PARAMETERS_END(); + + if (closure->func.type != ZEND_INTERNAL_FUNCTION + || closure->func.internal_function.handler != zend_closure_uninitialized_handler) { + zend_throw_exception(NULL, "Cannot unserialize an already initialized Closure", 0); + RETURN_THROWS(); + } + + /* Tagged-union payload: [ , [ , ] ]. + * Only the "const-expr" tag is known; an unknown tag from a future format + * is rejected cleanly rather than misread. */ + zval *z_props, *z_tagged, *z_tag, *z_payload; + HashTable *tagged, *payload; + + z_props = zend_hash_index_find(data, 0); + z_tagged = zend_hash_index_find(data, 1); + if (zend_hash_num_elements(data) != 2 || !z_props || !z_tagged) { + zend_throw_exception(NULL, "Invalid serialization data for Closure object", 0); + RETURN_THROWS(); + } + ZVAL_DEREF(z_tagged); + if (Z_TYPE_P(z_tagged) != IS_ARRAY) { + zend_throw_exception(NULL, "Invalid serialization data for Closure object", 0); + RETURN_THROWS(); + } + tagged = Z_ARRVAL_P(z_tagged); + z_tag = zend_hash_index_find(tagged, 0); + z_payload = zend_hash_index_find(tagged, 1); + if (zend_hash_num_elements(tagged) != 2 || !z_tag || !z_payload) { + zend_throw_exception(NULL, "Invalid serialization data for Closure object", 0); + RETURN_THROWS(); + } + ZVAL_DEREF(z_tag); + ZVAL_DEREF(z_payload); + if (Z_TYPE_P(z_tag) != IS_STRING || !zend_string_equals_literal(Z_STR_P(z_tag), "const-expr") + || Z_TYPE_P(z_payload) != IS_ARRAY) { + zend_throw_exception(NULL, "Invalid serialization data for Closure object", 0); + RETURN_THROWS(); + } + payload = Z_ARRVAL_P(z_payload); + + z_class = zend_hash_index_find(payload, 0); + z_id = zend_hash_index_find(payload, 1); + z_line = zend_hash_index_find(payload, 2); + if (z_class) { + ZVAL_DEREF(z_class); + } + if (z_id) { + ZVAL_DEREF(z_id); + } + if (z_line) { + ZVAL_DEREF(z_line); + } + if (!z_class || Z_TYPE_P(z_class) != IS_STRING + || !z_id || Z_TYPE_P(z_id) != IS_STRING) { + zend_throw_exception(NULL, "Invalid serialization data for Closure object", 0); + RETURN_THROWS(); + } + + ce = zend_lookup_class(Z_STR_P(z_class)); + if (!ce || ce->type != ZEND_USER_CLASS) { + if (!EG(exception)) { + zend_throw_exception_ex(NULL, 0, + "Invalid serialization data for Closure object (cannot load class \"%s\")", + Z_STRVAL_P(z_class)); + } + RETURN_THROWS(); + } + + site = zend_constexpr_closure_site_by_ref(ce, Z_STR_P(z_id)); + if (!site) { + zend_throw_exception_ex(NULL, 0, + "Invalid serialization data for Closure object (constant-expression closure \"%s\" of class %s not found)", + Z_STRVAL_P(z_id), ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + + if (site->kind == ZEND_AST_OP_ARRAY) { + /* Anonymous closures are referenced by a positional rank: the relative + * start line is the staleness tripwire. Name-keyed references carry + * none, so a line here means a malformed payload. */ + if (!z_line || Z_TYPE_P(z_line) != IS_LONG) { + zend_throw_exception(NULL, "Invalid serialization data for Closure object", 0); + RETURN_THROWS(); + } + if (zend_constexpr_closure_rel_lineno(ce, site) != Z_LVAL_P(z_line)) { + zend_throw_exception_ex(NULL, 0, + "Invalid serialization data for Closure object (constant-expression closure \"%s\" of class %s not found)", + Z_STRVAL_P(z_id), ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + zend_closure_init_ex(closure, + (zend_function *) zend_ast_get_op_array(site)->op_array, ce, ce, NULL, /* is_fake */ false); + } else { + /* First-class callable site: keyed by name, no line tripwire. */ + if (z_line) { + zend_throw_exception(NULL, "Invalid serialization data for Closure object", 0); + RETURN_THROWS(); + } + /* First-class callable site: re-evaluate it like attribute evaluation + * would, including its visibility checks against the site class. */ + zval tmp; + zend_closure *tmp_closure; + + if (zend_ast_evaluate(&tmp, site, ce) == FAILURE) { + if (!EG(exception)) { + zend_throw_exception(NULL, "Invalid serialization data for Closure object", 0); + } + RETURN_THROWS(); + } + + ZEND_ASSERT(Z_TYPE(tmp) == IS_OBJECT && Z_OBJCE(tmp) == zend_ce_closure); + tmp_closure = (zend_closure *) Z_OBJ(tmp); + + zend_closure_init_ex(closure, &tmp_closure->func, + tmp_closure->func.common.scope, tmp_closure->called_scope, NULL, /* is_fake */ true); + closure->func.common.fn_flags |= ZEND_ACC_FAKE_CLOSURE; + closure->constexpr_site = tmp_closure->constexpr_site; + GC_ADD_FLAGS(&closure->std, GC_NOT_COLLECTABLE); + + zval_ptr_dtor(&tmp); + } +} +/* }}} */ + +/* {{{ Recreate a closure declared in a constant expression of the given class */ +ZEND_METHOD(Closure, fromConstExpr) +{ + zend_string *class_name; + zend_string *id; + zend_class_entry *ce; + zend_ast *site; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR(class_name) + Z_PARAM_STR(id) + ZEND_PARSE_PARAMETERS_END(); + + ce = zend_lookup_class(class_name); + if (!ce) { + if (!EG(exception)) { + zend_throw_error(NULL, "Class \"%s\" not found", ZSTR_VAL(class_name)); + } + RETURN_THROWS(); + } + + site = zend_constexpr_closure_site_by_ref(ce, id); + if (!site) { + zend_value_error("Closure::fromConstExpr(): Argument #2 ($id) does not refer to a constant-expression closure of class %s", ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + + if (site->kind == ZEND_AST_OP_ARRAY) { + zend_create_closure(return_value, (zend_function *) zend_ast_get_op_array(site)->op_array, ce, ce, NULL); + } else if (zend_ast_evaluate(return_value, site, ce) == FAILURE) { + if (!EG(exception)) { + zend_value_error("Closure::fromConstExpr(): Argument #2 ($id) does not refer to a constant-expression closure of class %s", ZSTR_VAL(ce->name)); + } + RETURN_THROWS(); + } +} +/* }}} */ + static ZEND_COLD zend_function *zend_closure_get_constructor(zend_object *object) /* {{{ */ { zend_throw_error(NULL, "Instantiation of class Closure is not allowed"); @@ -570,6 +1458,12 @@ static void zend_closure_free_storage(zend_object *object) /* {{{ */ } /* }}} */ +static ZEND_COLD ZEND_NAMED_FUNCTION(zend_closure_uninitialized_handler) /* {{{ */ +{ + zend_throw_error(NULL, "Cannot call an uninitialized Closure"); +} +/* }}} */ + static zend_object *zend_closure_new(zend_class_entry *class_type) /* {{{ */ { zend_closure *closure; @@ -579,6 +1473,17 @@ static zend_object *zend_closure_new(zend_class_entry *class_type) /* {{{ */ zend_object_std_init(&closure->std, class_type); + /* Initialize the function as a safe placeholder. Until the closure is + * actually initialized through zend_closure_init_ex() it may be observed + * by userland code (e.g. while delayed __unserialize() calls are still + * pending) and must not crash any object handler. The placeholder mimics + * a fake internal closure with a handler that always throws. */ + closure->func.type = ZEND_INTERNAL_FUNCTION; + closure->func.common.fn_flags = ZEND_ACC_PUBLIC | ZEND_ACC_CLOSURE | ZEND_ACC_FAKE_CLOSURE; + closure->func.common.function_name = ZSTR_EMPTY_ALLOC(); + closure->func.internal_function.handler = zend_closure_uninitialized_handler; + closure->orig_internal_handler = zend_closure_uninitialized_handler; + return (zend_object*)closure; } /* }}} */ @@ -590,6 +1495,7 @@ static zend_object *zend_closure_clone(zend_object *zobject) /* {{{ */ zend_create_closure(&result, &closure->func, closure->func.common.scope, closure->called_scope, &closure->this_ptr); + ((zend_closure *) Z_OBJ(result))->constexpr_site = closure->constexpr_site; return Z_OBJ(result); } /* }}} */ @@ -757,15 +1663,10 @@ static ZEND_NAMED_FUNCTION(zend_closure_internal_handler) /* {{{ */ } /* }}} */ -static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr, bool is_fake) /* {{{ */ +static void zend_closure_init_ex(zend_closure *closure, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr, bool is_fake) /* {{{ */ { - zend_closure *closure; void *ptr; - object_init_ex(res, zend_ce_closure); - - closure = (zend_closure *)Z_OBJ_P(res); - if ((scope == NULL) && this_ptr && (Z_TYPE_P(this_ptr) != IS_UNDEF)) { /* use dummy scope if we're binding an object without specifying a scope */ /* maybe it would be better to create one for this purpose */ @@ -859,6 +1760,13 @@ static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_en } /* }}} */ +static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr, bool is_fake) /* {{{ */ +{ + object_init_ex(res, zend_ce_closure); + zend_closure_init_ex((zend_closure *) Z_OBJ_P(res), func, scope, called_scope, this_ptr, is_fake); +} +/* }}} */ + ZEND_API void zend_create_closure(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr) { zend_create_closure_ex(res, func, scope, called_scope, this_ptr, diff --git a/Zend/zend_closures.h b/Zend/zend_closures.h index 2ff4934f2a3a..7220d0809f15 100644 --- a/Zend/zend_closures.h +++ b/Zend/zend_closures.h @@ -40,6 +40,20 @@ ZEND_API zend_function *zend_get_closure_invoke_method(zend_object *obj); ZEND_API const zend_function *zend_get_closure_method_def(zend_object *obj); ZEND_API zval* zend_get_closure_this_ptr(zval *obj); +/* Closures declared in constant expressions (anonymous declarations as well + * as first-class callable references) are addressable by an element-scoped + * reference: an opaque "@" string where names the declaring + * reflection element and is the closure's rank within that element (for + * an anonymous closure) or the referenced callable's name (for a first-class + * callable). zend_constexpr_closure_ref() returns the reference (a fresh + * zend_string the caller owns) for a Closure object, when it has one. For an + * anonymous closure it also sets *has_line and *lineno to the closure's line + * relative to the declaring class (the staleness tripwire); a first-class + * callable reference has no line (*has_line is false). Any out-pointer may be + * NULL. */ +ZEND_API zend_result zend_constexpr_closure_ref(zend_object *closure_obj, zend_class_entry **ce, zend_string **id, zend_long *lineno, bool *has_line); +ZEND_API void zend_closure_mark_as_constexpr_fcc(zval *closure_zv, zend_class_entry *site_class); + END_EXTERN_C() #endif diff --git a/Zend/zend_closures.stub.php b/Zend/zend_closures.stub.php index 46b51617eef9..88800e532f68 100644 --- a/Zend/zend_closures.stub.php +++ b/Zend/zend_closures.stub.php @@ -4,7 +4,6 @@ /** * @strict-properties - * @not-serializable */ final class Closure { @@ -22,5 +21,11 @@ public function call(object $newThis, mixed ...$args): mixed {} public static function fromCallable(callable $callback): Closure {} + public static function fromConstExpr(string $class, string $id): Closure {} + public static function getCurrent(): Closure {} + + public function __serialize(): array {} + + public function __unserialize(array $data): void {} } diff --git a/Zend/zend_closures_arginfo.h b/Zend/zend_closures_arginfo.h index 5bc983a97c2c..bbd2fe37305e 100644 --- a/Zend/zend_closures_arginfo.h +++ b/Zend/zend_closures_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit zend_closures.stub.php instead. - * Stub hash: e0626e52adb2d38dad1140c1a28cc7774cc84500 */ + * Stub hash: d84d010e45e23e4a61d3b40e795d1075bcbae91c */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Closure___construct, 0, 0, 0) ZEND_END_ARG_INFO() @@ -24,15 +24,30 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Closure_fromCallable, 0, 1, ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Closure_fromConstExpr, 0, 2, Closure, 0) + ZEND_ARG_TYPE_INFO(0, class, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, id, IS_STRING, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Closure_getCurrent, 0, 0, Closure, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Closure___serialize, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Closure___unserialize, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + ZEND_METHOD(Closure, __construct); ZEND_METHOD(Closure, bind); ZEND_METHOD(Closure, bindTo); ZEND_METHOD(Closure, call); ZEND_METHOD(Closure, fromCallable); +ZEND_METHOD(Closure, fromConstExpr); ZEND_METHOD(Closure, getCurrent); +ZEND_METHOD(Closure, __serialize); +ZEND_METHOD(Closure, __unserialize); static const zend_function_entry class_Closure_methods[] = { ZEND_ME(Closure, __construct, arginfo_class_Closure___construct, ZEND_ACC_PRIVATE) @@ -40,7 +55,10 @@ static const zend_function_entry class_Closure_methods[] = { ZEND_ME(Closure, bindTo, arginfo_class_Closure_bindTo, ZEND_ACC_PUBLIC) ZEND_ME(Closure, call, arginfo_class_Closure_call, ZEND_ACC_PUBLIC) ZEND_ME(Closure, fromCallable, arginfo_class_Closure_fromCallable, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(Closure, fromConstExpr, arginfo_class_Closure_fromConstExpr, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) ZEND_ME(Closure, getCurrent, arginfo_class_Closure_getCurrent, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(Closure, __serialize, arginfo_class_Closure___serialize, ZEND_ACC_PUBLIC) + ZEND_ME(Closure, __unserialize, arginfo_class_Closure___unserialize, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -49,7 +67,7 @@ static zend_class_entry *register_class_Closure(void) zend_class_entry ce, *class_entry; INIT_CLASS_ENTRY(ce, "Closure", class_Closure_methods); - class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES); return class_entry; } diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 105f99d24171..87e497246da7 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -11788,6 +11788,7 @@ static void zend_compile_const_expr_closure(zend_ast **ast_ptr) znode node; zend_op_array *op = zend_compile_func_decl(&node, (zend_ast*)closure_ast, FUNC_DECL_LEVEL_CONSTEXPR); + op->fn_flags2 |= ZEND_ACC2_CONSTEXPR_CLOSURE; zend_ast_destroy(*ast_ptr); *ast_ptr = zend_ast_create_op_array(op); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 0e31332c97f0..7df202da60d3 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -412,11 +412,19 @@ typedef struct _zend_oparray_context { /* op_array uses strict mode types | | | */ #define ZEND_ACC_STRICT_TYPES (1U << 31) /* | X | | */ /* | | | */ -/* Function Flags 2 (fn_flags2) (unused: 1-31) | | | */ +/* Function Flags 2 (fn_flags2) (unused: 3-31) | | | */ /* ============================ | | | */ /* | | | */ /* Function forbids dynamic calls | | | */ #define ZEND_ACC2_FORBID_DYN_CALLS (1 << 0) /* | X | | */ +/* | | | */ +/* Closure was declared in a constant expression | | | */ +#define ZEND_ACC2_CONSTEXPR_CLOSURE (1 << 1) /* | X | | */ +/* | | | */ +/* Fake closure was created by evaluating a first-class | | | */ +/* callable in a constant expression (set on the | | | */ +/* closure's own function copy, never on the original) | | | */ +#define ZEND_ACC2_CONSTEXPR_FCC (1 << 2) /* | X | | */ #define ZEND_ACC_PPP_MASK (ZEND_ACC_PUBLIC | ZEND_ACC_PROTECTED | ZEND_ACC_PRIVATE) #define ZEND_ACC_PPP_SET_MASK (ZEND_ACC_PUBLIC_SET | ZEND_ACC_PROTECTED_SET | ZEND_ACC_PRIVATE_SET) diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index af565ed53a32..0773934b7b20 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -1963,6 +1963,57 @@ ZEND_METHOD(ReflectionFunction, isAnonymous) } /* }}} */ +/* {{{ Returns the declaration-site id of a closure declared in a constant + expression (an anonymous closure or a first-class callable reference), + for use with Closure::fromConstExpr(), or null */ +ZEND_METHOD(ReflectionFunction, getConstExprId) +{ + reflection_object *intern; + zend_function *fptr; + zend_class_entry *ce; + zend_string *id; + + ZEND_PARSE_PARAMETERS_NONE(); + + GET_REFLECTION_OBJECT_PTR(fptr); + (void) fptr; + + if (Z_ISUNDEF(intern->obj) + || Z_OBJCE(intern->obj) != zend_ce_closure + || zend_constexpr_closure_ref(Z_OBJ(intern->obj), &ce, &id, NULL, NULL) == FAILURE) { + RETURN_NULL(); + } + + RETURN_STR(id); +} +/* }}} */ + +/* {{{ Returns the class whose constant expressions declared this closure, + for use with Closure::fromConstExpr() together with getConstExprId(), + or null */ +ZEND_METHOD(ReflectionFunction, getConstExprClass) +{ + reflection_object *intern; + zend_function *fptr; + zend_class_entry *ce; + zend_string *id; + + ZEND_PARSE_PARAMETERS_NONE(); + + GET_REFLECTION_OBJECT_PTR(fptr); + (void) fptr; + + if (Z_ISUNDEF(intern->obj) + || Z_OBJCE(intern->obj) != zend_ce_closure + || zend_constexpr_closure_ref(Z_OBJ(intern->obj), &ce, &id, NULL, NULL) == FAILURE) { + RETURN_NULL(); + } + + zend_string_release(id); + RETURN_STR_COPY(ce->name); +} +/* }}} */ + /* {{{ Returns whether this function has been disabled or not */ ZEND_METHOD(ReflectionFunction, isDisabled) { diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index dd605100f8ba..b438064b7329 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -128,6 +128,10 @@ public function __toString(): string {} public function isAnonymous(): bool {} + public function getConstExprId(): ?string {} + + public function getConstExprClass(): ?string {} + /** * @tentative-return-type */ diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index 65571f38d43c..5d1671b6de08 100644 --- a/ext/reflection/php_reflection_arginfo.h +++ b/ext/reflection/php_reflection_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit php_reflection.stub.php instead. - * Stub hash: c80946cc8c8215bb6527e09bb71b3a97a76a6a98 + * Stub hash: 6617d3a93a0de137083bf0a8c309da867a34f5c9 * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Reflection_getModifierNames, 0, 1, IS_ARRAY, 0) @@ -96,6 +96,11 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionFunction_isAnonymous arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionFunction_getConstExprId, 0, 0, IS_STRING, 1) +ZEND_END_ARG_INFO() + +#define arginfo_class_ReflectionFunction_getConstExprClass arginfo_class_ReflectionFunction_getConstExprId + #define arginfo_class_ReflectionFunction_isDisabled arginfo_class_ReflectionFunctionAbstract_inNamespace ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionFunction_invoke, 0, 0, IS_MIXED, 0) @@ -698,8 +703,7 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_ReflectionFiber_getFiber, 0, 0, Fiber, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionFiber_getExecutingFile, 0, 0, IS_STRING, 1) -ZEND_END_ARG_INFO() +#define arginfo_class_ReflectionFiber_getExecutingFile arginfo_class_ReflectionFunction_getConstExprId ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionFiber_getExecutingLine, 0, 0, IS_LONG, 1) ZEND_END_ARG_INFO() @@ -771,6 +775,8 @@ ZEND_METHOD(ReflectionFunctionAbstract, getAttributes); ZEND_METHOD(ReflectionFunction, __construct); ZEND_METHOD(ReflectionFunction, __toString); ZEND_METHOD(ReflectionFunction, isAnonymous); +ZEND_METHOD(ReflectionFunction, getConstExprId); +ZEND_METHOD(ReflectionFunction, getConstExprClass); ZEND_METHOD(ReflectionFunction, isDisabled); ZEND_METHOD(ReflectionFunction, invoke); ZEND_METHOD(ReflectionFunction, invokeArgs); @@ -1055,6 +1061,8 @@ static const zend_function_entry class_ReflectionFunction_methods[] = { ZEND_ME(ReflectionFunction, __construct, arginfo_class_ReflectionFunction___construct, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionFunction, __toString, arginfo_class_ReflectionFunction___toString, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionFunction, isAnonymous, arginfo_class_ReflectionFunction_isAnonymous, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionFunction, getConstExprId, arginfo_class_ReflectionFunction_getConstExprId, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionFunction, getConstExprClass, arginfo_class_ReflectionFunction_getConstExprClass, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionFunction, isDisabled, arginfo_class_ReflectionFunction_isDisabled, ZEND_ACC_PUBLIC|ZEND_ACC_DEPRECATED) ZEND_ME(ReflectionFunction, invoke, arginfo_class_ReflectionFunction_invoke, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionFunction, invokeArgs, arginfo_class_ReflectionFunction_invokeArgs, ZEND_ACC_PUBLIC) diff --git a/ext/reflection/php_reflection_decl.h b/ext/reflection/php_reflection_decl.h index a87e1635419b..d5998220c744 100644 --- a/ext/reflection/php_reflection_decl.h +++ b/ext/reflection/php_reflection_decl.h @@ -1,12 +1,12 @@ /* This is a generated file, edit php_reflection.stub.php instead. - * Stub hash: c80946cc8c8215bb6527e09bb71b3a97a76a6a98 */ + * Stub hash: 6617d3a93a0de137083bf0a8c309da867a34f5c9 */ -#ifndef ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H -#define ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H +#ifndef ZEND_PHP_REFLECTION_DECL_6617d3a93a0de137083bf0a8c309da867a34f5c9_H +#define ZEND_PHP_REFLECTION_DECL_6617d3a93a0de137083bf0a8c309da867a34f5c9_H typedef enum zend_enum_PropertyHookType { ZEND_ENUM_PropertyHookType_Get = 1, ZEND_ENUM_PropertyHookType_Set = 2, } zend_enum_PropertyHookType; -#endif /* ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H */ +#endif /* ZEND_PHP_REFLECTION_DECL_6617d3a93a0de137083bf0a8c309da867a34f5c9_H */ diff --git a/ext/reflection/tests/ReflectionFunction_getConstExprId.phpt b/ext/reflection/tests/ReflectionFunction_getConstExprId.phpt new file mode 100644 index 000000000000..4257f79bde0f --- /dev/null +++ b/ext/reflection/tests/ReflectionFunction_getConstExprId.phpt @@ -0,0 +1,59 @@ +--TEST-- +ReflectionFunction::getConstExprId() and getConstExprClass() +--FILE-- +getAttributes(); + +// Anonymous closure declared in a constant expression +$r = new ReflectionFunction($attrs[0]->getArguments()[0]); +var_dump($r->getConstExprId(), $r->getConstExprClass()); + +$recreated = Closure::fromConstExpr($r->getConstExprClass(), $r->getConstExprId()); +var_dump($recreated()); + +// First-class callable reference declared in a constant expression: the +// const-expr class is the declaring site, not the callable's scope, and +// the id is keyed by the callable's name, not by a rank. +$r = new ReflectionFunction($attrs[1]->getArguments()[0]); +var_dump($r->getConstExprId(), $r->getConstExprClass(), $r->getClosureScopeClass()->name); + +$recreated = Closure::fromConstExpr($r->getConstExprClass(), $r->getConstExprId()); +var_dump($recreated()); + +// Null for anything that is not declared in a constant expression +var_dump((new ReflectionFunction(function () {}))->getConstExprId()); +var_dump((new ReflectionFunction(strlen(...)))->getConstExprId()); +var_dump((new ReflectionFunction(Validators::check(...)))->getConstExprId()); +var_dump((new ReflectionFunction('strlen'))->getConstExprId()); +var_dump((new ReflectionFunction('strlen'))->getConstExprClass()); + +?> +--EXPECT-- +string(4) "$p@0" +string(4) "Demo" +string(2) "ok" +string(20) "$p@Validators::check" +string(4) "Demo" +string(10) "Validators" +string(7) "checked" +NULL +NULL +NULL +NULL +NULL