perf: add request-local dynamic call caches

master
韩天峰 3 days ago
parent 1c16639418
commit 683a7c6ec0
  1. 2
      benchmark/README.md
  2. 3
      benchmark/dynamic-call/.gitignore
  3. 28
      benchmark/dynamic-call/README.md
  4. 284
      benchmark/dynamic-call/benchmark.php
  5. 8
      benchmark/dynamic-call/project.yml
  6. 162
      benchmark/dynamic-call/run.php
  7. 28
      phpunit/code/call-cache-sites.php
  8. 34
      phpunit/src/CallCacheCodegenTest.php
  9. 4
      phpunit/src/CompilerBaseApiTest.php
  10. 2
      phpunit/src/Python/PythonModuleTest.php
  11. 16
      src/CompilerBase.php
  12. 7
      src/Parser/FunctionCallTrait.php
  13. 8
      src/Parser/MethodCallTrait.php
  14. 18
      src/Translator.php
  15. 85
      tests/compiler/dynamic_call/call-cache-dispatch.phpt
  16. 30
      tests/compiler/native-class/shutdown-finalizer-dynamic-call.phpt

@ -9,6 +9,8 @@ machine instead of committing absolute timing expectations.
from `examples/`.
- `bridge/` measures calls, property operations, and container operations that
cross the generated-code/PHPX/Zend boundary.
- `dynamic-call/` measures monomorphic and polymorphic runtime callables so
call-site cache changes can be evaluated independently from direct AOT calls.
- `property-access/` builds and compares dynamic/static property access under
Zend PHP and TypePHP.

