From da508ed950c115f974098f4cc9da97c12cd356cb Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Mon, 15 Jun 2026 21:00:00 -0500 Subject: [PATCH] implement function autoloading --- UPGRADING.FUNCTION-AUTOLOADING | 60 ++++++ .../function_autoload_callable_kinds.phpt | 70 +++++++ .../autoload/function_autoload_chain.phpt | 49 +++++ .../function_autoload_exceptions.phpt | 104 ++++++++++ .../function_autoload_four_scenarios.phpt | 54 +++++ .../function_autoload_invocation.phpt | 187 ++++++++++++++++++ .../function_autoload_manual_trigger.phpt | 41 ++++ .../function_autoload_name_normalization.phpt | 84 ++++++++ .../autoload/function_autoload_namespace.phpt | 72 +++++++ .../function_autoload_no_pinning.phpt | 59 ++++++ .../function_autoload_recursion_mutation.phpt | 82 ++++++++ .../function_autoload_registration.phpt | 78 ++++++++ ...nction_autoload_registry_independence.phpt | 83 ++++++++ .../function_autoload_shadow_global.phpt | 35 ++++ .../function_autoload_use_function.phpt | 22 +++ Zend/zend_API.c | 14 +- Zend/zend_autoload.c | 103 ++++++++++ Zend/zend_autoload.h | 6 + Zend/zend_builtin_functions.c | 12 +- Zend/zend_builtin_functions.stub.php | 2 +- Zend/zend_builtin_functions_arginfo.h | 3 +- Zend/zend_execute.c | 22 ++- Zend/zend_execute.h | 11 ++ Zend/zend_execute_API.c | 57 ++++++ Zend/zend_globals.h | 1 + Zend/zend_vm_def.h | 31 ++- Zend/zend_vm_execute.h | 62 ++++-- ext/opcache/jit/zend_jit_helpers.c | 33 +++- ext/opcache/jit/zend_jit_ir.c | 17 +- .../tests/jit/function_autoload_001.phpt | 29 +++ .../tests/jit/function_autoload_002.phpt | 36 ++++ .../tests/jit/function_autoload_003.phpt | 31 +++ ext/reflection/php_reflection.c | 4 +- ext/spl/php_spl.c | 67 +++++++ ext/spl/php_spl.stub.php | 8 + ext/spl/php_spl_arginfo.h | 23 ++- ...n_loader_rejects_call_function_loader.phpt | 12 ++ 37 files changed, 1624 insertions(+), 40 deletions(-) create mode 100644 UPGRADING.FUNCTION-AUTOLOADING create mode 100644 Zend/tests/autoload/function_autoload_callable_kinds.phpt create mode 100644 Zend/tests/autoload/function_autoload_chain.phpt create mode 100644 Zend/tests/autoload/function_autoload_exceptions.phpt create mode 100644 Zend/tests/autoload/function_autoload_four_scenarios.phpt create mode 100644 Zend/tests/autoload/function_autoload_invocation.phpt create mode 100644 Zend/tests/autoload/function_autoload_manual_trigger.phpt create mode 100644 Zend/tests/autoload/function_autoload_name_normalization.phpt create mode 100644 Zend/tests/autoload/function_autoload_namespace.phpt create mode 100644 Zend/tests/autoload/function_autoload_no_pinning.phpt create mode 100644 Zend/tests/autoload/function_autoload_recursion_mutation.phpt create mode 100644 Zend/tests/autoload/function_autoload_registration.phpt create mode 100644 Zend/tests/autoload/function_autoload_registry_independence.phpt create mode 100644 Zend/tests/autoload/function_autoload_shadow_global.phpt create mode 100644 Zend/tests/autoload/function_autoload_use_function.phpt create mode 100644 ext/opcache/tests/jit/function_autoload_001.phpt create mode 100644 ext/opcache/tests/jit/function_autoload_002.phpt create mode 100644 ext/opcache/tests/jit/function_autoload_003.phpt create mode 100644 ext/spl/tests/autoloading/spl_autoload_register_function_loader_rejects_call_function_loader.phpt diff --git a/UPGRADING.FUNCTION-AUTOLOADING b/UPGRADING.FUNCTION-AUTOLOADING new file mode 100644 index 000000000000..87120122d9d2 --- /dev/null +++ b/UPGRADING.FUNCTION-AUTOLOADING @@ -0,0 +1,60 @@ +UPGRADE NOTES: FUNCTION AUTOLOADING + +1. New Features +2. Changed Functions +3. New Functions +4. Internal API Changes + +======================================== +1. New Features +======================================== + +- Core: + . Added function autoloading support. Register callbacks via + spl_autoload_register_function_loader() that are invoked when an + undefined function is referenced. It is triggered wherever an + undefined function name is resolved: direct calls, dynamic calls + through a string variable ("$func()"), function_exists(), callable + resolution (is_callable(), call_user_func()/call_user_func_array(), + array and sort callbacks such as array_map()/array_filter()/usort(), + callable typed parameters, and Closure::fromCallable()), and the + Reflection API (new ReflectionFunction($name)). + As with class autoloading, it is NOT triggered by contexts that + deliberately avoid autoloading: function_exists($name, false), + is_callable($name, true) (syntax-only checks), and + get_defined_functions(). spl_autoload_call_function_loader() can be + used to trigger function autoloading manually in those situations. + +======================================== +2. Changed Functions +======================================== + +- Core: + . function_exists() now accepts an optional bool $autoload parameter. + When true (the default, matching class_exists()), function + autoloading is triggered before the function reports as missing. + BC note: existing function_exists() guards around conditional + function definitions (polyfills) will now consult registered function + loaders before falling through to define the fallback. Pass + function_exists($name, false) to check without autoloading. + +======================================== +3. New Functions +======================================== + +- SPL: + . spl_autoload_register_function_loader() registers a function autoloader. + . spl_autoload_unregister_function_loader() unregisters a function autoloader. + . spl_autoload_function_loaders() returns registered function autoloaders. + . spl_autoload_call_function_loader() manually triggers function autoloading. + +======================================== +4. Internal API Changes +======================================== + +- Zend: + . Added zend_autoload_function_fcc_map_to_callable_zval_map() as the + function-loader counterpart to the existing + zend_autoload_fcc_map_to_callable_zval_map(). The existing function + keeps its name and behaviour (it backs spl_autoload_functions()), so + there is no break for extensions that call it. diff --git a/Zend/tests/autoload/function_autoload_callable_kinds.phpt b/Zend/tests/autoload/function_autoload_callable_kinds.phpt new file mode 100644 index 000000000000..8ed7782a4ab5 --- /dev/null +++ b/Zend/tests/autoload/function_autoload_callable_kinds.phpt @@ -0,0 +1,70 @@ +--TEST-- +Function autoloading: method and trampoline loaders +--FILE-- + +--EXPECT-- +== static method == +string(11) "from_static" +== instance method == +string(13) "from_instance" +int(2) +== trampoline == +int(2) +Trampoline for trampoline1: demo_func3 +Trampoline for trampoline2: demo_func3 +bool(false) +bool(true) +bool(false) +bool(true) +array(0) { +} +bool(false) diff --git a/Zend/tests/autoload/function_autoload_chain.phpt b/Zend/tests/autoload/function_autoload_chain.phpt new file mode 100644 index 000000000000..187a08938260 --- /dev/null +++ b/Zend/tests/autoload/function_autoload_chain.phpt @@ -0,0 +1,49 @@ +--TEST-- +Function autoloading: multiple loaders and prepend order +--FILE-- + +--EXPECT-- +== order == +loader1: demo_func +loader2: demo_func +string(12) "from_loader2" +== prepend == +prepended: demo_func2 +string(2) "ok" diff --git a/Zend/tests/autoload/function_autoload_exceptions.phpt b/Zend/tests/autoload/function_autoload_exceptions.phpt new file mode 100644 index 000000000000..372bd919710e --- /dev/null +++ b/Zend/tests/autoload/function_autoload_exceptions.phpt @@ -0,0 +1,104 @@ +--TEST-- +Function autoloading: loader exceptions propagate, failures are not cached +--FILE-- +getMessage(), "\n"; +} +// failed autoloads are not cached, so the loader runs again +try { + missing_func(); +} catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; +} + +echo "== dynamic propagates ==\n"; +$f = 'missing_func'; +try { + $f(); +} catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; +} +spl_autoload_unregister_function_loader($thrower); + +echo "== callable resolution wrapping ==\n"; +// is_callable() rethrows the loader's exception directly; the call/closure APIs +// wrap it in a TypeError but keep it as the previous exception. Either way the +// loader's exception must not be swallowed. +$thrower = function (string $name) { + throw new RuntimeException("loader failed"); +}; +spl_autoload_register_function_loader($thrower); +function check(callable $cb): void { + try { + $cb(); + echo "no exception\n"; + } catch (\Throwable $e) { + $origin = $e instanceof RuntimeException ? $e : $e->getPrevious(); + echo get_class($e), " -> ", get_class($origin), ": ", $origin->getMessage(), "\n"; + } +} +check(fn() => is_callable('boom')); +check(fn() => call_user_func('boom')); +check(fn() => Closure::fromCallable('boom')); +spl_autoload_unregister_function_loader($thrower); + +echo "== silent decline retries ==\n"; +// A name that fails to load is retried; there is no negative cache. +// A loader that silently declines (no throw, no definition) does not poison the +// name, so a name unloadable now may become loadable later. (The throwing-loader +// retry is covered by the "direct propagates" section above.) +$attempts = 0; +$enabled = false; +$loader = function (string $name) use (&$attempts, &$enabled) { + if ($name !== 'late_func') { + return; + } + $attempts++; + echo "attempt $attempts\n"; + if ($enabled) { + eval('function late_func() { return "loaded on retry"; }'); + } + // otherwise decline silently: no exception, no definition +}; +spl_autoload_register_function_loader($loader); +try { + late_func(); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +try { + late_func(); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +$enabled = true; +var_dump(late_func()); +echo "attempts: $attempts\n"; +spl_autoload_unregister_function_loader($loader); +?> +--EXPECT-- +== direct propagates == +Autoload failed for: missing_func +Autoload failed for: missing_func +== dynamic propagates == +Autoload failed for: missing_func +== callable resolution wrapping == +RuntimeException -> RuntimeException: loader failed +TypeError -> RuntimeException: loader failed +TypeError -> RuntimeException: loader failed +== silent decline retries == +attempt 1 +Call to undefined function late_func() +attempt 2 +Call to undefined function late_func() +attempt 3 +string(15) "loaded on retry" +attempts: 3 diff --git a/Zend/tests/autoload/function_autoload_four_scenarios.phpt b/Zend/tests/autoload/function_autoload_four_scenarios.phpt new file mode 100644 index 000000000000..f5314a17ba5e --- /dev/null +++ b/Zend/tests/autoload/function_autoload_four_scenarios.phpt @@ -0,0 +1,54 @@ +--TEST-- +Function autoloading: the four namespace resolution scenarios for an unqualified call +--FILE-- +getMessage(), "\n"; +} +\var_dump(s4()); // global found via fallback: no re-consult + +echo "loader calls: $count\n"; +\var_dump(\function_exists('App\s4', false)); // no pinning: never became App\s4 +?> +--EXPECT-- +string(5) "ns s1" +string(9) "global s2" +loader(App\s3) +string(5) "ns s3" +string(5) "ns s3" +loader(App\s4) +Call to undefined function App\s4() +string(9) "global s4" +loader calls: 2 +bool(false) diff --git a/Zend/tests/autoload/function_autoload_invocation.phpt b/Zend/tests/autoload/function_autoload_invocation.phpt new file mode 100644 index 000000000000..e2881b128cac --- /dev/null +++ b/Zend/tests/autoload/function_autoload_invocation.phpt @@ -0,0 +1,187 @@ +--TEST-- +Function autoloading: the call surfaces that trigger a loader +--FILE-- + $b; }'); + } +}; +spl_autoload_register_function_loader($loader); +echo implode(',', array_map('doubler', [1, 2, 3])), "\n"; +echo implode(',', array_filter([1, 2, 3, 4], 'odd_filter')), "\n"; +$data = [3, 1, 2]; +usort($data, 'comparator'); +echo implode(',', $data), "\n"; +spl_autoload_unregister_function_loader($loader); + +echo "== callable param coercion ==\n"; +$loader = function (string $name) { + echo "loader($name)\n"; + if ($name === 'demo_func5') { + eval('function demo_func5($x) { return "cp:$x"; }'); + } +}; +spl_autoload_register_function_loader($loader); +function apply(callable $fn, $arg) { + return $fn($arg); +} +var_dump(apply('demo_func5', 5)); // type check resolves the string, autoloads +try { + apply('missing_func', 0); // loader runs during the check, then TypeError +} catch (\TypeError $e) { + echo get_class($e), "\n"; +} +spl_autoload_unregister_function_loader($loader); + +echo "== Closure::fromCallable ==\n"; +// For a name that never resolves, fromCallable() may consult the loader more +// than once, so assert only that it ran (a flag), not an exact echo count. +$consulted = false; +$loader = function (string $name) use (&$consulted) { + if ($name === 'missing_func') { + $consulted = true; + return; + } + echo "loader($name)\n"; + if ($name === 'demo_func6') { + eval('function demo_func6($x) { return "fc:$x"; }'); + } +}; +spl_autoload_register_function_loader($loader); +$closure = Closure::fromCallable('demo_func6'); +var_dump($closure(7)); +try { + Closure::fromCallable('missing_func'); +} catch (\TypeError $e) { + echo get_class($e), "\n"; +} +var_dump($consulted); +spl_autoload_unregister_function_loader($loader); + +echo "== dynamic call ==\n"; +$loader = function (string $name) { + echo "loader($name)\n"; + if ($name === 'demo_func7') { + eval('function demo_func7() { return "dynamic"; }'); + } +}; +spl_autoload_register_function_loader($loader); +$f = 'demo_func7'; +var_dump($f()); +spl_autoload_unregister_function_loader($loader); + +echo "== ReflectionFunction ==\n"; +$loader = function (string $name) { + echo "loader($name)\n"; + if ($name === 'demo_func8') { + eval('function demo_func8() {}'); + } +}; +spl_autoload_register_function_loader($loader); +$rf = new ReflectionFunction('demo_func8'); +var_dump($rf->getName()); +try { + new ReflectionFunction('missing_func'); // loader consulted, then ReflectionException +} catch (\ReflectionException $e) { + echo $e->getMessage(), "\n"; +} +spl_autoload_unregister_function_loader($loader); +?> +--EXPECT-- +== function_exists == +loader(demo_func) +bool(true) +bool(true) +bool(true) +bool(false) +loader(missing_func) +bool(false) +== is_callable == +loader(demo_func2) +bool(true) +bool(true) +bool(true) +loader(missing_func) +bool(false) +== call_user_func == +loader(demo_func3) +string(5) "cuf:1" +loader(demo_func4) +string(8) "cufa:2:3" +== array callbacks == +loader(doubler) +2,4,6 +loader(odd_filter) +1,3 +loader(comparator) +1,2,3 +== callable param coercion == +loader(demo_func5) +string(4) "cp:5" +loader(missing_func) +TypeError +== Closure::fromCallable == +loader(demo_func6) +string(4) "fc:7" +TypeError +bool(true) +== dynamic call == +loader(demo_func7) +string(7) "dynamic" +== ReflectionFunction == +loader(demo_func8) +string(10) "demo_func8" +loader(missing_func) +Function missing_func() does not exist diff --git a/Zend/tests/autoload/function_autoload_manual_trigger.phpt b/Zend/tests/autoload/function_autoload_manual_trigger.phpt new file mode 100644 index 000000000000..ca4b63780135 --- /dev/null +++ b/Zend/tests/autoload/function_autoload_manual_trigger.phpt @@ -0,0 +1,41 @@ +--TEST-- +Function autoloading: spl_autoload_call_function_loader() manual trigger +--FILE-- + +--EXPECT-- +== triggers and re-triggers == +bool(false) +loader(demo_func) +bool(true) +string(6) "manual" +loader(demo_func) +== non-existent stays undefined == +loader(missing_func) +no exception +bool(false) diff --git a/Zend/tests/autoload/function_autoload_name_normalization.phpt b/Zend/tests/autoload/function_autoload_name_normalization.phpt new file mode 100644 index 000000000000..33bbc74a22a6 --- /dev/null +++ b/Zend/tests/autoload/function_autoload_name_normalization.phpt @@ -0,0 +1,84 @@ +--TEST-- +Function autoloading: name normalization and invalid names +--FILE-- +getMessage(), "\n"; +} +var_dump(function_exists('', true)); +var_dump(function_exists('foo bar', true)); +$g = 'demo_func'; +$g(); +spl_autoload_unregister_function_loader($loader); +?> +--EXPECT-- +== leading backslash (direct) == +loader(Ns\prefixed_func) +bool(true) +string(2) "ok" +== leading backslash (callable string) == +loader(Ns\bs_func) +bool(true) +string(2) "ok" +== case insensitive == +loader(My_Func) +string(6) "loaded" +string(6) "loaded" +== invalid names rejected == +Call to undefined function foo bar() +bool(false) +bool(false) +loader(demo_func) +demo_func called diff --git a/Zend/tests/autoload/function_autoload_namespace.phpt b/Zend/tests/autoload/function_autoload_namespace.phpt new file mode 100644 index 000000000000..9765417a452e --- /dev/null +++ b/Zend/tests/autoload/function_autoload_namespace.phpt @@ -0,0 +1,72 @@ +--TEST-- +Function autoloading: namespaced names and global fallback +--FILE-- +getMessage(), "\n"; + } + var_dump(global_func()); // global found via fallback: no re-consult + \spl_autoload_unregister_function_loader($loader); +} + +namespace App { + echo "== namespaced loader exception ==\n"; + // The unqualified call resolves to App\some_func; the loader's exception + // carries the namespaced name. + $loader = function (string $name) { + throw new \RuntimeException("Autoload failed for: $name"); + }; + \spl_autoload_register_function_loader($loader); + try { + some_func(); + } catch (\RuntimeException $e) { + echo $e->getMessage(), "\n"; + } + \spl_autoload_unregister_function_loader($loader); +} +?> +--EXPECT-- +== fully qualified == +loader(App\Util\helper) +string(9) "ns_helper" +== global fallback == +loader(App\local_func) +string(5) "local" +string(5) "local" +loader(App\global_func) +Call to undefined function App\global_func() +string(6) "global" +== namespaced loader exception == +Autoload failed for: App\some_func diff --git a/Zend/tests/autoload/function_autoload_no_pinning.phpt b/Zend/tests/autoload/function_autoload_no_pinning.phpt new file mode 100644 index 000000000000..bf086d27a520 --- /dev/null +++ b/Zend/tests/autoload/function_autoload_no_pinning.phpt @@ -0,0 +1,59 @@ +--TEST-- +Function autoloading does not pin lookup results (no observable leak into function_exists) +--FILE-- + +--EXPECT-- +bool(false) +int(5) +bool(false) +loader(Foo\strlen) +string(10) "foo-strlen" +bool(true) +loader consulted: 1 diff --git a/Zend/tests/autoload/function_autoload_recursion_mutation.phpt b/Zend/tests/autoload/function_autoload_recursion_mutation.phpt new file mode 100644 index 000000000000..c9f97ff41bcc --- /dev/null +++ b/Zend/tests/autoload/function_autoload_recursion_mutation.phpt @@ -0,0 +1,82 @@ +--TEST-- +Function autoloading: recursion guard and loader-list mutation +--FILE-- +getMessage() . "\n"; + } + } +}; +spl_autoload_register_function_loader($loader); +try { + recursive_func(); +} catch (Error $e) { + echo "Final: " . $e->getMessage() . "\n"; +} +spl_autoload_unregister_function_loader($loader); + +echo "== register during autoload ==\n"; +// A loader that registers another loader mid-pass: the newly added loader is +// consulted in the same pass and defines the function. +$second = function (string $name) { + echo "second: $name\n"; + if ($name === 'demo_func') { + eval('function demo_func() { return "ok"; }'); + } +}; +$first = function (string $name) use (&$second) { + echo "first: $name\n"; + spl_autoload_register_function_loader($second); +}; +spl_autoload_register_function_loader($first); +var_dump(demo_func()); +spl_autoload_unregister_function_loader($first); +spl_autoload_unregister_function_loader($second); + +echo "== unregister during autoload ==\n"; +// A loader that unregisters itself mid-pass skips the next loader on this pass; +// a later call reaches the surviving loader and succeeds. +$second = function (string $name) { + echo "second: $name\n"; + if ($name === 'demo_func2') { + eval('function demo_func2() { return "ok"; }'); + } +}; +$first = function (string $name) use (&$first) { + echo "first: $name\n"; + spl_autoload_unregister_function_loader($first); +}; +spl_autoload_register_function_loader($first); +spl_autoload_register_function_loader($second); +try { + demo_func2(); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump(count(spl_autoload_function_loaders())); +var_dump(demo_func2()); +spl_autoload_unregister_function_loader($second); +?> +--EXPECT-- +== recursion prevented == +loader(recursive_func) +Caught: Call to undefined function recursive_func() +Final: Call to undefined function recursive_func() +== register during autoload == +first: demo_func +second: demo_func +string(2) "ok" +== unregister during autoload == +first: demo_func2 +Call to undefined function demo_func2() +int(1) +second: demo_func2 +string(2) "ok" diff --git a/Zend/tests/autoload/function_autoload_registration.phpt b/Zend/tests/autoload/function_autoload_registration.phpt new file mode 100644 index 000000000000..a5380d9b99bf --- /dev/null +++ b/Zend/tests/autoload/function_autoload_registration.phpt @@ -0,0 +1,78 @@ +--TEST-- +Function autoloading: register, list, and unregister loaders +--FILE-- +getMessage(), "\n"; +} + +echo "== unregister false ==\n"; +$once = function (string $name) {}; +var_dump(spl_autoload_unregister_function_loader($once)); // never registered +spl_autoload_register_function_loader($once); +var_dump(spl_autoload_unregister_function_loader($once)); // removed +var_dump(spl_autoload_unregister_function_loader($once)); // already gone +?> +--EXPECT-- +== basic == +loader(demo_func) +string(10) "autoloaded" +string(10) "autoloaded" +== list == +array(0) { +} +int(2) +bool(true) +bool(true) +== duplicate == +int(1) +== unregister == +int(1) +int(0) +Call to undefined function missing_func() +== unregister false == +bool(false) +bool(true) +bool(false) diff --git a/Zend/tests/autoload/function_autoload_registry_independence.phpt b/Zend/tests/autoload/function_autoload_registry_independence.phpt new file mode 100644 index 000000000000..efd42973c0f0 --- /dev/null +++ b/Zend/tests/autoload/function_autoload_registry_independence.phpt @@ -0,0 +1,83 @@ +--TEST-- +Function autoloading: function and class loader registries are independent +--FILE-- + 1) { + throw new \TypeError("class autoloader received an unexpected extra argument"); + } + echo "class loader: $name\n"; + } +} + +$classLoader = function (string $name) { + echo "class loader: $name\n"; +}; +$funcLoader = function (string $name) { + echo "function loader: $name\n"; +}; +spl_autoload_register($classLoader); +spl_autoload_register_function_loader($funcLoader); + +echo "== invocation independence ==\n"; +// A class lookup consults only class loaders; a function lookup only function +// loaders. +class_exists('SomeClass'); +function_exists('some_func', true); + +echo "== management API separation ==\n"; +// Function and class autoloaders live in fully separate registries. +// Stas Malyshev's 2013 objection to a unified mechanism was that one callback +// list mixing different logic needs an ugly type switch. Each introspection, +// unregister, and manual-trigger entry point sees only its own registry. +var_dump(count(spl_autoload_functions())); +var_dump(count(spl_autoload_function_loaders())); +var_dump(in_array($classLoader, spl_autoload_functions(), true)); +var_dump(in_array($funcLoader, spl_autoload_function_loaders(), true)); +// A loader cannot be removed through the other registry's unregister function. +var_dump(spl_autoload_unregister($funcLoader)); +var_dump(spl_autoload_unregister_function_loader($classLoader)); +var_dump(count(spl_autoload_functions())); +var_dump(count(spl_autoload_function_loaders())); +// Manual triggering stays within its own registry. +spl_autoload_call('X'); +spl_autoload_call_function_loader('y'); +spl_autoload_unregister($classLoader); +spl_autoload_unregister_function_loader($funcLoader); + +echo "== class loader arg arity unchanged ==\n"; +// A class autoloader's argument arity is unchanged. The 2024 +// function_autoloading4 RFC passed a $type argument to existing class +// autoloaders, crashing Symfony's loader; a separate registry touches nothing, +// so a class autoloader still receives exactly one argument. +spl_autoload_register([SymfonyShapedLoader::class, 'load']); +spl_autoload_register_function_loader(function (string $name) { + if (func_num_args() > 1) { + throw new \TypeError("function loader received an unexpected extra argument"); + } + echo "function loader: $name\n"; +}); +class_exists('Some\Missing\ClassName'); +function_exists('some_missing_function', true); +echo "done\n"; +?> +--EXPECT-- +== invocation independence == +class loader: SomeClass +function loader: some_func +== management API separation == +int(1) +int(1) +bool(true) +bool(true) +bool(false) +bool(false) +int(1) +int(1) +class loader: X +function loader: y +== class loader arg arity unchanged == +class loader: Some\Missing\ClassName +function loader: some_missing_function +done diff --git a/Zend/tests/autoload/function_autoload_shadow_global.phpt b/Zend/tests/autoload/function_autoload_shadow_global.phpt new file mode 100644 index 000000000000..33484a96fa0c --- /dev/null +++ b/Zend/tests/autoload/function_autoload_shadow_global.phpt @@ -0,0 +1,35 @@ +--TEST-- +Unqualified call to an existing global function does not autoload a namespaced override +--FILE-- + +--EXPECT-- +int(5) +loader(App\strlen) +string(11) "shadowed(5)" +int(5) +string(11) "shadowed(5)" diff --git a/Zend/tests/autoload/function_autoload_use_function.phpt b/Zend/tests/autoload/function_autoload_use_function.phpt new file mode 100644 index 000000000000..fb641102f49b --- /dev/null +++ b/Zend/tests/autoload/function_autoload_use_function.phpt @@ -0,0 +1,22 @@ +--TEST-- +use function import triggers autoloading of the namespaced function +--FILE-- + +--EXPECT-- +loader(App\strlen) +string(11) "shadowed(5)" diff --git a/Zend/zend_API.c b/Zend/zend_API.c index e334b18fe1a4..5ff323a77e4d 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -3818,6 +3818,7 @@ static zend_always_inline bool zend_is_callable_check_func(const zval *callable, if (!ce_org) { zend_function *func; zend_string *lmname; + bool free_lmname = false; /* Check if function with given name exists. * This may be a compound name that includes namespace name */ @@ -3826,7 +3827,7 @@ static zend_always_inline bool zend_is_callable_check_func(const zval *callable, ZSTR_ALLOCA_ALLOC(lmname, Z_STRLEN_P(callable) - 1, use_heap); zend_str_tolower_copy(ZSTR_VAL(lmname), Z_STRVAL_P(callable) + 1, Z_STRLEN_P(callable) - 1); func = zend_fetch_function(lmname); - ZSTR_ALLOCA_FREE(lmname, use_heap); + free_lmname = true; } else { lmname = Z_STR_P(callable); func = zend_fetch_function(lmname); @@ -3834,9 +3835,18 @@ static zend_always_inline bool zend_is_callable_check_func(const zval *callable, ZSTR_ALLOCA_ALLOC(lmname, Z_STRLEN_P(callable), use_heap); zend_str_tolower_copy(ZSTR_VAL(lmname), Z_STRVAL_P(callable), Z_STRLEN_P(callable)); func = zend_fetch_function(lmname); - ZSTR_ALLOCA_FREE(lmname, use_heap); + free_lmname = true; } } + /* Not found: try autoloading, as the class lookup in + * zend_is_callable_check_class() does. zend_lookup_function() rejects + * invalid names, so "Class::method" strings fall through below. */ + if (!func) { + func = zend_lookup_function(Z_STR_P(callable), lmname); + } + if (free_lmname) { + ZSTR_ALLOCA_FREE(lmname, use_heap); + } if (EXPECTED(func != NULL)) { fcc->function_handler = func; return 1; diff --git a/Zend/zend_autoload.c b/Zend/zend_autoload.c index 2ca3d7eea022..f946b5dc2d83 100644 --- a/Zend/zend_autoload.c +++ b/Zend/zend_autoload.c @@ -24,6 +24,7 @@ #include "zend_string.h" ZEND_TLS HashTable *zend_class_autoload_functions; +ZEND_TLS HashTable *zend_function_autoload_functions; static void zend_autoload_callback_zval_destroy(zval *element) { @@ -146,6 +147,103 @@ ZEND_API void zend_autoload_fcc_map_to_callable_zval_map(zval *return_value) { RETURN_EMPTY_ARRAY(); } +ZEND_API zend_function *zend_perform_function_autoload(zend_string *function_name, zend_string *lc_name) +{ + if (!zend_function_autoload_functions) { + return NULL; + } + + zval zname; + ZVAL_STR(&zname, function_name); + + const HashTable *function_autoload_functions = zend_function_autoload_functions; + + /* Cannot use ZEND_HASH_MAP_FOREACH_PTR here as autoloaders may be + * added/removed during autoloading. */ + HashPosition pos; + zend_hash_internal_pointer_reset_ex(function_autoload_functions, &pos); + while (true) { + zend_fcall_info_cache *func_info = zend_hash_get_current_data_ptr_ex(function_autoload_functions, &pos); + if (!func_info) { + break; + } + zend_call_known_fcc(func_info, /* retval */ NULL, /* param_count */ 1, /* params */ &zname, /* named_params */ NULL); + + if (EG(exception)) { + return NULL; + } + + zend_function *fbc = zend_hash_find_ptr(EG(function_table), lc_name); + if (fbc) { + return fbc; + } + + zend_hash_move_forward_ex(function_autoload_functions, &pos); + } + return NULL; +} + +ZEND_API void zend_autoload_register_function_loader(zend_fcall_info_cache *fcc, bool prepend) +{ + ZEND_ASSERT(ZEND_FCC_INITIALIZED(*fcc)); + + if (!zend_function_autoload_functions) { + ALLOC_HASHTABLE(zend_function_autoload_functions); + zend_hash_init(zend_function_autoload_functions, 1, NULL, zend_autoload_callback_zval_destroy, false); + /* Initialize as non-packed hash table for prepend functionality. */ + zend_hash_real_init_mixed(zend_function_autoload_functions); + } + + ZEND_ASSERT( + fcc->function_handler->type != ZEND_INTERNAL_FUNCTION + || !zend_string_equals_literal(fcc->function_handler->common.function_name, "spl_autoload_call_function_loader") + ); + + /* If function is already registered, don't do anything */ + if (autoload_find_registered_function(zend_function_autoload_functions, fcc)) { + /* Release potential call trampoline */ + zend_release_fcall_info_cache(fcc); + return; + } + + zend_fcc_addref(fcc); + zend_hash_next_index_insert_mem(zend_function_autoload_functions, fcc, sizeof(zend_fcall_info_cache)); + if (prepend && zend_hash_num_elements(zend_function_autoload_functions) > 1) { + /* Move the newly created element to the head of the hashtable */ + ZEND_ASSERT(!HT_IS_PACKED(zend_function_autoload_functions)); + Bucket tmp = zend_function_autoload_functions->arData[zend_function_autoload_functions->nNumUsed-1]; + memmove(zend_function_autoload_functions->arData + 1, zend_function_autoload_functions->arData, sizeof(Bucket) * (zend_function_autoload_functions->nNumUsed - 1)); + zend_function_autoload_functions->arData[0] = tmp; + zend_hash_rehash(zend_function_autoload_functions); + } +} + +ZEND_API bool zend_autoload_unregister_function_loader(const zend_fcall_info_cache *fcc) { + if (zend_function_autoload_functions) { + Bucket *p = autoload_find_registered_function(zend_function_autoload_functions, fcc); + if (p) { + zend_hash_del_bucket(zend_function_autoload_functions, p); + return true; + } + } + return false; +} + +ZEND_API void zend_autoload_function_fcc_map_to_callable_zval_map(zval *return_value) { + if (zend_function_autoload_functions) { + zend_fcall_info_cache *fcc; + + zend_array *map = zend_new_array(zend_hash_num_elements(zend_function_autoload_functions)); + ZEND_HASH_MAP_FOREACH_PTR(zend_function_autoload_functions, fcc) { + zval tmp; + zend_get_callable_zval_from_fcc(fcc, &tmp); + zend_hash_next_index_insert(map, &tmp); + } ZEND_HASH_FOREACH_END(); + RETURN_ARR(map); + } + RETURN_EMPTY_ARRAY(); +} + /* Only for deprecated strange behaviour of spl_autoload_unregister() */ ZEND_API void zend_autoload_clean_class_loaders(void) { @@ -162,4 +260,9 @@ void zend_autoload_shutdown(void) FREE_HASHTABLE(zend_class_autoload_functions); zend_class_autoload_functions = NULL; } + if (zend_function_autoload_functions) { + zend_hash_destroy(zend_function_autoload_functions); + FREE_HASHTABLE(zend_function_autoload_functions); + zend_function_autoload_functions = NULL; + } } diff --git a/Zend/zend_autoload.h b/Zend/zend_autoload.h index 84e6ab80b5af..17a949a8ebbe 100644 --- a/Zend/zend_autoload.h +++ b/Zend/zend_autoload.h @@ -29,6 +29,12 @@ ZEND_API bool zend_autoload_unregister_class_loader(const zend_fcall_info_cache ZEND_API void zend_autoload_fcc_map_to_callable_zval_map(zval *return_value); /* Only for deprecated strange behaviour of spl_autoload_unregister() */ ZEND_API void zend_autoload_clean_class_loaders(void); + +ZEND_API zend_function *zend_perform_function_autoload(zend_string *function_name, zend_string *lc_name); +ZEND_API void zend_autoload_register_function_loader(zend_fcall_info_cache *fcc, bool prepend); +ZEND_API bool zend_autoload_unregister_function_loader(const zend_fcall_info_cache *fcc); +ZEND_API void zend_autoload_function_fcc_map_to_callable_zval_map(zval *return_value); + void zend_autoload_shutdown(void); #endif diff --git a/Zend/zend_builtin_functions.c b/Zend/zend_builtin_functions.c index 2dceac2512db..208b1658441e 100644 --- a/Zend/zend_builtin_functions.c +++ b/Zend/zend_builtin_functions.c @@ -37,6 +37,7 @@ ZEND_MINIT_FUNCTION(core) { /* {{{ */ zend_autoload = zend_perform_class_autoload; + zend_function_autoload = zend_perform_function_autoload; zend_register_default_classes(); zend_standard_class_def = register_class_stdClass(); @@ -1174,11 +1175,14 @@ ZEND_FUNCTION(enum_exists) ZEND_FUNCTION(function_exists) { zend_string *name; + bool autoload = true; bool exists; zend_string *lcname; - ZEND_PARSE_PARAMETERS_START(1, 1) + ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_STR(name) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(autoload) ZEND_PARSE_PARAMETERS_END(); if (ZSTR_VAL(name)[0] == '\\') { @@ -1190,6 +1194,12 @@ ZEND_FUNCTION(function_exists) } exists = zend_hash_exists(EG(function_table), lcname); + + if (!exists && autoload) { + zend_function *fbc = zend_lookup_function(name, lcname); + exists = (fbc != NULL); + } + zend_string_release_ex(lcname, 0); RETURN_BOOL(exists); diff --git a/Zend/zend_builtin_functions.stub.php b/Zend/zend_builtin_functions.stub.php index 1d405587145d..58509d5adb74 100644 --- a/Zend/zend_builtin_functions.stub.php +++ b/Zend/zend_builtin_functions.stub.php @@ -99,7 +99,7 @@ function trait_exists(string $trait, bool $autoload = true): bool {} function enum_exists(string $enum, bool $autoload = true): bool {} -function function_exists(string $function): bool {} +function function_exists(string $function, bool $autoload = true): bool {} function class_alias(string $class, string $alias, bool $autoload = true): bool {} diff --git a/Zend/zend_builtin_functions_arginfo.h b/Zend/zend_builtin_functions_arginfo.h index b3af43fef340..85c4c522a997 100644 --- a/Zend/zend_builtin_functions_arginfo.h +++ b/Zend/zend_builtin_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit zend_builtin_functions.stub.php instead. - * Stub hash: 64c61862de86d9968930893bf21b516119724064 */ + * Stub hash: b1c327918df321b439573fdfb241a0617eaf0e30 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_clone, 0, 1, IS_OBJECT, 0) ZEND_ARG_TYPE_INFO(0, object, IS_OBJECT, 0) @@ -126,6 +126,7 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_function_exists, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, function, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, autoload, _IS_BOOL, 0, "true") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_alias, 0, 2, _IS_BOOL, 0) diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 1b28ce25fe37..9f9f6c3d38ce 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -5154,15 +5154,21 @@ static zend_never_inline zend_execute_data *zend_init_dynamic_call_string(zend_s lcname = zend_string_tolower(function); } if (UNEXPECTED((func = zend_hash_find(EG(function_table), lcname)) == NULL)) { - zend_throw_error(NULL, "Call to undefined function %s()", ZSTR_VAL(function)); + fbc = zend_lookup_function(function, lcname); + if (UNEXPECTED(fbc == NULL)) { + if (!EG(exception)) { + zend_throw_error(NULL, "Call to undefined function %s()", ZSTR_VAL(function)); + } + zend_string_release_ex(lcname, 0); + return NULL; + } zend_string_release_ex(lcname, 0); - return NULL; - } - zend_string_release_ex(lcname, 0); - - fbc = Z_FUNC_P(func); - if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache(&fbc->op_array); + } else { + zend_string_release_ex(lcname, 0); + fbc = Z_FUNC_P(func); + if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache(&fbc->op_array); + } } called_scope = NULL; } diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index ba48b19bcfe1..1581cdd55a52 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -36,6 +36,9 @@ ZEND_API extern void (*zend_execute_internal)(zend_execute_data *execute_data, z /* The lc_name may be stack allocated! */ ZEND_API extern zend_class_entry *(*zend_autoload)(zend_string *name, zend_string *lc_name); +/* The lc_name may be stack allocated! */ +ZEND_API extern zend_function *(*zend_function_autoload)(zend_string *name, zend_string *lc_name); + void init_executor(void); void shutdown_executor(void); void shutdown_destructors(void); @@ -48,8 +51,16 @@ ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value); ZEND_API void execute_ex(zend_execute_data *execute_data); ZEND_API void execute_internal(zend_execute_data *execute_data, zval *return_value); ZEND_API bool zend_is_valid_class_name(const zend_string *name); + +/* Function names use the same character set as class names. */ +static zend_always_inline bool zend_is_valid_function_name(const zend_string *name) +{ + return zend_is_valid_class_name(name); +} + ZEND_API zend_class_entry *zend_lookup_class(zend_string *name); ZEND_API zend_class_entry *zend_lookup_class_ex(zend_string *name, zend_string *lcname, uint32_t flags); +ZEND_API zend_function *zend_lookup_function(zend_string *name, zend_string *lc_name); ZEND_API zend_class_entry *zend_get_called_scope(const zend_execute_data *ex); ZEND_API zend_object *zend_get_this_object(const zend_execute_data *ex); ZEND_API zend_result zend_eval_string(const char *str, zval *retval_ptr, const char *string_name); diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 71e0c56a51c8..f45424400b5a 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -51,6 +51,7 @@ ZEND_API void (*zend_execute_ex)(zend_execute_data *execute_data); ZEND_API void (*zend_execute_internal)(zend_execute_data *execute_data, zval *return_value); ZEND_API zend_class_entry *(*zend_autoload)(zend_string *name, zend_string *lc_name); +ZEND_API zend_function *(*zend_function_autoload)(zend_string *name, zend_string *lc_name); #ifdef ZEND_WIN32 ZEND_TLS HANDLE tq_timer = NULL; @@ -155,6 +156,7 @@ void init_executor(void) /* {{{ */ zend_hash_init(&EG(included_files), 8, NULL, NULL, 0); zend_hash_init(&EG(autoload_current_classnames), 8, NULL, NULL, 0); + zend_hash_init(&EG(autoload_current_functionnames), 8, NULL, NULL, 0); EG(ticks_count) = 0; @@ -502,6 +504,7 @@ void shutdown_executor(void) /* {{{ */ zend_hash_destroy(&EG(included_files)); zend_hash_destroy(&EG(autoload_current_classnames)); + zend_hash_destroy(&EG(autoload_current_functionnames)); zend_stack_destroy(&EG(user_error_handlers_error_reporting)); zend_stack_destroy(&EG(user_error_handlers)); @@ -1299,6 +1302,60 @@ ZEND_API zend_class_entry *zend_lookup_class(zend_string *name) /* {{{ */ } /* }}} */ +ZEND_API zend_function *zend_lookup_function(zend_string *name, zend_string *lc_name) /* {{{ */ +{ + zval *func = zend_hash_find(EG(function_table), lc_name); + if (func) { + zend_function *fbc = Z_FUNC_P(func); + if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + zend_init_func_run_time_cache(&fbc->op_array); + } + return fbc; + } + + /* The compiler is not-reentrant. Make sure we autoload only during run-time. */ + if (zend_is_compiling()) { + return NULL; + } + + if (!zend_function_autoload) { + return NULL; + } + + /* Verify function name before passing it to the autoloader. */ + if (!ZSTR_LEN(name) || !zend_is_valid_function_name(name)) { + return NULL; + } + + if (zend_hash_add_empty_element(&EG(autoload_current_functionnames), lc_name) == NULL) { + return NULL; + } + + zend_string *autoload_name; + if (ZSTR_VAL(name)[0] == '\\') { + autoload_name = zend_string_init(ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 1, 0); + } else { + autoload_name = zend_string_copy(name); + } + + zend_string *previous_filename = EG(filename_override); + zend_long previous_lineno = EG(lineno_override); + EG(filename_override) = NULL; + EG(lineno_override) = -1; + zend_function *fbc = zend_function_autoload(autoload_name, lc_name); + EG(filename_override) = previous_filename; + EG(lineno_override) = previous_lineno; + + zend_string_release_ex(autoload_name, 0); + zend_hash_del(&EG(autoload_current_functionnames), lc_name); + + if (fbc && EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + zend_init_func_run_time_cache(&fbc->op_array); + } + return fbc; +} +/* }}} */ + ZEND_API zend_class_entry *zend_get_called_scope(const zend_execute_data *ex) /* {{{ */ { while (ex) { diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 8257df32e831..d47adf014225 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -227,6 +227,7 @@ struct _zend_executor_globals { zend_atomic_bool timed_out; HashTable autoload_current_classnames; + HashTable autoload_current_functionnames; zend_long hard_timeout; void *stack_base; diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 1de7a7cd4195..6760c3568e0e 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -3903,11 +3903,19 @@ ZEND_VM_HOT_HANDLER(59, ZEND_INIT_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) function_name = (zval*)RT_CONSTANT(opline, opline->op2); func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(function_name+1)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_DISPATCH_TO_HELPER(zend_undefined_function_helper); - } - fbc = Z_FUNC_P(func); - if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache(&fbc->op_array); + SAVE_OPLINE(); + fbc = zend_lookup_function(Z_STR_P(function_name), Z_STR_P(function_name+1)); + if (UNEXPECTED(fbc == NULL)) { + if (EG(exception)) { + HANDLE_EXCEPTION(); + } + ZEND_VM_DISPATCH_TO_HELPER(zend_undefined_function_helper); + } + } else { + fbc = Z_FUNC_P(func); + if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache(&fbc->op_array); + } } CACHE_PTR(opline->result.num, fbc); } @@ -4059,7 +4067,17 @@ ZEND_VM_HOT_HANDLER(69, ZEND_INIT_NS_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) if (func == NULL) { func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(func_name + 2)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_DISPATCH_TO_HELPER(zend_undefined_function_helper); + /* Autoload with the fully qualified name */ + SAVE_OPLINE(); + fbc = zend_lookup_function(Z_STR_P(func_name), Z_STR_P(func_name + 1)); + if (UNEXPECTED(fbc == NULL)) { + if (EG(exception)) { + HANDLE_EXCEPTION(); + } + ZEND_VM_DISPATCH_TO_HELPER(zend_undefined_function_helper); + } + CACHE_PTR(opline->result.num, fbc); + ZEND_VM_C_GOTO(ns_fcall_init); } } fbc = Z_FUNC_P(func); @@ -4069,6 +4087,7 @@ ZEND_VM_HOT_HANDLER(69, ZEND_INIT_NS_FCALL_BY_NAME, ANY, CONST, NUM|CACHE_SLOT) CACHE_PTR(opline->result.num, fbc); } +ZEND_VM_C_LABEL(ns_fcall_init): call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 5b52f1941845..d842cef22b1a 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -4072,11 +4072,19 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I function_name = (zval*)RT_CONSTANT(opline, opline->op2); func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(function_name+1)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); - } - fbc = Z_FUNC_P(func); - if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache(&fbc->op_array); + SAVE_OPLINE(); + fbc = zend_lookup_function(Z_STR_P(function_name), Z_STR_P(function_name+1)); + if (UNEXPECTED(fbc == NULL)) { + if (EG(exception)) { + HANDLE_EXCEPTION(); + } + ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + } + } else { + fbc = Z_FUNC_P(func); + if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache(&fbc->op_array); + } } CACHE_PTR(opline->result.num, fbc); } @@ -4157,7 +4165,17 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I if (func == NULL) { func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(func_name + 2)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + /* Autoload with the fully qualified name */ + SAVE_OPLINE(); + fbc = zend_lookup_function(Z_STR_P(func_name), Z_STR_P(func_name + 1)); + if (UNEXPECTED(fbc == NULL)) { + if (EG(exception)) { + HANDLE_EXCEPTION(); + } + ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + } + CACHE_PTR(opline->result.num, fbc); + goto ns_fcall_init; } } fbc = Z_FUNC_P(func); @@ -4167,6 +4185,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_I CACHE_PTR(opline->result.num, fbc); } +ns_fcall_init: call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); @@ -56746,11 +56765,19 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_F function_name = (zval*)RT_CONSTANT(opline, opline->op2); func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(function_name+1)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); - } - fbc = Z_FUNC_P(func); - if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache(&fbc->op_array); + SAVE_OPLINE(); + fbc = zend_lookup_function(Z_STR_P(function_name), Z_STR_P(function_name+1)); + if (UNEXPECTED(fbc == NULL)) { + if (EG(exception)) { + HANDLE_EXCEPTION(); + } + ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + } + } else { + fbc = Z_FUNC_P(func); + if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache(&fbc->op_array); + } } CACHE_PTR(opline->result.num, fbc); } @@ -56831,7 +56858,17 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_N if (func == NULL) { func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(func_name + 2)); if (UNEXPECTED(func == NULL)) { - ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + /* Autoload with the fully qualified name */ + SAVE_OPLINE(); + fbc = zend_lookup_function(Z_STR_P(func_name), Z_STR_P(func_name + 1)); + if (UNEXPECTED(fbc == NULL)) { + if (EG(exception)) { + HANDLE_EXCEPTION(); + } + ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + } + CACHE_PTR(opline->result.num, fbc); + goto ns_fcall_init; } } fbc = Z_FUNC_P(func); @@ -56841,6 +56878,7 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_N CACHE_PTR(opline->result.num, fbc); } +ns_fcall_init: call = _zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_FUNCTION, fbc, opline->extended_value, NULL); call->prev_execute_data = EX(call); diff --git a/ext/opcache/jit/zend_jit_helpers.c b/ext/opcache/jit/zend_jit_helpers.c index 64a48068f378..a5b25cab9945 100644 --- a/ext/opcache/jit/zend_jit_helpers.c +++ b/ext/opcache/jit/zend_jit_helpers.c @@ -51,9 +51,30 @@ static zend_never_inline zend_op_array* ZEND_FASTCALL zend_jit_init_func_run_tim } /* }}} */ -static zend_function* ZEND_FASTCALL zend_jit_find_func_helper(zend_string *name, void **cache_slot) +static zend_function* ZEND_FASTCALL zend_jit_find_func_helper(zend_string *name, zend_string *lc_name, void **cache_slot) { - zval *func = zend_hash_find_known_hash(EG(function_table), name); + zval *func = zend_hash_find_known_hash(EG(function_table), lc_name); + zend_function *fbc; + + if (UNEXPECTED(func == NULL)) { + fbc = zend_lookup_function(name, lc_name); + if (fbc == NULL) { + return NULL; + } + } else { + fbc = Z_FUNC_P(func); + if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + fbc = _zend_jit_init_func_run_time_cache(&fbc->op_array); + } + } + *cache_slot = fbc; + return fbc; +} + +/* ZEND_INIT_FCALL: the function existed at compile time, so don't autoload on a runtime miss. */ +static zend_function* ZEND_FASTCALL zend_jit_find_known_func_helper(zend_string *lc_name, void **cache_slot) +{ + zval *func = zend_hash_find_known_hash(EG(function_table), lc_name); zend_function *fbc; if (UNEXPECTED(func == NULL)) { @@ -83,7 +104,13 @@ static zend_function* ZEND_FASTCALL zend_jit_find_ns_func_helper(zval *func_name if (func == NULL) { func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(func_name + 2)); if (UNEXPECTED(func == NULL)) { - return NULL; + /* Autoload with the fully qualified name */ + fbc = zend_lookup_function(Z_STR_P(func_name), Z_STR_P(func_name + 1)); + if (fbc == NULL) { + return NULL; + } + *cache_slot = fbc; + return fbc; } } fbc = Z_FUNC_P(func); diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index cf43d3ad840f..7838c3c0eb55 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -2164,6 +2164,13 @@ static int zend_jit_invalid_this_stub(zend_jit_ctx *jit) static int zend_jit_undefined_function_stub(zend_jit_ctx *jit) { + // JIT: if (EG(exception)) goto exception_handler; + ir_ref exception_ref = ir_LOAD_A(jit_EG_exception(jit)); + ir_ref if_exception = ir_IF(exception_ref); + ir_IF_TRUE(if_exception); + ir_IJMP(jit_STUB_ADDR(jit, jit_stub_exception_handler)); + ir_IF_FALSE(if_exception); + // JIT: load EX(opline) ir_ref ref = ir_LOAD_A(jit_FP(jit)); ir_ref arg3 = ir_LOAD_U32(ir_ADD_OFFSET(ref, offsetof(zend_op, op2.constant))); @@ -3108,6 +3115,7 @@ static void zend_jit_setup_disasm(void) REGISTER_HELPER(zend_jit_extend_stack_helper); REGISTER_HELPER(zend_jit_init_func_run_time_cache_helper); REGISTER_HELPER(zend_jit_find_func_helper); + REGISTER_HELPER(zend_jit_find_known_func_helper); REGISTER_HELPER(zend_jit_find_ns_func_helper); REGISTER_HELPER(zend_jit_jmp_frameless_helper); REGISTER_HELPER(zend_jit_unref_helper); @@ -8887,12 +8895,16 @@ static int zend_jit_init_fcall(zend_jit_ctx *jit, const zend_op *opline, uint32_ } else { zval *zv = RT_CONSTANT(opline, opline->op2); + /* The helpers may invoke a function autoloader, which runs user code. */ + jit_SET_EX_OPLINE(jit, opline); + if (opline->opcode == ZEND_INIT_FCALL) { - ref = ir_CALL_2(IR_ADDR, ir_CONST_FC_FUNC(zend_jit_find_func_helper), + ref = ir_CALL_2(IR_ADDR, ir_CONST_FC_FUNC(zend_jit_find_known_func_helper), ir_CONST_ADDR(Z_STR_P(zv)), cache_slot_ref); } else if (opline->opcode == ZEND_INIT_FCALL_BY_NAME) { - ref = ir_CALL_2(IR_ADDR, ir_CONST_FC_FUNC(zend_jit_find_func_helper), + ref = ir_CALL_3(IR_ADDR, ir_CONST_FC_FUNC(zend_jit_find_func_helper), + ir_CONST_ADDR(Z_STR_P(zv)), ir_CONST_ADDR(Z_STR_P(zv + 1)), cache_slot_ref); } else if (opline->opcode == ZEND_INIT_NS_FCALL_BY_NAME) { @@ -8916,7 +8928,6 @@ static int zend_jit_init_fcall(zend_jit_ctx *jit, const zend_op *opline, uint32_ return 0; } } else { -jit_SET_EX_OPLINE(jit, opline); ir_GUARD(ref, jit_STUB_ADDR(jit, jit_stub_undefined_function)); } } diff --git a/ext/opcache/tests/jit/function_autoload_001.phpt b/ext/opcache/tests/jit/function_autoload_001.phpt new file mode 100644 index 000000000000..32abc95f98a7 --- /dev/null +++ b/ext/opcache/tests/jit/function_autoload_001.phpt @@ -0,0 +1,29 @@ +--TEST-- +JIT INIT_FCALL_BY_NAME: function autoloading preserves the original case +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit=function +opcache.jit_buffer_size=16M +--EXTENSIONS-- +opcache +--FILE-- + +--EXPECT-- +loader(Jit_Func) +string(6) "jit_ok" +string(6) "jit_ok" diff --git a/ext/opcache/tests/jit/function_autoload_002.phpt b/ext/opcache/tests/jit/function_autoload_002.phpt new file mode 100644 index 000000000000..a524c2b732ac --- /dev/null +++ b/ext/opcache/tests/jit/function_autoload_002.phpt @@ -0,0 +1,36 @@ +--TEST-- +JIT INIT_FCALL_BY_NAME: exception thrown by the function autoloader +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit=function +opcache.jit_buffer_size=16M +--EXTENSIONS-- +opcache +--FILE-- +getMessage(), "\n"; + var_dump($e->getTrace()[0]['line']); +} +try { + test(); +} catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Autoload failed for: missing_func +int(7) +Autoload failed for: missing_func diff --git a/ext/opcache/tests/jit/function_autoload_003.phpt b/ext/opcache/tests/jit/function_autoload_003.phpt new file mode 100644 index 000000000000..b844ba9bf817 --- /dev/null +++ b/ext/opcache/tests/jit/function_autoload_003.phpt @@ -0,0 +1,31 @@ +--TEST-- +JIT INIT_NS_FCALL_BY_NAME: function autoloading with namespace fallback +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit=function +opcache.jit_buffer_size=16M +--EXTENSIONS-- +opcache +--FILE-- + +--EXPECT-- +loader(App\ns_func) +string(5) "ns_ok" +string(5) "ns_ok" diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index eff61659d078..9d30d52313cc 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -1731,11 +1731,11 @@ ZEND_METHOD(ReflectionFunction, __construct) ALLOCA_FLAG(use_heap) ZSTR_ALLOCA_ALLOC(lcname, ZSTR_LEN(fname) - 1, use_heap); zend_str_tolower_copy(ZSTR_VAL(lcname), ZSTR_VAL(fname) + 1, ZSTR_LEN(fname) - 1); - fptr = zend_fetch_function(lcname); + fptr = zend_lookup_function(fname, lcname); ZSTR_ALLOCA_FREE(lcname, use_heap); } else { lcname = zend_string_tolower(fname); - fptr = zend_fetch_function(lcname); + fptr = zend_lookup_function(fname, lcname); zend_string_release(lcname); } diff --git a/ext/spl/php_spl.c b/ext/spl/php_spl.c index 0610e79196f9..2ef0fe39e19c 100644 --- a/ext/spl/php_spl.c +++ b/ext/spl/php_spl.c @@ -470,6 +470,73 @@ PHP_FUNCTION(spl_autoload_functions) zend_autoload_fcc_map_to_callable_zval_map(return_value); } /* }}} */ +/* {{{ Register given function as a function autoloader */ +PHP_FUNCTION(spl_autoload_register_function_loader) +{ + zend_fcall_info fci; + zend_fcall_info_cache fcc; + bool prepend = 0; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_FUNC(fci, fcc) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(prepend) + ZEND_PARSE_PARAMETERS_END(); + + if (!ZEND_FCC_INITIALIZED(fcc)) { + /* Call trampoline has been cleared by zpp. Refetch it, because we want to deal + * with it ourselves. It is important that it is not refetched on every call, + * because calls may occur from different scopes. */ + zend_is_callable_ex(&fci.function_name, NULL, IS_CALLABLE_SUPPRESS_DEPRECATIONS, NULL, &fcc, NULL); + } + + if (fcc.function_handler->type == ZEND_INTERNAL_FUNCTION && + fcc.function_handler->internal_function.handler == zif_spl_autoload_call_function_loader) { + zend_argument_value_error(1, "must not be the spl_autoload_call_function_loader() function"); + RETURN_THROWS(); + } + + zend_autoload_register_function_loader(&fcc, prepend); + RETURN_TRUE; +} /* }}} */ + +/* {{{ Unregister given function as a function autoloader */ +PHP_FUNCTION(spl_autoload_unregister_function_loader) +{ + zend_fcall_info fci; + zend_fcall_info_cache fcc; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_FUNC_NO_TRAMPOLINE_FREE(fci, fcc) + ZEND_PARSE_PARAMETERS_END(); + + RETVAL_BOOL(zend_autoload_unregister_function_loader(&fcc)); + /* Release trampoline */ + zend_release_fcall_info_cache(&fcc); +} /* }}} */ + +/* {{{ Return all registered function autoloader functions */ +PHP_FUNCTION(spl_autoload_function_loaders) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + zend_autoload_function_fcc_map_to_callable_zval_map(return_value); +} /* }}} */ + +/* {{{ Try all registered function autoloaders to load the requested function */ +PHP_FUNCTION(spl_autoload_call_function_loader) +{ + zend_string *function_name; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(function_name) + ZEND_PARSE_PARAMETERS_END(); + + zend_string *lc_name = zend_string_tolower(function_name); + zend_perform_function_autoload(function_name, lc_name); + zend_string_release(lc_name); +} /* }}} */ + /* {{{ Return hash id for given object */ PHP_FUNCTION(spl_object_hash) { diff --git a/ext/spl/php_spl.stub.php b/ext/spl/php_spl.stub.php index d3b5d44f11d1..4f92dcfddca8 100644 --- a/ext/spl/php_spl.stub.php +++ b/ext/spl/php_spl.stub.php @@ -35,6 +35,14 @@ function spl_autoload_register(?callable $callback = null, bool $throw = true, b function spl_autoload_unregister(callable $callback): bool {} +function spl_autoload_register_function_loader(callable $callback, bool $prepend = false): bool {} + +function spl_autoload_unregister_function_loader(callable $callback): bool {} + +function spl_autoload_function_loaders(): array {} + +function spl_autoload_call_function_loader(string $function_name): void {} + /** * @return array * @refcount 1 diff --git a/ext/spl/php_spl_arginfo.h b/ext/spl/php_spl_arginfo.h index 8b0ea4b7245b..3687afd2844f 100644 --- a/ext/spl/php_spl_arginfo.h +++ b/ext/spl/php_spl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit php_spl.stub.php instead. - * Stub hash: 21ec2dcca99c85c90afcd319da76016a9f678dc2 */ + * Stub hash: cc3c674e48e9fb4cf658dcb9603e78076bfdd56a */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_implements, 0, 1, MAY_BE_ARRAY|MAY_BE_FALSE) ZEND_ARG_INFO(0, object_or_class) @@ -36,6 +36,19 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_spl_autoload_unregister, 0, 1, _ ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_spl_autoload_register_function_loader, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, prepend, _IS_BOOL, 0, "false") +ZEND_END_ARG_INFO() + +#define arginfo_spl_autoload_unregister_function_loader arginfo_spl_autoload_unregister + +#define arginfo_spl_autoload_function_loaders arginfo_spl_autoload_functions + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_spl_autoload_call_function_loader, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, function_name, IS_STRING, 0) +ZEND_END_ARG_INFO() + #define arginfo_spl_classes arginfo_spl_autoload_functions ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_spl_object_hash, 0, 1, IS_STRING, 0) @@ -70,6 +83,10 @@ ZEND_FUNCTION(spl_autoload_extensions); ZEND_FUNCTION(spl_autoload_functions); ZEND_FUNCTION(spl_autoload_register); ZEND_FUNCTION(spl_autoload_unregister); +ZEND_FUNCTION(spl_autoload_register_function_loader); +ZEND_FUNCTION(spl_autoload_unregister_function_loader); +ZEND_FUNCTION(spl_autoload_function_loaders); +ZEND_FUNCTION(spl_autoload_call_function_loader); ZEND_FUNCTION(spl_classes); ZEND_FUNCTION(spl_object_hash); ZEND_FUNCTION(spl_object_id); @@ -87,6 +104,10 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(spl_autoload_functions, arginfo_spl_autoload_functions) ZEND_FE(spl_autoload_register, arginfo_spl_autoload_register) ZEND_FE(spl_autoload_unregister, arginfo_spl_autoload_unregister) + ZEND_FE(spl_autoload_register_function_loader, arginfo_spl_autoload_register_function_loader) + ZEND_FE(spl_autoload_unregister_function_loader, arginfo_spl_autoload_unregister_function_loader) + ZEND_FE(spl_autoload_function_loaders, arginfo_spl_autoload_function_loaders) + ZEND_FE(spl_autoload_call_function_loader, arginfo_spl_autoload_call_function_loader) ZEND_FE(spl_classes, arginfo_spl_classes) ZEND_FE(spl_object_hash, arginfo_spl_object_hash) ZEND_FE(spl_object_id, arginfo_spl_object_id) diff --git a/ext/spl/tests/autoloading/spl_autoload_register_function_loader_rejects_call_function_loader.phpt b/ext/spl/tests/autoloading/spl_autoload_register_function_loader_rejects_call_function_loader.phpt new file mode 100644 index 000000000000..a85218dcdee1 --- /dev/null +++ b/ext/spl/tests/autoloading/spl_autoload_register_function_loader_rejects_call_function_loader.phpt @@ -0,0 +1,12 @@ +--TEST-- +spl_autoload_register_function_loader() rejects spl_autoload_call_function_loader as callback +--FILE-- +getMessage(), "\n"; +} +?> +--EXPECT-- +spl_autoload_register_function_loader(): Argument #1 ($callback) must not be the spl_autoload_call_function_loader() function