Re: [RFC] [VOTE] Transform exit() from a language construct into a standard function

From: Date: Mon, 05 Aug 2024 16:50:20 +0000
Subject: Re: [RFC] [VOTE] Transform exit() from a language construct into a standard function
References: 1 2 3  Groups: php.internals 
Request: Send a blank email to [email protected] to get a copy of this message
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

Thread (37 messages)

« previous php.internals (#124778) next »