@ -0,0 +1,3 @@
/build/
/dynamic_call
/*.rsp

@ -0,0 +1,28 @@
# Dynamic call benchmark
This benchmark compares direct calls with the runtime callable forms handled
by PHPX. It deliberately separates stable call sites from alternating and
megamorphic sites. TypePHP's function-call cache keeps one name inline and
promotes polymorphic sites to a request-local table. Its method-call cache is
monomorphic and disables itself after observing a different class or name.
It also measures dynamic method names with a stable receiver, alternating
method names, and a fixed method name on changing receiver classes. Those
cases require a class-entry guard in addition to a callable-name guard.
Run it from the repository root against a release PHP/PHPX build:
```bash
PHPX_HOME=../phpx PHP_BIN=/opt/php-8.5-nts/bin/php php benchmark/dynamic-call/run.php
```
The TypePHP binary is built with `-O3` and LTO. `PHP_BIN` selects both the Zend
PHP baseline and, by default, the PHP executable used to run the compiler.
`TPC_PHP_BIN` may override the latter, but the runner rejects different PHP
versions, ZTS/debug modes, or integer widths. PHPX must also be a Release build
for that same PHP ABI. Add `--skip-build` to reuse the binary or
`--case=<name>` to measure one workload while profiling.
Reported values are the best of five rounds after two warm-up rounds. The
checksum must match between PHP and TypePHP; absolute timing is intentionally
not used as a pass/fail condition.

@ -0,0 +1,284 @@
<?php
declare(strict_types=1);
const DYNAMIC_CALL_ITERATIONS = 1_000_000;
const DYNAMIC_CALL_ROUNDS = 5;
function dynamicCallAddOne(int $value): int
{
return $value + 1;
}
function dynamicCallAddTwo(int $value): int
{
return $value + 2;
}
function dynamicCallAddThree(int $value): int
{
return $value + 3;
}
function dynamicCallAddFour(int $value): int
{
return $value + 4;
}
function dynamicCallAddFive(int $value): int
{
return $value + 5;
}
function dynamicCallAddSix(int $value): int
{
return $value + 6;
}
function dynamicCallAddSeven(int $value): int
{
return $value + 7;
}
function dynamicCallAddEight(int $value): int
{
return $value + 8;
}
final class DynamicCallTarget
{
public static function addOne(int $value): int
{
return $value + 1;
}
public function addTwo(int $value): int
{
return $value + 2;
}
public function hitOne(int $value): int
{
return $value + 1;
}
public function hitTwo(int $value): int
{
return $value + 2;
}
public function __invoke(int $value): int
{
return $value + 3;
}
}
final class DynamicCallAlternateTarget
{
public function hitOne(int $value): int
{
return $value + 1;
}
}
function runDirectCall(int $iterations): int
{
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$sum += dynamicCallAddOne($i);
}
return $sum;
}
function runMonomorphicStringCall(int $iterations): int
{
$callback = 'dynamicCallAddOne';
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$sum += $callback($i);
}
return $sum;
}
function runAlternatingStringCall(int $iterations): int
{
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$callback = ($i & 1) === 0 ? 'dynamicCallAddOne' : 'dynamicCallAddTwo';
$sum += $callback($i);
}
return $sum;
}
function runMegamorphicStringCall(int $iterations): int
{
$callbacks = [
'dynamicCallAddOne',
'dynamicCallAddTwo',
'dynamicCallAddThree',
'dynamicCallAddFour',
'dynamicCallAddFive',
'dynamicCallAddSix',
'dynamicCallAddSeven',
'dynamicCallAddEight',
];
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$callback = $callbacks[($i * 5 + 3) & 7];
$sum += $callback($i);
}
return $sum;
}
function runMonomorphicClosureCall(int $iterations): int
{
$callback = static fn (int $value): int => $value + 1;
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$sum += $callback($i);
}
return $sum;
}
function runAlternatingClosureCall(int $iterations): int
{
$first = static fn (int $value): int => $value + 1;
$second = static fn (int $value): int => $value + 2;
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$callback = ($i & 1) === 0 ? $first : $second;
$sum += $callback($i);
}
return $sum;
}
function runStaticMethodStringCall(int $iterations): int
{
$callback = 'DynamicCallTarget::addOne';
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$sum += $callback($i);
}
return $sum;
}
function runObjectMethodArrayCall(int $iterations): int
{
$target = new DynamicCallTarget();
$callback = [$target, 'addTwo'];
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$sum += $callback($i);
}
return $sum;
}
function runInvokableObjectCall(int $iterations): int
{
$callback = new DynamicCallTarget();
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$sum += $callback($i);
}
return $sum;
}
function runMonomorphicMethodNameCall(int $iterations): int
{
$target = new DynamicCallTarget();
$method = 'hitOne';
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$sum += $target->$method($i);
}
return $sum;
}
function runAlternatingMethodNameCall(int $iterations): int
{
$target = new DynamicCallTarget();
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$method = ($i & 1) === 0 ? 'hitOne' : 'hitTwo';
$sum += $target->$method($i);
}
return $sum;
}
function runPolymorphicMethodReceiverCall(int $iterations): int
{
$targets = [new DynamicCallTarget(), new DynamicCallAlternateTarget()];
$method = 'hitOne';
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$target = $targets[$i & 1];
$sum += $target->$method($i);
}
return $sum;
}
function runDynamicCallCase(string $case, int $iterations): int
{
return match ($case) {
'direct' => runDirectCall($iterations),
'string_monomorphic' => runMonomorphicStringCall($iterations),
'string_alternating' => runAlternatingStringCall($iterations),
'string_megamorphic' => runMegamorphicStringCall($iterations),
'closure_monomorphic' => runMonomorphicClosureCall($iterations),
'closure_alternating' => runAlternatingClosureCall($iterations),
'static_method_string' => runStaticMethodStringCall($iterations),
'object_method_array' => runObjectMethodArrayCall($iterations),
'invokable_object' => runInvokableObjectCall($iterations),
'method_name_monomorphic' => runMonomorphicMethodNameCall($iterations),
'method_name_alternating' => runAlternatingMethodNameCall($iterations),
'method_receiver_polymorphic' => runPolymorphicMethodReceiverCall($iterations),
default => throw new RuntimeException("Unknown benchmark case: {$case}"),
};
}
function measureDynamicCallCase(string $case): array
{
for ($warmup = 0; $warmup < 2; $warmup++) {
runDynamicCallCase($case, 1_000);
}
$best = 1.0e30;
$bestResult = 0;
for ($round = 0; $round < DYNAMIC_CALL_ROUNDS; $round++) {
$start = hrtime(true);
$result = runDynamicCallCase($case, DYNAMIC_CALL_ITERATIONS);
$elapsed = hrtime(true) - $start;
if ($elapsed < $best) {
$best = $elapsed;
$bestResult = $result;
}
}
return [$best / DYNAMIC_CALL_ITERATIONS, $bestResult];
}
function main(): void
{
$selectedCase = getenv('DYNAMIC_CALL_CASE');
foreach ([
'direct',
'string_monomorphic',
'string_alternating',
'string_megamorphic',
'closure_monomorphic',
'closure_alternating',
'static_method_string',
'object_method_array',
'invokable_object',
'method_name_monomorphic',
'method_name_alternating',
'method_receiver_polymorphic',
] as $case) {
if (is_string($selectedCase) && $selectedCase !== '' && $selectedCase !== $case) {
continue;
}
[$nanoseconds, $result] = measureDynamicCallCase($case);
printf("%s_ns=%.3f\n", $case, $nanoseconds);
printf("checksum_%s=%d\n", $case, $result);
}
}

@ -0,0 +1,8 @@
name: dynamic_call_benchmark
mode: bin
optimize: 3
lto: true
build-dir: build
output: dynamic_call
sources:
- benchmark.php

@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
$root = dirname(__DIR__, 2);
$source = __DIR__ . '/benchmark.php';
$project = __DIR__ . '/project.yml';
$binary = __DIR__ . '/dynamic_call' . (PHP_OS_FAMILY === 'Windows' ? '.exe' : '');
$skipBuild = in_array('--skip-build', $argv, true);
$selectedCase = null;
foreach ($argv as $argument) {
if (str_starts_with($argument, '--case=')) {
$selectedCase = substr($argument, strlen('--case='));
}
}
/**
* @param list<string> $command
* @param array<string, string>|null $environment
*/
function runDynamicCallCommand(array $command, string $cwd, bool $capture, ?array $environment = null): string
{
$stdout = $capture ? ['pipe', 'w'] : STDOUT;
$stderr = $capture ? ['pipe', 'w'] : STDERR;
$process = proc_open(
$command,
[STDIN, $stdout, $stderr],
$pipes,
$cwd,
$environment,
['bypass_shell' => true],
);
if (!is_resource($process)) {
throw new RuntimeException('Failed to start: ' . implode(' ', $command));
}
$output = '';
$error = '';
if ($capture) {
$output = stream_get_contents($pipes[1]);
$error = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
}
$status = proc_close($process);
if ($status !== 0) {
throw new RuntimeException(
'Command failed (' . $status . '): ' . implode(' ', $command) . "\n" . $output . $error,
);
}
return $output;
}
/** @return array{metrics: array<string, float>, checksums: array<string, string>} */
function parseDynamicCallResults(string $output): array
{
$metrics = [];
$checksums = [];
foreach (explode("\n", trim($output)) as $line) {
if (preg_match('/^([a-z_]+)_ns=([0-9.]+)$/', $line, $matches)) {
$metrics[$matches[1]] = (float) $matches[2];
} elseif (preg_match('/^checksum_([a-z_]+)=(-?[0-9]+)$/', $line, $matches)) {
$checksums[$matches[1]] = $matches[2];
}
}
return ['metrics' => $metrics, 'checksums' => $checksums];
}
$baselinePhp = getenv('PHP_BIN') ?: PHP_BINARY;
$compilerPhp = getenv('TPC_PHP_BIN') ?: $baselinePhp;
$runtimeProbe = static fn (string $binary): string => trim(runDynamicCallCommand([
$binary,
'-n',
'-r',
'printf("%s;%d;%d;%d", PHP_VERSION, PHP_ZTS, PHP_DEBUG, PHP_INT_SIZE);',
], $root, true));
$baselineRuntime = $runtimeProbe($baselinePhp);
$compilerRuntime = $runtimeProbe($compilerPhp);
if ($baselineRuntime !== $compilerRuntime) {
throw new RuntimeException(
"PHP_BIN and TPC_PHP_BIN must use the same PHP ABI for comparable results\n"
. "PHP_BIN={$baselineRuntime}\nTPC_PHP_BIN={$compilerRuntime}",
);
}
if (!$skipBuild) {
echo "Building TypePHP benchmark (-O3 + LTO)...\n";
runDynamicCallCommand([
$compilerPhp,
$root . '/bin/tpc.php',
$project,
'-j',
'8',
'--no-color',
'--no-progress',
], $root, false);
}
if (!is_file($binary)) {
throw new RuntimeException('Benchmark binary does not exist: ' . $binary);
}
$environment = getenv();
if ($selectedCase !== null && $selectedCase !== '') {
$environment['DYNAMIC_CALL_CASE'] = $selectedCase;
}
$php = parseDynamicCallResults(runDynamicCallCommand([
$baselinePhp,
'-n',
'-d',
'opcache.enable_cli=0',
'-d',
'opcache.jit=0',
'-r',
'require ' . var_export($source, true) . '; main();',
], $root, true, $environment));
if (PHP_OS_FAMILY !== 'Windows') {
$phpxHome = getenv('PHPX_HOME') ?: dirname($root) . '/phpx';
$loaderVariable = PHP_OS_FAMILY === 'Darwin' ? 'DYLD_LIBRARY_PATH' : 'LD_LIBRARY_PATH';
$existing = $environment[$loaderVariable] ?? '';
$phpHome = dirname(dirname(realpath($compilerPhp) ?: $compilerPhp));
$environment[$loaderVariable] = $phpxHome . '/lib' . PATH_SEPARATOR . $phpHome . '/lib'
. ($existing === '' ? '' : PATH_SEPARATOR . $existing);
}
$typephp = parseDynamicCallResults(runDynamicCallCommand([$binary], $root, true, $environment));
$cases = [
'direct',
'string_monomorphic',
'string_alternating',
'string_megamorphic',
'closure_monomorphic',
'closure_alternating',
'static_method_string',
'object_method_array',
'invokable_object',
'method_name_monomorphic',
'method_name_alternating',
'method_receiver_polymorphic',
];
if ($selectedCase !== null && $selectedCase !== '') {
$cases = [$selectedCase];
}
echo "Runtime: {$baselineRuntime}\n";
echo "Metric PHP ns/op TypePHP ns/op TypePHP/PHP\n";
echo "--------------------------------------------------------------\n";
foreach ($cases as $case) {
if (!isset($php['metrics'][$case], $typephp['metrics'][$case])) {
throw new RuntimeException("Missing benchmark metric: {$case}");
}
if (($php['checksums'][$case] ?? null) !== ($typephp['checksums'][$case] ?? null)) {
throw new RuntimeException("Checksum mismatch for benchmark case: {$case}");
}
printf(
"%-25s %10.2f %14.2f %12.2fx\n",
$case,
$php['metrics'][$case],
$typephp['metrics'][$case],
$typephp['metrics'][$case] / $php['metrics'][$case],
);
}

