Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ext/standard/JitPhpcRunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
38 changes: 0 additions & 38 deletions ext/standard/ProcessJitHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
55 changes: 55 additions & 0 deletions ext/standard/ProcessPhpcRunCommandJitHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\standard;

use PHPCompiler\VM\HashTable;
use PHPCompiler\VM\Variable;

/**
* phpc_run_command for compiled JIT/AOT embed modules (#9337, #10492).
*
* Split from {@see ProcessJitHelper} so exec/passthru/system capture helpers compile
* without nested-JITing HashTable::iterateKeyed paths (see ParseStrNativeJitHelper).
*/
final class ProcessPhpcRunCommandJitHelper
{
/** @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;
}
}
111 changes: 89 additions & 22 deletions lib/JIT/Builtin/ProcessRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> */
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<string> */
private const RUNTIME_FUNCTIONS = [
private const CAPTURE_RUNTIME_FUNCTIONS = [
'__compiler_shell_exec',
'__compiler_escapeshellarg',
'__compiler_escapeshellcmd',
'__compiler_phpc_run_command',
'__compiler_process_exec_capture',
];

Expand All @@ -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);

Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -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<string> $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<string> $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)');
Expand All @@ -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)');
Expand Down
6 changes: 4 additions & 2 deletions test/unit/ProcessRuntimeShrinkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand All @@ -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
Expand Down