Hi

On 8/5/24 13:04, Derick Rethans wrote:
As userland PHP developer, I always regarded `exit` as a control flow
instruction (quite similar to `break`), and as such I'm not really in
favor of converting it to a proper function (especially since it is
not, because the parantheses could be omitted).

Xdebug uses exit for exactly that too. For control flow analysis. And I
also always have considered it to be a control flow instruction.

I see no benefit in changing it to a function, especially because
there will never be a function "exit" from it, just only an "entry".
This breaks function execution symmetry (and causes issues with Xdebug
when I last tried to make it work with a development branch for this
RFC).

This is false. The observers are perfectly capable of detecting that the `exit()` function returns / throws when observing `zend_observer_fcall_end_handler` as demonstrated by the following script:

    <?php
    class MyClass {
        public function __destruct() {
                echo __METHOD__, PHP_EOL;
        }
    }

    function a() {
        $dummy = new MyClass();

        exit();
    }

    a();

Running this script as:

sapi/cli/php -d zend_test.observer.enabled=1 -d zend_test.observer.observe_all=1 test.php

will return the following output:

    <!-- init 'php-src/test.php' -->
    <file 'php-src/test.php'>
      <!-- init a() -->
      <a>
        <!-- init exit() -->
        <exit>
          <!-- Exception: UnwindExit -->
        </exit>
        <!-- Exception: UnwindExit -->
      </a>
      <!-- init MyClass::__destruct() -->
      <MyClass::__destruct>
    MyClass::__destruct
      </MyClass::__destruct>
      <!-- Exception: UnwindExit -->
    </file 'php-src/test.php'>

Leaving the exit() function will be observed as indicated by `</exit>`. It also shows that any destructors will be called and it also shows the internal UnwindExit exception.

Thus you are able to detect if `exit()` has successfully been called by observing an fcall_end and checking for `zend_is_unwind_exit(EG(exception))` to determine if the exception you're dealing with is the unwind exception.

That way you will also correctly handle that `exit()` is not successful, e.g. when a non-stringable class is passed to it and a TypeError is thrown, because then the output will look something like this:

    <!-- init 'php-src/test.php' -->
    <file 'php-src/test.php'>
      <!-- init a() -->
      <a>
        <!-- init exit() -->
        <exit>
          <!-- Exception: TypeError -->
        </exit>
        <!-- Exception: TypeError -->
      </a>
      <!-- Exception: TypeError -->
    </file 'php-src/test.php'>
    <!-- init Error::__toString() -->
    <Error::__toString>
      <!-- init Error::getTraceAsString() -->
      <Error::getTraceAsString>
      </Error::getTraceAsString>
    </Error::__toString>

Fatal error: Uncaught TypeError: exit(): Argument #1 ($code) must be of type string|int, MyClass given in php-src/test.php:11
    Stack trace:
    #0 php-src/test.php(11): exit(Object(MyClass))
    #1 php-src/test.php(14): a()
    #2 {main}
      thrown in php-src/test.php on line 11
    <!-- init MyClass::__destruct() -->
    <MyClass::__destruct>
    MyClass::__destruct
    </MyClass::__destruct>

Best regards
Tim Düsterhus

Reply via email to