@ -0,0 +1,28 @@
<?php
function call_cache_target(int $value): int
{
return $value + 1;
}
function call_cache_sites(mixed $callback, object $object, mixed $method): array
{
return [$callback(1), $object->$method(2)];
}
class CallCacheScopedTarget
{
private function hidden(): int
{
return 3;
}
public function invoke(mixed $method): int
{
return $this->$method();
}
}
function main(): void
{
}

@ -0,0 +1,34 @@
<?php
use TypePhp\CompilerBase;
use TypePhp\CompilerTest;
final class CallCacheCodegenTest extends BaseTest
{
public function testDynamicCallSitesUseRequestLocalTypePhpCaches(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$compiler->setBuildMode(CompilerBase::BUILD_MODE_EXT);
$compiler->setTargetName('call_cache_sites');
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/call-cache-sites.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);
$extension = file_get_contents($compiler->genExtension());
self::assertIsString($code);
self::assertIsString($extension);
self::assertSame(1, substr_count($code, 'typephp_call_cached('));
self::assertSame(1, substr_count($code, 'typephp_call_method_cached('));
self::assertStringContainsString('php::callScoped(', $code);
self::assertStringContainsString('php::FunctionCallCacheSlot function_call_cache_map[1]', $extension);
self::assertStringContainsString('php::MethodCallCacheSlot method_call_cache_map[1]', $extension);
self::assertStringContainsString('typephp_get_function_call_cache(FunctionCallCacheId cache_id)', $extension);
self::assertStringContainsString('typephp_get_method_call_cache(MethodCallCacheId cache_id)', $extension);
}
}

