Skip to content

Commit 66e3dbf

Browse files
committed
Defer object destructors to the next VM safepoint
Run an object's __destruct at the next VM safepoint instead of inline at the point its refcount reaches zero, so a destructor can no longer reenter the VM in the middle of an opcode's operand cleanup. Logical death stays synchronous: a WeakReference reads null and a WeakMap entry is gone at the drop, and only the physical destructor call and the free are deferred. Safepoints are entry into a user function, method, closure, magic method or hook, a statement-level internal call, every try-region boundary (entry, normal exit, return/break/continue/goto/throw out of a try, and the end of a finally body), a loop back-edge, and script or fiber end. Because a flushed destructor exception is thrown from an opline still inside the try region that owns it, the existing exception table delivers it to the same catch that would have caught an inline throw; region scoping is positional and needs no per-region runtime state. Several destructor exceptions at one safepoint compose into a single getPrevious chain, an exit or unwind exception wins over a destructor exception, and every object is destroyed exactly once. Behavior is identical across the interpreter, tracing JIT, and function JIT. Generators and fibers carry their own deferred-destructor queue, so a destructor deferred inside one runs there rather than in the consumer.
1 parent c7265d4 commit 66e3dbf

100 files changed

Lines changed: 3273 additions & 885 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Zend/tests/bug38220.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ A Object
6767
)
6868
func1(): 1
6969
after call func1
70+
before call func1
7071
A::__destruct()
7172
before call close
7273
A Object
@@ -75,7 +76,6 @@ A Object
7576
)
7677
close(): 1
7778
after call close
78-
before call func1
7979
A Object
8080
(
8181
[i] => 2
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
--TEST--
2+
Deferred destructor: a throwing destructor dropped in a try is caught by that try when the try is left via return/break/continue
3+
--FILE--
4+
<?php
5+
class D {
6+
public function __construct(public string $id) {}
7+
public function __destruct() { throw new Exception("dtor {$this->id}"); }
8+
}
9+
10+
function via_return() {
11+
try {
12+
$d = new D("r");
13+
unset($d);
14+
return "returned";
15+
} catch (Exception $e) {
16+
echo "return-case caught: {$e->getMessage()}\n";
17+
return "caught";
18+
}
19+
}
20+
var_dump(via_return());
21+
22+
function via_break() {
23+
foreach ([1] as $v) {
24+
try {
25+
$d = new D("b");
26+
unset($d);
27+
break;
28+
} catch (Exception $e) {
29+
echo "break-case caught: {$e->getMessage()}\n";
30+
}
31+
}
32+
return "done";
33+
}
34+
var_dump(via_break());
35+
36+
function via_continue() {
37+
foreach ([1, 2] as $v) {
38+
try {
39+
$d = new D("c$v");
40+
unset($d);
41+
continue;
42+
} catch (Exception $e) {
43+
echo "continue-case caught: {$e->getMessage()}\n";
44+
}
45+
}
46+
return "done";
47+
}
48+
var_dump(via_continue());
49+
?>
50+
--EXPECT--
51+
return-case caught: dtor r
52+
string(6) "caught"
53+
break-case caught: dtor b
54+
string(4) "done"
55+
continue-case caught: dtor c1
56+
continue-case caught: dtor c2
57+
string(4) "done"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
--TEST--
2+
Deferred destructor: exit() in a destructor wins over a throwing destructor in the same flush, either order
3+
--FILE--
4+
<?php
5+
class Exiter {
6+
function __destruct() {
7+
echo "Exiter dtor\n";
8+
exit(7);
9+
}
10+
}
11+
class Thrower {
12+
public $id;
13+
function __construct(int $id) { $this->id = $id; }
14+
function __destruct() {
15+
echo "Thrower {$this->id} dtor\n";
16+
throw new Exception("thrown {$this->id}");
17+
}
18+
}
19+
20+
register_shutdown_function(function () { echo "shutdown\n"; });
21+
22+
// Thrower deferred before Exiter, then Exiter: exit must win over the pending throw.
23+
$t = new Thrower(1);
24+
$t = null;
25+
$e = new Exiter();
26+
$e = null;
27+
28+
// A safepoint flushes the batch. exit(7) wins; no user code runs afterwards.
29+
(function () {})();
30+
31+
echo "UNREACHABLE\n";
32+
?>
33+
--EXPECT--
34+
Thrower 1 dtor
35+
Exiter dtor
36+
shutdown
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
Deferred destructor: a destructor dropped in a finally body composes with a destructor exception pending from the protected block and is caught by the enclosing try
3+
--FILE--
4+
<?php
5+
class D {
6+
public function __construct(public string $id) {}
7+
public function __destruct() { throw new Exception("dtor {$this->id}"); }
8+
}
9+
function f() {
10+
try {
11+
try {
12+
$a = new D("try");
13+
unset($a);
14+
return "unreached";
15+
} finally {
16+
$b = new D("finally");
17+
unset($b);
18+
echo "finally body ran\n";
19+
}
20+
} catch (Exception $e) {
21+
echo $e::class, ": ", $e->getMessage(), "\n";
22+
echo "previous: ", $e->getPrevious()?->getMessage() ?? "none", "\n";
23+
return "caught";
24+
}
25+
}
26+
var_dump(f());
27+
?>
28+
--EXPECT--
29+
finally body ran
30+
Exception: dtor finally
31+
previous: dtor try
32+
string(6) "caught"
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
--TEST--
2+
Deferred destructor: a throwing destructor deferred in a finally runs exactly once
3+
--FILE--
4+
<?php
5+
class D {
6+
function __destruct() {
7+
echo "dtor\n";
8+
throw new Exception("boom");
9+
}
10+
}
11+
function g() {
12+
try {
13+
return 1;
14+
} finally {
15+
$x = new D();
16+
unset($x);
17+
}
18+
}
19+
g();
20+
echo "after g\n";
21+
?>
22+
--EXPECTF--
23+
dtor
24+
25+
Fatal error: Uncaught Exception: boom in %s:%d
26+
Stack trace:
27+
#0 %s(%d): D->__destruct()
28+
#1 %s(%d): g()
29+
#2 {main}
30+
thrown in %s on line %d
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
--TEST--
2+
Deferred destructor: a drop inside a GC destructor of a condemned sibling runs inline
3+
--FILE--
4+
<?php
5+
$log = 0;
6+
class Node {
7+
public $next;
8+
public $peer;
9+
function __destruct() {
10+
global $log;
11+
$log++;
12+
$this->peer = null;
13+
}
14+
}
15+
16+
for ($r = 0; $r < 2000; $r++) {
17+
$a = new Node();
18+
$b = new Node();
19+
$c = new Node();
20+
$a->next = $b;
21+
$b->next = $c;
22+
$c->next = $a;
23+
$a->peer = $c;
24+
$b->peer = $a;
25+
$c->peer = $b;
26+
unset($a, $b, $c);
27+
if ($r % 500 === 0) {
28+
gc_collect_cycles();
29+
}
30+
}
31+
gc_collect_cycles();
32+
echo "log=$log\n";
33+
?>
34+
--EXPECT--
35+
log=6000
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
Deferred destructors flush at loop back-edges
3+
--INI--
4+
opcache.enable_cli=0
5+
--FILE--
6+
<?php
7+
$dtors = 0;
8+
9+
class C {
10+
public function __destruct() {
11+
global $dtors;
12+
$dtors++;
13+
}
14+
}
15+
16+
for ($i = 0; $i < 3; $i++) {
17+
$object = new C;
18+
unset($object);
19+
echo $i, ":", $dtors, "\n";
20+
}
21+
22+
echo "done:", $dtors, "\n";
23+
?>
24+
--EXPECT--
25+
0:0
26+
1:1
27+
2:2
28+
done:2
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
--TEST--
2+
Deferred destructor: nested try regions scope the deferred queue; each region's destructor surfaces at its own level and a finally-return discards only its own
3+
--FILE--
4+
<?php
5+
class D {
6+
public function __construct(public string $id) {}
7+
public function __destruct() { throw new Exception("dtor {$this->id}"); }
8+
}
9+
10+
function inner_level() {
11+
try {
12+
try {
13+
$d = new D("inner");
14+
unset($d);
15+
return "inner-try-return";
16+
} catch (Exception $e) {
17+
echo "inner caught: {$e->getMessage()}\n";
18+
return "inner-catch";
19+
}
20+
} catch (Exception $e) {
21+
echo "outer WRONGLY caught: {$e->getMessage()}\n";
22+
return "outer-catch";
23+
}
24+
}
25+
var_dump(inner_level());
26+
27+
function outer_survives() {
28+
try {
29+
$d = new D("outer");
30+
unset($d);
31+
try {
32+
return "inner-try-return";
33+
} finally {
34+
return "inner-finally-return";
35+
}
36+
} catch (Exception $e) {
37+
echo "outer caught: {$e->getMessage()}\n";
38+
return "outer-catch";
39+
}
40+
}
41+
var_dump(outer_survives());
42+
43+
function finally_discards() {
44+
try {
45+
$d = new D("discarded");
46+
unset($d);
47+
return "try-return";
48+
} finally {
49+
return "finally-return";
50+
}
51+
}
52+
var_dump(finally_discards());
53+
?>
54+
--EXPECT--
55+
inner caught: dtor inner
56+
string(11) "inner-catch"
57+
outer caught: dtor outer
58+
string(11) "outer-catch"
59+
string(14) "finally-return"
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
--TEST--
2+
Deferred destructor: a throwing destructor pending at an explicit throw preempts it, whether dropped before the statement or during the throw operand's evaluation, so no spurious exception chain is created
3+
--FILE--
4+
<?php
5+
class D {
6+
public function __destruct() { throw new Exception("dtor"); }
7+
}
8+
9+
// The throw operand is a pre-existing object; the destructor is dropped before it.
10+
function rethrow_case() {
11+
try {
12+
try {
13+
throw new Exception("original");
14+
} catch (Exception $orig) {
15+
$x = new D();
16+
unset($x);
17+
throw $orig;
18+
}
19+
} catch (Exception $e) {
20+
echo "rethrow: ", $e->getMessage(), " prev=", $e->getPrevious()?->getMessage() ?? "none", "\n";
21+
}
22+
}
23+
rethrow_case();
24+
25+
// The throw operand is a call that drops the destructor during its evaluation.
26+
function make(Exception $e) {
27+
$x = new D();
28+
unset($x);
29+
return $e;
30+
}
31+
function operand_call_case() {
32+
$pre = new Exception("explicit");
33+
try {
34+
throw make($pre);
35+
} catch (Exception $e) {
36+
echo "operand-call: ", $e->getMessage(), " prev=", $e->getPrevious()?->getMessage() ?? "none", "\n";
37+
}
38+
}
39+
operand_call_case();
40+
?>
41+
--EXPECT--
42+
rethrow: dtor prev=none
43+
operand-call: dtor prev=none
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
--TEST--
2+
Deferred destructor: an uncatchable throwing destructor pending at an explicit throw or internal call preempts it positionally instead of being held to shutdown
3+
--FILE--
4+
<?php
5+
class D {
6+
public function __destruct() { throw new Exception("dtor"); }
7+
}
8+
$d = new D();
9+
$e = new Exception("main");
10+
unset($d);
11+
throw $e;
12+
?>
13+
--EXPECTF--
14+
Fatal error: Uncaught Exception: dtor in %s:%d
15+
Stack trace:
16+
#0 %s(%d): D->__destruct()
17+
#1 {main}
18+
thrown in %s on line %d

0 commit comments

Comments
 (0)