Weird destructor call order on stream wrappers

php.internals

Dennis Birkholz

8 years ago
Hello together, today I noticed that the order in which stream_flush/stream_close and the destructor of a (userspace implemented) stream wrapper are called is weird: When a stream is closed with fclose(), the call order is as expected: stream_flush, stream_close, finally the destructor. But if the stream is closed automatically on program exit, the order seems wrong: stream_flush and stream_close are called AFTER the destructor was called (and so the object should be destroyed!) The documentation (http://php.net/manual/en/streamwrapper.destruct.php) for the destructor contains the ominous sentence "Called when closing the stream wrapper, right before streamWrapper::stream_flush()." which is clearly only half of the truth. Example: https://3v4l.org/CMItP Is this some kind of "desired" behavior? Should I open a bug? Greets, Dennis P.s.: In addition to the call order, in auto-close case autoloading with registered autoloaders does not work inside stream_flush and stream_close, it seems registered autoloaders (maybe all objects) have been dispatched when the calls happen, see: https://3v4l.org/GB7Mm (I also appended both example files as I don't know how long snippets at 3v4l.org persist)

Dennis Birkholz

8 years ago
Hello again, Am 20.03.2018 um 23:13 schrieb Dennis Birkholz:
> (I also appended both example files as I don't know how long snippets at > 3v4l.org persist)
the list does not allow attachments, so inline. If this does not work, 3v4l must suffice. Greets, Dennis <?php \spl_autoload_register(function($class) { echo "Autoloader for '$class' called\n"; class_alias(Test::class, $class); return true; }); class Test { static function blubb() { echo static::class, "\n"; } } class MyStreamWrapper { static $counter = 1; private $id; public function __construct() { $this->id = self::$counter++; } public function stream_open($path, $mode, $options, &$opened_path) { echo "({$this->id}) stream_open()\n"; return true; } public function stream_write($data) { echo "({$this->id}) stream_write()\n"; return \strlen($data); } public function stream_flush() { echo "({$this->id}) stream_flush()\n"; if ($this->id === 1) { Lalala::blubb(); } else { Foobar::blubb(); } return true; } public function stream_close() { echo "({$this->id}) stream_close()\n"; return true; } public function __destruct() { echo "({$this->id}) __destruct()\n"; } } \stream_wrapper_register('test', MyStreamWrapper::class); if (($handle = \fopen('test:///foobar', 'r+')) === false) { throw new \RuntimeException("Failed to open file"); } if (\fwrite($handle, 'test') === false) { throw new \RuntimeException("Failed to write to file"); } if (\fclose($handle) === false) { throw new \RuntimeException("Failed to close file"); } if (($handle2 = \fopen('test:///blubb', 'r+')) === false) { throw new \RuntimeException("Failed to open file"); } if (\fwrite($handle2, 'test') === false) { throw new \RuntimeException("Failed to write to file"); }

Rowan Collins

8 years ago
On 20/03/2018 22:13, Dennis Birkholz wrote:
> P.s.: > In addition to the call order, in auto-close case autoloading with > registered autoloaders does not work inside stream_flush and > stream_close, it seems registered autoloaders (maybe all objects) have > been dispatched when the calls happen, see: https://3v4l.org/GB7Mm
The behaviour with the stream wrapper certainly seems wrong to me, since there is clearly still a reference to the object after its destructor is called. The autoloader not being available seems reasonable, though - during shutdown, the engine has to start unpicking various global state in order to cleanly release any references, and there is no order to do this that would guarantee no code breaking. You demonstrate a stream wrapper relying on an autoloader, but we could equally have an autoloader that relied on a stream wrapper. If both needed to be available for the other, the engine could never cleanly free any resources they held, so it has to start somewhere. In general, anything that might run on shutdown, such as a destructor, should be very conservative in its assumptions about the engine's state. Regards,
-- Rowan Collins [IMSoP]

=?utf-8?B?5rGf5rmW5aSn6Jm+5LuB?=

8 years ago
There is a related bug report: https://bugs.php.net/bug.php?id=75931 It also point out that the construcor method has been ignored. Best regards, CHU Zhaowei

Dennis Birkholz

8 years ago
Hello, Am 22.03.2018 um 06:59 schrieb CHU Zhaowei:
> There is a related bug report: https://bugs.php.net/bug.php?id=75931
Thank you for pointing out this bug report. It is about stream filters (extending the php_user_filter class) and not about stream wrappers which seem to have similar issues.
> It also point out that the construcor method has been ignored.
That does not apply to stream wrappers, the constructor is called just fine. Greets Dennis