@ -1287,9 +1287,9 @@ YAML);
$this->assertStringNotContainsString('slot.reset()', $moduleInit, $mode);
$this->assertMatchesRegularExpression(
'/PHP_RSHUTDOWN_FUNCTION\([^)]*\)\s*\{\s*'
. 'php::request_shutdown\(\);\s*'
. 'delete php_request_cache;\s*'
. 'php_request_cache = nullptr;\s*'
. 'php::request_shutdown\(\);/s',
. 'php_request_cache = nullptr;/s',
$extension,
$mode,
);

@ -36,7 +36,7 @@ final class PythonModuleTest extends TestCase
$compiler->prepareFile($source);
$cpp = file_get_contents($compiler->convertFile($source));
self::assertStringContainsString('.call(', $cpp);
self::assertStringContainsString('typephp_call_method_cached(', $cpp);
self::assertStringNotContainsString('php::python::callMember(', $cpp);
}

@ -340,6 +340,8 @@ class CompilerBase implements PropertyAccessContext
* cache a runtime class together with a dynamic/hooked-property sentinel.
*/
protected int $propertyAccessCacheIndex = 0;
protected int $methodCallCacheIndex = 0;
protected int $functionCallCacheIndex = 0;
/** @var array<string, array<Node\Stmt>> Prepared declaration ASTs keyed by real path. */
protected array $preparedFileAsts = [];
protected bool $traitDeclarationsComposed = false;
@ -1311,6 +1313,20 @@ class CompilerBase implements PropertyAccessContext
return 'get_property_cache(PropertyCacheId{' . $id . '})';
}
protected function getMethodCallCache(): string
{
$this->assertCompilerPhase(self::PHASE_CONVERT, 'method call cache ID allocation');
$id = $this->methodCallCacheIndex++;
return 'typephp_get_method_call_cache(MethodCallCacheId{' . $id . '})';
}
protected function getFunctionCallCache(): string
{
$this->assertCompilerPhase(self::PHASE_CONVERT, 'function call cache ID allocation');
$id = $this->functionCallCacheIndex++;
return 'typephp_get_function_call_cache(FunctionCallCacheId{' . $id . '})';
}
protected function getClassEntryPtr(string $className): string
{
$id = $this->getClassId($className);

@ -214,10 +214,17 @@ trait FunctionCallTrait
$name = '';
}
if (empty($expr->args)) {
if ($name === '' && $runtimeCallScope === null) {
return 'typephp_call_cached(' . $fn . ', ' . $this->getFunctionCallCache() . ')';
}
$scopeArg = $runtimeCallScope === null ? '' : $runtimeCallScope . ', ';
return 'php::call(' . $scopeArg . $fn . ')';
}
try {
if ($name === '' && $runtimeCallScope === null) {
return 'typephp_call_cached(' . $fn . ', ' . $this->getFunctionCallCache() . ', '
. $this->parseCallArgs($expr->args) . ')';
}
return $this->genRuntimeFunctionCall(
$fn,
$expr->args,

@ -857,10 +857,18 @@ trait MethodCallTrait
if ($requiresDynamicScope && $this->methodDef) {
return 'php::callScoped(' . $object . ', ' . $methodPtr . ', ' . $this->getCallableScopeExpr() . ')';
}
if (!$this->isNamedMethod($expr->name)) {
return 'typephp_call_method_cached(' . $object . ', ' . $methodPtr . ', '
. $this->getMethodCallCache() . ')';
}
return $object . '.call(' . $methodPtr . ')';
}
try {
$class = empty($class) ? self::DYNAMIC_CALLED_CLASS : $class;
if (!$this->isNamedMethod($expr->name) && !($requiresDynamicScope && $this->methodDef)) {
return 'typephp_call_method_cached(' . $object . ', ' . $methodPtr . ', '
. $this->getMethodCallCache() . ', ' . $this->parseCallArgs($expr->args) . ')';
}
return $this->genRuntimeObjectMethodCall(
$object,
$methodPtr,

@ -903,6 +903,8 @@ class Translator extends Preprocessor
$lines[] = 'enum class PersistentFuncId : uint32_t {};';
$lines[] = 'enum class PersistentPropertyId : uint32_t {};';
$lines[] = 'enum class PropertyCacheId : uint32_t {};' . PHP_EOL;
$lines[] = 'enum class MethodCallCacheId : uint32_t {};' . PHP_EOL;
$lines[] = 'enum class FunctionCallCacheId : uint32_t {};' . PHP_EOL;
$lines[] = 'zend_class_entry *get_class(RequestClassId class_id, const php::Str &class_name);';
$lines[] = 'zend_function *get_func(RequestFuncId func_id, const php::Str &func_name);';
@ -912,6 +914,8 @@ class Translator extends Preprocessor
$lines[] = 'zend_function *get_persistent_method(PersistentFuncId func_id, const php::Str &method_name, PersistentClassId class_id, const php::Str &class_name);';
$lines[] = 'uint32_t get_persistent_prop(PersistentPropertyId prop_id, const php::Str &prop_name, const php::Str &class_name);' . PHP_EOL;
$lines[] = 'php::PropertyCacheSlot &get_property_cache(PropertyCacheId cache_id);' . PHP_EOL;
$lines[] = 'php::MethodCallCacheSlot &typephp_get_method_call_cache(MethodCallCacheId cache_id);' . PHP_EOL;
$lines[] = 'php::FunctionCallCacheSlot &typephp_get_function_call_cache(FunctionCallCacheId cache_id);' . PHP_EOL;
foreach ($this->getClassLikesWithConstants() as $classDef) {
foreach ($classDef->constants as $constant) {
@ -1060,6 +1064,10 @@ class Translator extends Preprocessor
. max(1, count($this->funcMap)) . ']{};' . PHP_EOL;
$code .= $this->getIndent() . 'php::PropertyCacheSlot property_cache_map['
. max(1, $this->propertyAccessCacheIndex) . ']{};' . PHP_EOL;
$code .= $this->getIndent() . 'php::MethodCallCacheSlot method_call_cache_map['
. max(1, $this->methodCallCacheIndex) . ']{};' . PHP_EOL;
$code .= $this->getIndent() . 'php::FunctionCallCacheSlot function_call_cache_map['
. max(1, $this->functionCallCacheIndex) . ']{};' . PHP_EOL;
$code .= '};' . PHP_EOL;
$code .= 'static THREAD_LOCAL php_request_cache_storage *php_request_cache = nullptr;' . PHP_EOL;
@ -1142,6 +1150,14 @@ uint32_t get_persistent_prop(PersistentPropertyId prop_id, const php::Str &prop_
php::PropertyCacheSlot &get_property_cache(PropertyCacheId cache_id) {
return php_request_cache->property_cache_map[static_cast<uint32_t>(cache_id)];
}
php::MethodCallCacheSlot &typephp_get_method_call_cache(MethodCallCacheId cache_id) {
return php_request_cache->method_call_cache_map[static_cast<uint32_t>(cache_id)];
}
php::FunctionCallCacheSlot &typephp_get_function_call_cache(FunctionCallCacheId cache_id) {
return php_request_cache->function_call_cache_map[static_cast<uint32_t>(cache_id)];
}
CODE;
$code .= "\n\n";
@ -1512,9 +1528,9 @@ CODE;
$code .= <<<CODE
PHP_RSHUTDOWN_FUNCTION({$moduleName}) {
php::request_shutdown();
delete php_request_cache;
php_request_cache = nullptr;
php::request_shutdown();
module_clean();
return SUCCESS;
}

@ -0,0 +1,85 @@
--TEST--
Dynamic call caches preserve polymorphic, object, and magic dispatch
--FILE--
<?php
function cached_first(int $value): string
{
return 'first:' . $value;
}
function cached_second(int $value): string
{
return 'second:' . $value;
}
class CachedMethodFirst
{
public function run(int $value): string
{
return 'method-first:' . $value;
}
}
class CachedMethodSecond
{
public function run(int $value): string
{
return 'method-second:' . $value;
}
}
class CachedMagicMethod
{
public function __call(string $name, array $arguments): string
{
return 'magic-' . $name . ':' . $arguments[0];
}
}
class CachedStaticMethod
{
public static function run(int $value): string
{
return 'static:' . $value;
}
}
function invoke_function(mixed $callback, int $value): string
{
return $callback($value);
}
function invoke_method(object $object, mixed $method, int $value): string
{
return $object->$method($value);
}
function main(): void
{
$callbacks = ['cached_first', 'cached_second', 'cached_first'];
foreach ($callbacks as $index => $callback) {
var_dump(invoke_function($callback, $index));
}
$static = 'CachedStaticMethod::run';
var_dump(invoke_function($static, 3));
$closure = static fn (int $value): string => 'closure:' . $value;
var_dump(invoke_function($closure, 4));
$objects = [new CachedMethodFirst(), new CachedMethodSecond(), new CachedMagicMethod()];
foreach ($objects as $index => $object) {
var_dump(invoke_method($object, $index === 2 ? 'missing' : 'run', $index + 5));
}
}
?>
--EXPECT--
string(7) "first:0"
string(8) "second:1"
string(7) "first:2"
string(8) "static:3"
string(9) "closure:4"
string(14) "method-first:5"
string(15) "method-second:6"
string(15) "magic-missing:7"

@ -0,0 +1,30 @@
--TEST--
Native class: shutdown finalizers can use request-local dynamic call caches
--FILE--
<?php
function shutdown_dynamic_target(string $value): string
{
return 'finalizer:' . $value;
}
#[Native]
class ShutdownDynamicCaller
{
public function __destruct()
{
$callback = 'shutdown_dynamic_target';
echo $callback('ok') . "\n";
}
}
function main(): void
{
global $shutdownObject;
$shutdownObject = new ShutdownDynamicCaller();
echo "main\n";
}
?>
--EXPECT--
main
finalizer:ok
Loading…
Cancel
Save