Skip to content
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

Proto #1: Exception instances as Py<BaseException> #686

Closed
wants to merge 1 commit into from

Conversation

nagisa
Copy link
Contributor

@nagisa nagisa commented Dec 9, 2019

This prototype implements use of Py<BaseException> as the instance to
use for exception instances. These instances integrate reasonably well
with the Rust’s standard error mechanisms by implementing the Display
and Error traits. These error types can also be stored into e.g.
failure::Fails or other error types as a cause of some greater error.

There are a couple of sketchy points in this PR most notable of which is
the implementation of std::error::Error::source. See the included FIXME.

This is also somewhat of an experiment around the ownership-based Py<>
that I have outlined in #679 and the issues
implementing source appear to stem from exactly the fact that Python’s GIL
and Rust’s ownership are fairly incompatible.

FWIW: I think this might be revealing a soundness issue in our current setup too.

This prototype implements use of Py<BaseException> as the instance to
use for exception instances. These instances integrate reasonably well
with the Rust’s standard error mechanisms by implementing the `Display`
and `Error` traits. These error types can also be stored into e.g.
`failure::Fail`s or other error types as a cause of some greater error.
@nagisa
Copy link
Contributor Author

nagisa commented Dec 9, 2019

cc @kngwyu

impl std::error::Error for BaseException {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
unsafe {
// Returns either `None` or an instance of an exception.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to obtain the GIL.

unsafe impl PyTypeObject for $name {
fn init_type() -> std::ptr::NonNull<$crate::ffi::PyTypeObject> {
unsafe { std::ptr::NonNull::new_unchecked(ffi::$exc_name as *mut _) }
}
}

impl<'v> PyTryFrom<'v> for $name {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upcasting to PyAny is indeed a missing part 👍

impl std::fmt::Debug for BaseException {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// Sneaky: we don’t really need a GIL lock here as nothing should be able to just mutate
// the "type" of an object, right? RIGHT???
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I also am not sure 😓
We need tests.

let py_type_name = unsafe { CStr::from_ptr((*(*self.as_ptr()).ob_type).tp_name) };
let type_name = py_type_name.to_string_lossy();
f.debug_struct(&*type_name)
// TODO: print out actual fields!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can access actual field only by type name?
Or we need to get the global PyExc_~s here?

Copy link
Contributor Author

@nagisa nagisa Dec 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "actual fields" I meant the fields of the PyException_HEAD structure component, which is defined as such:

#define PyException_HEAD PyObject_HEAD PyObject *dict;\
             PyObject *args; PyObject *traceback;\
             PyObject *context; PyObject *cause;\
             char suppress_context;

At very least values of args, traceback, context, cause and suppress_context are interesting, but I can imagine it being useful to also print out at least the refcount from PyObject_HEAD too.

f.debug_struct(&*type_name) is just a builder for debug output. See https://doc.rust-lang.org/stable/std/fmt/struct.Formatter.html#method.debug_struct. The final result should look something like

f.debug_struct(type_name).field("args", &self.args).field("traceback", &self.traceback)/* etc... */.finish()

Comment on lines +453 to +455
// must return a `&Py<BaseException>` instead… but we cannot do that because
// nothing is storing such a thing anywhere and thus we cannot take a reference to
// that…
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I feel it difficult to understand why we cannot use Py<Exception> here... 🤔

Copy link
Contributor Author

@nagisa nagisa Dec 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot return a Py<Exception> because the trait contract requires:

fn source(&self) -> Option<  &  (dyn std::error::Error + 'static)>

The most important detail is that this method is returning a reference with a lifetime tied to &self (I tried to make it more visible by adding whitespace around it). In other words, it requires that you return a reference to something in Self. Trying to return a Py<BaseException> would result in the typical "value does not live long enough" error, very much like the snippet below would:

fn bogus(&self) -> &str {
    &String::from("banana") // cannot compile because `String` does not live long enough...
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks.

@kngwyu
Copy link
Member

kngwyu commented Dec 13, 2019

Thank you, this would be a big step for us.

let gil = Python::acquire_gil();
let py = gil.python();
if let Ok(s) = crate::ObjectProtocol::str(&*py_self.as_ref(py)) {
write!(f, ": {}", &s.to_string_lossy())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FIXME: if the resulting string is empty we want to not add the : .

@davidhewitt
Copy link
Member

@nagisa I'm curious if you plan to continue with this PR? If not, I'd be happy to take it on, rebase it on master and address the changes reviewed here.

@nagisa
Copy link
Contributor Author

nagisa commented Apr 11, 2020

@davidhewitt you’re welcome to take over this. I still think it is very worthwhile to do this but I also no longer have much drive to work on this feature as I implemented something simpler, although less elegant, directly in my code.

@kngwyu
Copy link
Member

kngwyu commented Jun 2, 2020

I noticed we lack the link to the most relevant issue #682

@davidhewitt
Copy link
Member

I have a PR which is almost ready to open as a draft to replace this one.

I've stuck to the original idea here but gone a bit further: &BaseException and Py<BaseException> implement std::error::Error.

&BaseException turns out to be pretty nice here - if we bound &BaseException by the GIL lifetime in the normal way, then the lifetime issue above related to Error::cause goes away. (Because the returned reference can be an owned &BaseException reference tied to the GIL lifetime.)

@davidhewitt
Copy link
Member

Superseded by #1024

@davidhewitt davidhewitt closed this Jul 6, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants