New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Catch exceptions without capturing them to variables #5345
Conversation
You will also need to adjust the |
Yeah, I noticed it was leaking, just haven't figured out where it should go yet. |
Adding a check around here should do it: Line 4489 in 21cfa03
RETURN_VALUE_USED do what the code does now, otherwise OBJ_RELEASE(EG(exception)) .
|
👍 for this, I'd rather avoid the unnecessary variable for It'd be useful to add a test of what happens when the destructor gets called, and to fix issues with destructors (keep the test in case this is improperly handled by opcache or the JIT in the future) I'd imagine the handling of __destruct should be like I think it does the right thing right now, because the catch opcode already needs to account for what happens if $e was already an object that could throw in __destruct(). <?php
class ThrowsOnDestruct extends Exception {
public function __destruct() {
echo "Throwing\n";
throw new RuntimeException(__METHOD__);
}
}
try {
throw new ThrowsOnDestruct();
} catch (Exception) {
echo "Unreachable catch\n";
}
echo "Unreachable fallthrough\n"; When I try to run this example (on a branch including this PR and the throws exception PR), I see an unexpected
|
I actually started a patch for this 4 years ago but never forced myself to write an RFC. 😄 So 💯+🥇! |
Implementation looks fine to me. Don't think any opcache changes needed either, as we bail out on try/catch for all the tricky parts. |
@TysonAndre, yep - committed a test for it, investigating how to fix it. |
Zend/tests/try/catch_novar_2.phpt
Outdated
} | ||
try { | ||
throw new ThrowsOnDestruct(); | ||
} catch (Exception $ex) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't $ex
be removed here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I must've reinstated it during my experiments. Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A suggested fix for the test exiting without properly reporting the uncaught exception - @nikic might know more about the edge case I'm worried about.
I'm also not sure why the GC_ADDREF leads to a memory leak when the exception has no variable, but taking it out seems to work.
The test for catch_novar_2.phpt seems to be crashing. I think that EG(exception) should be set to null before calling the destructor on the caught exception
.
I'm not 100% sure of what to do about public function __destruct() { throw $this; }
. The below case is different in that the RuntimeException
is the uncaught exception which still has a reference count, not the ThrowsOnDestruct
. Maybe the object still exists but __destruct
won't be called again?
php > class ThrowsOnDestruct extends Exception { public function __destruct() { echo "throwing self\n"; throw $this; }}
php > $e = new ThrowsOnDestruct();
php > try { throw new RuntimeException(); } catch (Exception $e) {}
throwing self
Warning: Uncaught RuntimeException in php shell code:1
Stack trace:
#0 {main}
Next ThrowsOnDestruct in php shell code:1
Stack trace:
#0 {main}
thrown in php shell code on line 1
diff --git a/Zend/tests/try/catch_novar_2.phpt b/Zend/tests/try/catch_novar_2.phpt
index 60a7ebb283..846fb86484 100644
--- a/Zend/tests/try/catch_novar_2.phpt
+++ b/Zend/tests/try/catch_novar_2.phpt
@@ -15,5 +15,11 @@ try {
}
echo "Unreachable fallthrough\n";
---EXPECT--
+--EXPECTF--
Throwing
+
+Fatal error: Uncaught RuntimeException: ThrowsOnDestruct::__destruct in %scatch_novar_2.php:5
+Stack trace:
+#0 %scatch_novar_2.php(10): ThrowsOnDestruct->__destruct()
+#1 {main}
+ thrown in %scatch_novar_2.php on line 5
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index ab44b9ca7c..20058b5337 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -4493,15 +4493,22 @@ ZEND_VM_HANDLER(107, ZEND_CATCH, CONST, JMP_ADDR, LAST_CATCH|CACHE_SLOT)
}
zval_ptr_dtor(ex);
ZVAL_OBJ(ex, EG(exception));
- } else {
- OBJ_RELEASE(EG(exception));
- }
- if (UNEXPECTED(EG(exception) != exception)) {
- GC_ADDREF(EG(exception));
- HANDLE_EXCEPTION();
+ if (UNEXPECTED(EG(exception) != exception)) {
+ GC_ADDREF(EG(exception));
+ HANDLE_EXCEPTION();
+ } else {
+ EG(exception) = NULL;
+ ZEND_VM_NEXT_OPCODE();
+ }
} else {
EG(exception) = NULL;
- ZEND_VM_NEXT_OPCODE();
+ OBJ_RELEASE(exception);
+ // TODO: What about function __destruct() { throw $this; }
+ if (UNEXPECTED(EG(exception) != NULL)) {
+ HANDLE_EXCEPTION();
+ } else {
+ ZEND_VM_NEXT_OPCODE();
+ }
}
}
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index 7ecc72a280..b8f511075a 100644
--- a/Zend/zend_vm_execute.h
+++ b/Zend/zend_vm_execute.h
@@ -3709,15 +3709,21 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CATCH_SPEC_CONST_HANDLER(ZEND_
}
zval_ptr_dtor(ex);
ZVAL_OBJ(ex, EG(exception));
- } else {
- OBJ_RELEASE(EG(exception));
- }
- if (UNEXPECTED(EG(exception) != exception)) {
- GC_ADDREF(EG(exception));
- HANDLE_EXCEPTION();
+ if (UNEXPECTED(EG(exception) != exception)) {
+ GC_ADDREF(EG(exception));
+ HANDLE_EXCEPTION();
+ } else {
+ EG(exception) = NULL;
+ ZEND_VM_NEXT_OPCODE();
+ }
} else {
EG(exception) = NULL;
- ZEND_VM_NEXT_OPCODE();
+ OBJ_RELEASE(exception);
+ if (UNEXPECTED(EG(exception) != NULL)) {
+ HANDLE_EXCEPTION();
+ } else {
+ ZEND_VM_NEXT_OPCODE();
+ }
}
}
echo "Unreachable fallthrough\n"; | ||
|
||
--EXPECT-- | ||
Throwing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks as if the php process exited - normally, if it rethrew, php's output would include a message about an uncaught exception and a stack trace.
The RFC is currently being drafted at https://wiki.php.net/rfc/non-capturing_catches
I've pushed a fix for the throw in dtor issue, basically following @TysonAndre's suggestion above. @TysonAndre 81c824f should make it more obvious why the GC_ADDREF doesn't make sense in this case.
That's correct. |
zval *ex = EX_VAR(opline->result.var); | ||
if (UNEXPECTED(Z_ISREF_P(ex))) { | ||
ex = Z_REFVAL_P(ex); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm... not related to your patch, but I just realized that this can bypass typed references.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
…ch` sniff > Allow catching exceptions without capturing them to variables Refs: * https://wiki.php.net/rfc/non-capturing_catches * php/php-src#5345 * php/php-src@23ee4d4 Includes unit tests.
See the RFC currently under discussion: https://wiki.php.net/rfc/non-capturing_catches