diff --git a/src/Translator.php b/src/Translator.php index caa69756..56c2dbba 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -3359,7 +3359,13 @@ CODE; array $implicitMethodArgs = [] ): string { - $cppCode = ''; + // A generated ZEND_FUNCTION/ZEND_METHOD is a callback boundary owned by + // ZendVM. Native TypePHP code uses C++ exceptions so its local RAII + // objects unwind correctly, but that exception must not escape through + // Zend's C frames: those frames perform their cleanup after the handler + // returns with EG(exception) set. Convert back to normal Zend exception + // propagation at the outermost wrapper. + $cppCode = 'try {' . PHP_EOL; $callParams = ''; if ($functionDef->argCountRequired > 0) { $cppCode .= $this->genWrapperRequiredArgCountCheck($functionDef, $displayName); @@ -3449,6 +3455,10 @@ CODE; } else { $cppCode .= $this->getIndent() . $fn . '(' . $callParams . ');' . PHP_EOL; } + $cppCode .= '}' . PHP_EOL; + $cppCode .= 'catch (zend_object *) {' . PHP_EOL; + $cppCode .= $this->getIndent() . '/* EG(exception) is already set; return control to ZendVM for frame cleanup. */' . PHP_EOL; + $cppCode .= '}' . PHP_EOL; $cppCode .= '}' . PHP_EOL . PHP_EOL; return $cppCode; diff --git a/tests/compiler/exception/dynamic-callback-frame-restored.phpt b/tests/compiler/exception/dynamic-callback-frame-restored.phpt new file mode 100644 index 00000000..96819d59 --- /dev/null +++ b/tests/compiler/exception/dynamic-callback-frame-restored.phpt @@ -0,0 +1,67 @@ +--TEST-- +Zend callback frames are restored after caught exceptions +--FILE-- +getTrace() as $frame) { + if (($frame['function'] ?? '') === $function) { + return true; + } + } + return false; +} + +function main(): void +{ + try { + $copy = clone new CallbackFrameProbe(); + } catch (DomainException $exception) { + } + try { + throw new RuntimeException('after clone'); + } catch (RuntimeException $exception) { + echo 'clone=', callback_frame_is_stale($exception, '__clone') ? 'stale' : 'clean', "\n"; + } + + try { + array_map(static function (int $value): int { + throw new DomainException('array_map'); + }, [1]); + } catch (DomainException $exception) { + } + try { + throw new RuntimeException('after array_map'); + } catch (RuntimeException $exception) { + echo 'array_map=', callback_frame_is_stale($exception, '{closure}') ? 'stale' : 'clean', "\n"; + } + + try { + (new ReflectionMethod(CallbackFrameProbe::class, 'fail'))->invoke(new CallbackFrameProbe()); + } catch (DomainException $exception) { + } + try { + throw new RuntimeException('after reflection'); + } catch (RuntimeException $exception) { + echo 'reflection=', callback_frame_is_stale($exception, 'fail') ? 'stale' : 'clean', "\n"; + } +} +?> +--EXPECT-- +clone=clean +array_map=clean +reflection=clean