Skip to content
Merged
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: 2 additions & 0 deletions ext/standard/Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@ public function getFunctions(): array
new trigger_error_(),
new set_error_handler_(),
new restore_error_handler_(),
new set_exception_handler(),
new restore_exception_handler(),
new error_get_last(),
new error_clear_last(),
new eval_(),
Expand Down
109 changes: 109 additions & 0 deletions ext/standard/VmExceptionHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\standard;

use PHPCompiler\VM\Context;
use PHPCompiler\VM\Variable;

/**
* set_exception_handler() / restore_exception_handler() VM state (issue #3146).
*
* @see ext/standard/basic_functions.c — PHP_FUNCTION(set_exception_handler)
*/
final class VmExceptionHandler
{
public static function set(Context $context, Variable $callback): Variable
{
$resolved = $callback->resolveIndirect();
if (Variable::TYPE_NULL === $resolved->type) {
$removed = $context->exceptionHandlers->popReturningRemoved();

return self::handlerReturnValue($removed);
}
self::assertSupportedCallback($resolved);
$previous = $context->exceptionHandlers->push($callback);

return self::handlerReturnValue($previous);
}

public static function restore(Context $context): bool
{
return $context->exceptionHandlers->pop();
}

public static function invoke(Context $context, Variable $handler, Variable $exception): bool
{
$handler = $handler->resolveIndirect();
$exceptionArg = new Variable();
$exceptionArg->copyFrom($exception);

if (VmClosureCall::isClosure($handler)) {
$result = VmClosureCall::invoke(
$context,
VmClosureCall::resolve($handler),
$exceptionArg
);
} elseif (Variable::TYPE_STRING === $handler->type) {
$fn = self::resolveStringCallback($context, $handler->toString());
$result = $context->runtime->vm->invokePhpFunction($fn, $exceptionArg);
} else {
throw new \LogicException(
'set_exception_handler() callback must be null, a closure, or a string function name in this compiler build'
);
}

$resolved = $result->resolveIndirect();
if (Variable::TYPE_BOOLEAN === $resolved->type && !$resolved->toBool()) {
return false;
}

return true;
}

public static function handlerReturnValue(?Variable $handler): Variable
{
$out = new Variable();
if (null === $handler) {
$out->null();

return $out;
}
$out->copyFrom($handler);

return $out;
}

private static function assertSupportedCallback(Variable $callback): void
{
if (VmClosureCall::isClosure($callback)) {
return;
}
if (Variable::TYPE_STRING === $callback->type) {
return;
}

throw new \LogicException(
'set_exception_handler() callback must be null, a closure, or a string function name in this compiler build'
);
}

private static function resolveStringCallback(Context $context, string $name): \PHPCompiler\Func\PHP
{
$lc = strtolower($name);
if (!isset($context->functions[$lc])) {
throw new \LogicException(
"set_exception_handler() callback '{$name}' is not a defined function in this compiler build"
);
}
$fn = $context->functions[$lc];
if (!$fn instanceof \PHPCompiler\Func\PHP) {
throw new \LogicException(
"set_exception_handler() callback '{$name}' must be a user-defined function in this compiler build"
);
}

return $fn;
}
}
43 changes: 43 additions & 0 deletions ext/standard/restore_exception_handler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\standard;

use PHPCompiler\Frame;
use PHPCompiler\Func\Internal;
use PHPCompiler\JIT\Context;
use PHPCompiler\JIT\Variable as JITVariable;
use PHPLLVM\Value;

/**
* restore_exception_handler() — pop handler stack (issue #3146).
*/
final class restore_exception_handler extends Internal
{
public function __construct()
{
parent::__construct('restore_exception_handler');
}

public function execute(Frame $frame): void
{
if (\count($frame->calledArgs) > 0) {
throw new \LogicException('restore_exception_handler() takes no arguments');
}
if (null === $frame->vmContext) {
return;
}
$restored = VmExceptionHandler::restore($frame->vmContext);
if (null !== $frame->returnVar) {
$frame->returnVar->bool($restored);
}
}

public function call(Context $context, JITVariable ...$args): Value
{
throw new \LogicException(
'restore_exception_handler() is VM-only in this compiler build (issue #3146)'
);
}
}
44 changes: 44 additions & 0 deletions ext/standard/set_exception_handler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\standard;

use PHPCompiler\Frame;
use PHPCompiler\Func\Internal;
use PHPCompiler\JIT\Context;
use PHPCompiler\JIT\Variable as JITVariable;
use PHPLLVM\Value;

