-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Use PyErr_WriteUnraisable when an error occurs in a destructor #2358
Conversation
I'm not sure about this. Calling I believe @jbarlow83 was still developing an extra utility that would catch error and call In principle, a |
The python docs say that
I imagine if you needed to have a destructor that called python code, you would need to do something like this?
|
Yes, I've spent a while on this. It's all pretty complicated and nothing is obvious without some study. What I do like about this PR is that @virtuald and and I have kind of independently converged on a similar solution for That said I do have a few concerns with this PR as written which I will tag. One thing that is obvious - whatever we do - is we need to expand the documentation on destructors to discuss the issues. I've already ready drafted some material for that, because I think there should be a thorough explanation. (Quite busy with other work right now.)
Python complains if:
There could be some One case that could be missing a
Anyway, the template function I was considering now looks like this: // C++ runtime errors are the responsibility of the user, this is only for Python errors
template <typename Func>
inline void destructor_exception_trap(Func f) {
try {
f();
} catch (error_already_set &e) { // trap any thrown py::error_already_set
e.restore(); // sets error
} catch (builtin_exception &e) { // trap py::value_error, etc.
e.set_error(); // sets error
}
}
// Usage
struct PythonRaiseInDestructor {
PythonRaiseInDestructor(const py::str &s) : s(s) {}
~PythonRaiseInDestructor() {
py::destructor_exception_trap([&]() {
throw py::value_error(s);
});
}
py::str s;
}; What I like about this is the necessary steps are sufficient complex that they ought to be encapsulated, and encapsulating them makes it easier to apply fixes universally or improve the solution. What I don't like is hiding a try-catch. |
def test_pyexception_destructor(): | ||
import sys | ||
|
||
if hasattr(sys, "unraisablehook"): |
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.
Only Python 3.8+ have unraisablehook
. I think for earlier versions we could call a subprocess and check its stderr.
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.
That's why I made it conditional. If the logic works on one version of python since this is so esoteric I didn't think it was worth testing on the older versions too.
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.
pybind11 has some way of capturing the stderr. It would definitely be good to test on all Python versions, if possible (especially Python 2, which tends to act up/do things differently!).
@@ -1406,6 +1406,9 @@ class class_ : public detail::generic_type { | |||
); | |||
} | |||
v_h.value_ptr() = nullptr; | |||
if (PyErr_Occurred()) { | |||
PyErr_WriteUnraisable(v_h.type ? (PyObject*)v_h.type->type : nullptr); |
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.
What's the reason for passing the type
rather than the value here? According to the docs:
The function is called with a single argument obj that identifies the context in which the unraisable exception occurred. If possible, the repr of obj will be printed in the warning message.
As written it would print Exception ignored in: ValueError
(since repr(v_h.type->type)
is ValueError). It would be better if we could find a way to get better information to the user, e.g. the name of the destructor.
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.
v_h.type->type
is the class, not the exception.
I assumed that if I hit an error here, then the value no longer existed so a __repr__
wouldn't be sensible? I'm not sure if the name is still around, though I guess it must be?
Though, you're right, it would be good to do something more sensible. Python does this:
$ cat t.py
class X:
def __del__(self):
raise ValueError("something")
X()
$ python t.py
Exception ignored in: <function X.__del__ at 0x7fd070258f28>
Traceback (most recent call last):
File "t.py", line 3, in __del__
raise ValueError("something")
ValueError: something
As mentioned above, here's what I currently have:
Exception ignored in: <class 'pybind11_tests.class_.PyRaiseDestructor'>
Traceback (most recent call last):
File "<snip>/pybind11/tests/test_class.py", line 389, in test_pyexception_destructor
m.PyRaiseDestructor()
ValueError: unraisable error
I suppose we could add a name and a pointer value?
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.
Actually it's probably good as is, since printing the name of the offending class gives a good idea as to where the occurred.
We definitely don't want to call any extra functions in this type of code, using what is available is best.
I was contemplating something similar to
I agree. I think if we update the documentation with a version of what's in these comments, then that would be sufficient. One case that I was a bit worried about was if I had an object that had a |
I'm not disputing this is the correct functionality to call. I'm saying that
Exactly. Your PR won't fix this, because the thrown exception will abort your |
Sure, yes.
I'm not sure about this, though. Again, we never have pybind11 set any error without
Exactly. But these can only occur by accident. I think these concerns are orthogonal to this current problem/PR?
That's a bug. If you find those, we should fix them (rather than implementing a workaround here)! :-) |
Ahh, now I see. The idea in the PR amounts to saying that in the case of a destructor, pybind11 would possibly exit with the destructor with an error, and the The increasing complexity of handling this justifies adding a helper function to support it. // C++ runtime errors are the responsibility of the user, this is only for Python errors
template <typename Func>
inline void destructor_exception_trap(const char *caller, Func f) {
bool unraisable = false;
try {
f();
} catch (error_already_set &e) { // trap any thrown py::error_already_set
e.restore();
unraisable = true;
} catch (builtin_exception &e) { // trap py::value_error, etc.
e.set_error(); // sets error
unraisable = true;
}
if (unraisable) {
PyErr_WriteUnraisable(py::str(caller).ptr());
}
}
// Usage
destroyme::~destroyme { destructor_exception_trap(__func__, []() { ... } } I'm not sure if there's a reasonable way to obtain the associated PyTypeObject* from inside that template function (if one exists). Automating the passing of |
Right, using the Python C API error indicator (basically a global variable) to bypass a
I'm not sure it's an explicit "contract", but that's what I got from working with pybind11 anyway. Feel free to correct me if necessary :-)
Good point! Hadn't even thought of that. I have a suggestion, to resolve this:
|
This thing is a quagmire. I realized, for consistency, we ought to also run any exception translators that the user registered, i.e. running the equivalent of the As such I am proposing that we add A few days ago I tried a |
Minor improvement to #2342
With the default hook, you end up with an error message looking something like so: