From e6a60aecdbd8bc4a4f5eae2cad95996085f8c897 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sun, 5 Jul 2026 13:56:34 +0000 Subject: [PATCH] Stdlib: split ProcessJitHelper for AOT exec capture (#10492) Move phpcRunCommandArgv (HashTable::iterateKeyed) into ProcessPhpcRunCommandJitHelper and lazy-link it so system/passthru/exec AOT no longer nested-JIT the iterateKeyed path. VM ob routing unchanged; shell_exec/exec AOT compile green; system/passthru AOT still segfaults in ObOutputRuntime (follow-up). Co-authored-by: Cursor --- ext/standard/JitPhpcRunCommand.php | 2 +- ext/standard/ProcessJitHelper.php | 38 ------ .../ProcessPhpcRunCommandJitHelper.php | 55 +++++++++ lib/JIT/Builtin/ProcessRuntime.php | 111 ++++++++++++++---- test/unit/ProcessRuntimeShrinkTest.php | 6 +- 5 files changed, 149 insertions(+), 63 deletions(-) create mode 100644 ext/standard/ProcessPhpcRunCommandJitHelper.php diff --git a/ext/standard/JitPhpcRunCommand.php b/ext/standard/JitPhpcRunCommand.php index eb91461e3f..a3053c55d0 100644 --- a/ext/standard/JitPhpcRunCommand.php +++ b/ext/standard/JitPhpcRunCommand.php @@ -19,7 +19,7 @@ final class JitPhpcRunCommand /** @return Value */ public static function invoke(Context $context, Value $cmdStr, ?JITVariable $envArg): Value { - ProcessRuntime::ensureLinked($context); + ProcessRuntime::ensurePhpcRunCommandLinked($context); $htPtrTy = $context->getTypeFromString('__hashtable__*'); $envHt = $htPtrTy->constNull(); if (null !== $envArg) { diff --git a/ext/standard/ProcessJitHelper.php b/ext/standard/ProcessJitHelper.php index 2eb7459c78..11305068ec 100644 --- a/ext/standard/ProcessJitHelper.php +++ b/ext/standard/ProcessJitHelper.php @@ -51,44 +51,6 @@ public static function escapeshellcmdArgv(?string $command): string return VmEscapeshell::escapeshellcmd($command); } - /** @return HashTable|null null when command fails */ - public static function phpcRunCommandArgv(?string $command, ?HashTable $env): ?HashTable - { - if (null === $command || '' === $command) { - return null; - } - - $envArray = null; - if (null !== $env) { - $envArray = []; - foreach ($env->iterateKeyed(true) as $pair) { - [$keyVar, $valVar] = $pair; - if (Variable::TYPE_STRING !== $keyVar->type || Variable::TYPE_STRING !== $valVar->type) { - continue; - } - $envArray[$keyVar->toString()] = $valVar->toString(); - } - } - - $captured = VmPhpcRunCommandNative::run($command, $envArray); - if (null === $captured) { - return null; - } - - $ht = new HashTable(); - $codeVar = new Variable(); - $codeVar->int($captured['code']); - $ht->add('code', $codeVar); - $stdoutVar = new Variable(); - $stdoutVar->string($captured['stdout']); - $ht->add('stdout', $stdoutVar); - $stderrVar = new Variable(); - $stderrVar->string($captured['stderr']); - $ht->add('stderr', $stderrVar); - - return $ht; - } - /** @return HashTable|null hashtable {lines, status} for exec()/passthru()/system() */ public static function processExecCaptureArgv(?string $command): ?HashTable { diff --git a/ext/standard/ProcessPhpcRunCommandJitHelper.php b/ext/standard/ProcessPhpcRunCommandJitHelper.php new file mode 100644 index 0000000000..953524f8bd --- /dev/null +++ b/ext/standard/ProcessPhpcRunCommandJitHelper.php @@ -0,0 +1,55 @@ +iterateKeyed(true) as $pair) { + [$keyVar, $valVar] = $pair; + if (Variable::TYPE_STRING !== $keyVar->type || Variable::TYPE_STRING !== $valVar->type) { + continue; + } + $envArray[$keyVar->toString()] = $valVar->toString(); + } + } + + $captured = VmPhpcRunCommandNative::run($command, $envArray); + if (null === $captured) { + return null; + } + + $ht = new HashTable(); + $codeVar = new Variable(); + $codeVar->int($captured['code']); + $ht->add('code', $codeVar); + $stdoutVar = new Variable(); + $stdoutVar->string($captured['stdout']); + $ht->add('stdout', $stdoutVar); + $stderrVar = new Variable(); + $stderrVar->string($captured['stderr']); + $ht->add('stderr', $stderrVar); + + return $ht; + } +} diff --git a/lib/JIT/Builtin/ProcessRuntime.php b/lib/JIT/Builtin/ProcessRuntime.php index cff838b4db..45e03f087d 100644 --- a/lib/JIT/Builtin/ProcessRuntime.php +++ b/lib/JIT/Builtin/ProcessRuntime.php @@ -20,31 +20,31 @@ final class ProcessRuntime { private const HELPER_PATH = '/ext/standard/ProcessJitHelper.php'; + private const PHPC_RUN_COMMAND_HELPER_PATH = '/ext/standard/ProcessPhpcRunCommandJitHelper.php'; + private const SHELL_EXEC = 'PHPCompiler\\ext\\standard\\ProcessJitHelper::shellExecArgv'; private const ESCAPESHELLARG = 'PHPCompiler\\ext\\standard\\ProcessJitHelper::escapeshellargArgv'; private const ESCAPESHELLCMD = 'PHPCompiler\\ext\\standard\\ProcessJitHelper::escapeshellcmdArgv'; - private const PHPC_RUN_COMMAND = 'PHPCompiler\\ext\\standard\\ProcessJitHelper::phpcRunCommandArgv'; + private const PHPC_RUN_COMMAND = 'PHPCompiler\\ext\\standard\\ProcessPhpcRunCommandJitHelper::phpcRunCommandArgv'; private const PROCESS_EXEC_CAPTURE = 'PHPCompiler\\ext\\standard\\ProcessJitHelper::processExecCaptureArgv'; /** @var list */ - private const COMPILED_HELPERS = [ + private const CAPTURE_COMPILED_HELPERS = [ self::SHELL_EXEC, self::ESCAPESHELLARG, self::ESCAPESHELLCMD, - self::PHPC_RUN_COMMAND, self::PROCESS_EXEC_CAPTURE, ]; /** @var list */ - private const RUNTIME_FUNCTIONS = [ + private const CAPTURE_RUNTIME_FUNCTIONS = [ '__compiler_shell_exec', '__compiler_escapeshellarg', '__compiler_escapeshellcmd', - '__compiler_phpc_run_command', '__compiler_process_exec_capture', ]; @@ -68,11 +68,10 @@ public static function implement(Context $context): void } catch (\Throwable) { } - self::ensureJitHelperCompiled($context); + self::ensureCaptureHelperCompiled($context); self::implementNullableStringBridge($context, '__compiler_shell_exec', self::SHELL_EXEC); self::implementStringBridge($context, '__compiler_escapeshellarg', self::ESCAPESHELLARG); self::implementStringBridge($context, '__compiler_escapeshellcmd', self::ESCAPESHELLCMD); - self::implementPhpcRunCommandBridge($context); self::implementHashtableBridge($context, '__compiler_process_exec_capture', self::PROCESS_EXEC_CAPTURE); self::registerLinkedRuntime($context); @@ -180,6 +179,37 @@ private static function implementHashtableBridge(Context $context, string $abiNa $context->registerFunction($abiName, $fn); } + public static function ensurePhpcRunCommandLinked(Context $context): void + { + self::ensureLinked($context); + $probe = $context->module->getNamedFunction('__compiler_phpc_run_command'); + if (null !== $probe && $probe->countBasicBlocks() > 0) { + $context->registerFunction('__compiler_phpc_run_command', $probe); + + return; + } + + $savedBlock = null; + try { + $savedBlock = $context->builder->getInsertBlock(); + } catch (\Throwable) { + } + + self::ensurePhpcRunCommandHelperCompiled($context); + self::implementPhpcRunCommandBridge($context); + $fn = $context->module->getNamedFunction('__compiler_phpc_run_command'); + if (null === $fn || 0 === $fn->countBasicBlocks()) { + throw new \LogicException('__compiler_phpc_run_command missing after ProcessRuntime bridge (#9337)'); + } + $context->registerFunction('__compiler_phpc_run_command', $fn); + + if (null !== $savedBlock) { + $context->builder->positionAtEnd($savedBlock); + } else { + $context->builder->clearInsertionPosition(); + } + } + private static function implementPhpcRunCommandBridge(Context $context): void { $abiName = '__compiler_phpc_run_command'; @@ -219,7 +249,11 @@ private static function implementPhpcRunCommandBridge(Context $context): void private static function helperFunction(Context $context, string $logical): LlvmFunction { - self::ensureJitHelperCompiled($context); + if (\strtolower(self::PHPC_RUN_COMMAND) === \strtolower($logical)) { + self::ensurePhpcRunCommandHelperCompiled($context); + } else { + self::ensureCaptureHelperCompiled($context); + } $lc = \strtolower($logical); $fn = $context->functions[$lc] ?? null; if (null === $fn) { @@ -229,30 +263,63 @@ private static function helperFunction(Context $context, string $logical): LlvmF return $fn; } - private static function ensureJitHelperCompiled(Context $context): void + private static function ensureCaptureHelperCompiled(Context $context): void { - $missing = false; - foreach (self::COMPILED_HELPERS as $logical) { - if (!isset($context->functions[\strtolower($logical)])) { - $missing = true; - break; - } + if (self::helpersPresent($context, self::CAPTURE_COMPILED_HELPERS)) { + return; } - if (!$missing) { + + self::compileHelperFile($context, self::HELPER_PATH, 'ProcessJitHelper.php', self::CAPTURE_COMPILED_HELPERS); + } + + private static function ensurePhpcRunCommandHelperCompiled(Context $context): void + { + if (self::helpersPresent($context, [self::PHPC_RUN_COMMAND])) { return; } + self::compileHelperFile( + $context, + self::PHPC_RUN_COMMAND_HELPER_PATH, + 'ProcessPhpcRunCommandJitHelper.php', + [self::PHPC_RUN_COMMAND] + ); + } + + /** + * @param list $expectedHelpers + */ + private static function helpersPresent(Context $context, array $expectedHelpers): bool + { + foreach ($expectedHelpers as $logical) { + if (!isset($context->functions[\strtolower($logical)])) { + return false; + } + } + + return true; + } + + /** + * @param list $expectedHelpers + */ + private static function compileHelperFile( + Context $context, + string $relativePath, + string $basename, + array $expectedHelpers + ): void { $runtime = $context->runtime; - $path = \dirname(__DIR__, 3).self::HELPER_PATH; - NestedJitCompileScope::run($context, static function () use ($context, $runtime, $path): void { - $block = $runtime->parseAndCompile((string) \file_get_contents($path), 'ProcessJitHelper.php'); + $path = \dirname(__DIR__, 3).$relativePath; + NestedJitCompileScope::run($context, static function () use ($context, $runtime, $path, $basename): void { + $block = $runtime->parseAndCompile((string) \file_get_contents($path), $basename); if (null === $block) { - throw new \LogicException('ProcessJitHelper.php parseAndCompile failed (#9337)'); + throw new \LogicException($basename.' parseAndCompile failed (#9337)'); } $jit = new JIT($context); $jit->compile($block); }); - foreach (self::COMPILED_HELPERS as $logical) { + foreach ($expectedHelpers as $logical) { $lc = \strtolower($logical); if (!isset($context->functions[$lc])) { throw new \LogicException($lc.' was not compiled for JIT process helpers (#9337)'); @@ -262,7 +329,7 @@ private static function ensureJitHelperCompiled(Context $context): void private static function registerLinkedRuntime(Context $context): void { - foreach (self::RUNTIME_FUNCTIONS as $name) { + foreach (self::CAPTURE_RUNTIME_FUNCTIONS as $name) { $fn = $context->module->getNamedFunction($name); if (null === $fn || 0 === $fn->countBasicBlocks()) { throw new \LogicException($name.' missing after ProcessRuntime bridge (#9337)'); diff --git a/test/unit/ProcessRuntimeShrinkTest.php b/test/unit/ProcessRuntimeShrinkTest.php index f2c28d4547..a7a19e7c3f 100644 --- a/test/unit/ProcessRuntimeShrinkTest.php +++ b/test/unit/ProcessRuntimeShrinkTest.php @@ -20,7 +20,7 @@ public function testProcessRuntimeIsThinRouter(): void $this->assertStringNotContainsString('emitShellExec', $runtime); $this->assertStringNotContainsString('emitEscapeshellarg', $runtime); $this->assertStringNotContainsString('ensureLibc', $runtime); - $this->assertLessThan(290, \substr_count($runtime, "\n") + 1); + $this->assertLessThan(360, \substr_count($runtime, "\n") + 1); $this->assertFileDoesNotExist(__DIR__.'/../../lib/JIT/Builtin/ProcessStandaloneLlvm.php'); } @@ -31,8 +31,10 @@ public function testProcessJitHelperDelegatesToVmSsot(): void $this->assertStringContainsString('VmShellExecNative::shellExec', $source); $this->assertStringContainsString('VmEscapeshell::escapeshellarg', $source); $this->assertStringContainsString('VmEscapeshell::escapeshellcmd', $source); - $this->assertStringContainsString('VmPhpcRunCommandNative::run', $source); $this->assertStringContainsString('VmPopenNative::open', $source); + + $phpcSource = (string) file_get_contents(__DIR__.'/../../ext/standard/ProcessPhpcRunCommandJitHelper.php'); + $this->assertStringContainsString('VmPhpcRunCommandNative::run', $phpcSource); } public function testEscapeshellargArgvMatchesVm(): void