/**
* set_exception_handler() — user uncaught-exception callbacks (issue #3146).
*/
final class set_exception_handler extends Internal
{
public function __construct()
{
parent::__construct('set_exception_handler');
}

public function execute(Frame $frame): void
{
$argc = \count($frame->calledArgs);
if ($argc < 1 || $argc > 1) {
throw new \LogicException('set_exception_handler() expects exactly 1 argument');
}
if (null === $frame->vmContext) {
return;
}
$result = VmExceptionHandler::set($frame->vmContext, $frame->calledArgs[0]);
if (null !== $frame->returnVar) {
$frame->returnVar->copyFrom($result);
}
}

public function call(Context $context, JITVariable ...$args): Value
{
throw new \LogicException(
'set_exception_handler() is VM-only in this compiler build (issue #3146)'
);
}
}
4 changes: 4 additions & 0 deletions lib/VM.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use PHPCompiler\VM\NamedArgs;
use PHPCompiler\VM\ObjectEntry;
use PHPCompiler\VM\ObjectPropertyIterator;
use PHPCompiler\VM\ScriptExit;
use PHPCompiler\VM\TypeCheck;
use PHPCompiler\VM\TypedPropertyReadSignal;
use PHPCompiler\VM\Variable;
Expand Down Expand Up @@ -2889,6 +2890,9 @@ private function schedulePendingReturnDispatch(): bool
private function raiseUncaughtException(Variable $thrown): void
{
$this->clearTryCatchUnwindState();
if ($this->context->exceptionHandlers->dispatch($this->context, $thrown)) {
throw new ScriptExit(0);
}
if (Variable::TYPE_OBJECT === $thrown->type) {
$entry = $thrown->toObject();
try {
Expand Down
3 changes: 3 additions & 0 deletions lib/VM/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ class Context {

public ErrorReporter $errors;

public ExceptionHandlerStack $exceptionHandlers;

public ScriptStack $scriptStack;

/** @var array<int, Variable> foreach iterator container cache (issue #167, #1885). */
Expand Down Expand Up @@ -108,6 +110,7 @@ class Context {
public function __construct(Runtime $runtime) {
$this->runtime = $runtime;
$this->errors = new ErrorReporter();
$this->exceptionHandlers = new ExceptionHandlerStack();
$this->scriptStack = new ScriptStack();
BuiltinClasses::register($this);
}
Expand Down
77 changes: 77 additions & 0 deletions lib/VM/ExceptionHandlerStack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\VM;

use PHPCompiler\ext\standard\VmExceptionHandler;

/**
* Stack of user exception handlers (set_exception_handler parity, issue #3146).
*
* @see Zend/zend_exceptions.c — uncaught handler chain
*/
final class ExceptionHandlerStack
{
/** @var list<Variable> */
private array $stack = [];

public function push(Variable $callback): ?Variable
{
$previous = $this->activeCopy();
$stored = new Variable();
$stored->copyFrom($callback->resolveIndirect());
$this->stack[] = $stored;

return $previous;
}

public function pop(): bool
{
if ([] === $this->stack) {
return false;
}
array_pop($this->stack);

return true;
}

public function popReturningRemoved(): ?Variable
{
if ([] === $this->stack) {
return null;
}
$removed = array_pop($this->stack);
$out = new Variable();
$out->copyFrom($removed);

return $out;
}

/**
* Invoke handlers from innermost to outermost until one handles the exception.
*
* Handlers that return false are not removed from the stack (Zend parity).
*/
public function dispatch(Context $context, Variable $exception): bool
{
for ($i = \count($this->stack) - 1; $i >= 0; $i--) {
if (VmExceptionHandler::invoke($context, $this->stack[$i], $exception)) {
return true;
}
}

return false;
}

private function activeCopy(): ?Variable
{
if ([] === $this->stack) {
return null;
}
$out = new Variable();
$out->copyFrom($this->stack[\count($this->stack) - 1]);

return $out;
}
}
4 changes: 4 additions & 0 deletions test/compliance/JITTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ public static function providePHPTests(): \Generator
if (str_contains($name, 'gc_collect_cycles')) {
continue;
}
// set_exception_handler() / restore_exception_handler() VM-only (#3146).
if (str_contains($name, 'exception_handler')) {
continue;
}
// WeakReference get() return used in locals — MCJIT execute (#3667).
if (str_contains($name, 'weak_reference_gc_jit')) {
continue;
Expand Down
12 changes: 12 additions & 0 deletions test/compliance/cases/stdlib/exception_handler.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Runtime: set_exception_handler() handles uncaught Throwable (issue #3146)
--FILE--
<?php
set_exception_handler(function (Throwable $e): void {
echo 'handled:', $e->getMessage(), "\n";
});
throw new Exception('x');
--EXPECT--
handled:x
--EXPECT_EXIT--
0
11 changes: 11 additions & 0 deletions test/compliance/cases/stdlib/exception_handler_fallthrough.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--TEST--
Runtime: exception handler returning false falls through (issue #3146)
--FILE--
<?php
set_exception_handler(fn () => print "outer\n");
set_exception_handler(fn (): bool => false);
throw new Exception('fall');
--EXPECT--
outer
--EXPECT_EXIT--
0
12 changes: 12 additions & 0 deletions test/compliance/cases/stdlib/exception_handler_restore.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Runtime: restore_exception_handler() restores nested handlers (issue #3146)
--FILE--
<?php
set_exception_handler(fn () => print "a\n");
set_exception_handler(fn () => print "b\n");
restore_exception_handler();
throw new Exception('z');
--EXPECT--
a
--EXPECT_EXIT--
0
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
Runtime: set_exception_handler() with string function name (issue #3146)
--FILE--
<?php
function my_handler(Throwable $e): void {
echo 'str:', $e->getMessage(), "\n";
}
set_exception_handler('my_handler');
throw new Exception('y');
--EXPECT--
str:y
--EXPECT_EXIT--
0