Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP garbage collection when using static method to create instance

After much tracking down I finally figured out what's going wrong in my code, so this question isn't "how do I fix it", but rather "why does this happen?".

Consider the following code:

class Foo {
    private $id;
    public $handle;

    public function __construct($id) {
        $this->id = $id;
        $this->handle = fopen('php://memory', 'r+');

        echo $this->id . ' - construct' . PHP_EOL;
    }

    public function __destruct() {
        echo $this->id . ' - destruct' . PHP_EOL;

        fclose($this->handle);
    }

    public function bar() {
        echo $this->id . ' - bar - ' . get_resource_type($this->handle) . PHP_EOL;

        return $this;
    }

    public static function create($id) {
        return new Foo($id);
    }
}

Seems simple enough - when created it will open up a memory stream and set the property $handle and $id. When destructing it will use fclose to close this stream.

Usage:

$foo = Foo::create(1); // works

var_dump( $foo->bar()->handle ); // works

var_dump( Foo::create(2)->bar()->handle ); // doesn't work

What seems to be the issue here is that I'm expecting both calls to return exactly the same but for some reason the Foo::create(2) call where I don't save the instance to a variable calls the garbage collector somewhere between the return $this part of the bar() method and me actually using the property $handle.

In case you're wondering, this is the output:

1 - construct                 // echo $this->id . ' - construct' . PHP_EOL;
1 - bar - stream              // echo $this->id . ' - bar - ' ...
resource(5) of type (stream)  // var_dump
2 - construct                 // echo $this->id . ' - construct' . PHP_EOL;
2 - bar - stream              // echo $this->id . ' - bar - ' ...
2 - destruct                  // echo $this->id . ' - destruct' . PHP_EOL;
resource(6) of type (Unknown) // var_dump
1 - destruct                  // echo $this->id . ' - destruct' . PHP_EOL;

From what I can see this is what happens:

var_dump( Foo::create(2)->bar()->handle );
// run GC before continuing..  ^^ .. but I'm not done with it :(

But why? Why does PHP think I'm done with the variable/class instance and hence feels the need to destruct it?

Demos:

eval.in demo
3v4l demo (only HHVM can figure it out - all other PHP versions can't)

like image 219
h2ooooooo Avatar asked Dec 24 '22 23:12

h2ooooooo


2 Answers

This all boils down to refcounts and how PHP treats resources differently.

When a class instance is destroyed, all non-database link resources are closed (see above link on resources). All non-resources referenced elsewhere will still be valid.

In your first example you assign $temp = Foo::create(1) which increases the refcount to an instance of Foo, preventing it from being destroyed which keeps the resource open.

In your second example, var_dump( Foo::create(2)->bar()->handle );, here's how things play out:

  1. Foo::create(2) is called, creating an instance of Foo.
  2. You call method bar() on the new instance, returning $this which increases the refcount by one.
  3. You leave bar()'s scope and the next action isn't a method call or an assignment, refcount goes down by one.
  4. The instance's refcount is zero, so it's destroyed. All non-database link resources are closed.
  5. You attempt to access a closed resource, returning Unknown.

As additional proof, this works just fine:

$temp = Foo::create(3)->bar();
// $temp keep's Foo::create(3)'s refcount above zero
var_dump( $temp->handle );

As does this:

$temp = Foo::create(4)->bar()->bar()->bar();
// Same as previous example
var_dump( $temp->handle );

And this:

// Assuming you made "id" public.
// Foo is destroyed, but "id" isn't a resource.  It will be garbage collected later.
var_dump( Foo::create(5)->id );

This doesn't work:

$temp = Foo::create(6)->handle;
// Nothing has a reference to Foo, it gets destroyed, all resources closed.
var_dump($temp);

Neither does this:

$temp = Foo::create(7);
$handle = $temp->handle;
unset($temp);
// $handle is now a reference to a closed resource because Foo was destroyed
var_dump($handle);

When Foo is destroyed, all open resources (except database links) are closed. References other properties from Foo are still valid.

Demos: https://eval.in/271514

like image 157
Mr. Llama Avatar answered Dec 27 '22 12:12

Mr. Llama


It seems that it's all about variable scoping.

In short if you assign Foo::create() to a global variable you can access the handle in the global scope and the destructor won't be called until the end of the script.

Whereas if you don't actually assign it to a global variable the last method call in the local scope will trigger the destructor; the handle is closed at Foo::create(1)->bar() so ->method is now closed when you're attempting to access it.

Further investigation reveals that premise is flawed - there's definitely something hinky going on here! It only seems to affect resources.


case 1

$foo = Foo::create(1);
var_dump( $foo->bar()->handle );

Results in:

resource(3) of type (stream)

In this case we have assigned the global variable $foo to be a new instance of Foo created with Foo::create(1). We're now accessing that global variable with bar() to return itself and then the public handle.


case 2

$bar = Foo::create(2)->bar();
var_dump( $bar->handle );

Results in:

resource(4) of type (stream)

Again, it's still OK because Foo::create(2) has created a new instance of Foo and bar() has simply returned it (it still had access to it within the local scope). This has been assigned to the global variable $bar and it's from that, that handle is being retrieved.


case 3

var_dump( Foo::create(3)->bar()->handle );

Results in:

resource(5) of type (Unknown)

This is because when Foo::create() returns a new instance of Foo, that's used by bar()... however when bar() closes there's no longer any local use of that instance and the __destruct() method is called which closes the handle. It's the same result you'd get if you simply wrote:

$h = fopen('php://memory', 'r+');
fclose($h);
var_dump($h);

You get exactly the same result if you try:

var_dump( Foo::create(3)->handle );

Foo::create(3) will call the destructor because there are no more local calls to that instance.


EDIT

Further tinkering has muddied the waters further...

I've added this method:

public function handle() {
    return $this->handle;
}

Now if my premise was right, doing:

var_dump( Foo::create(3)->handle() );

should have resulted in:

resource(3) of type (stream)

... but it doesn't, again you get a resource type of Unknown - it seems the destructor is called at return $this before the public class member is accessed! Yet it's absolutely fine to call a method on it:

public function handle() {
    return $this->bar();
}

That will quite happily give you your object back:

object(Foo)#1 (2) {
  ["id":"Foo":private]=>
  int(3)
  ["handle"]=>
  resource(3) of type (stream)
}

It seems there's no way to access resource class members, in this fashion, before the destructor is called?!


As Alex Howansky points out, it's fine with scalars:

public function __destruct() {
    $this->id = 2000;
    fclose($this->handle);
}

public function handle() {
    return $this->id;
}

Now:

var_dump( Foo::create(3)->handle() );

Results in:

int(3)

... the original $id was returned before the destructor was called.

This definitely smells like a bug to me.

like image 28
CD001 Avatar answered Dec 27 '22 12:12

CD001