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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 14 additions & 7 deletions src/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,12 +299,13 @@ impl PyErr {
/// Retrieves the exception instance for this error.
/// This method takes `mut self` because the error might need
/// to be normalized in order to create the exception instance.
fn instance(mut self, py: Python) -> PyObject {
fn instance(mut self, py: Python) -> Py<exceptions::BaseException> {
self.normalize(py);
match self.pvalue {
PyErrValue::Value(ref instance) => instance.clone_ref(py),
_ => py.None(),
}
let r = match self.pvalue {
PyErrValue::Value(ref instance) => instance.clone_ref(py).extract(py),
_ => Err(PyDowncastError.into()),
};
r.expect("Normalized error instance should be a BaseException")
}

/// Writes the error back to the Python interpreter's global state.
Expand Down Expand Up @@ -370,6 +371,12 @@ impl std::fmt::Debug for PyErr {
}

impl FromPy<PyErr> for PyObject {
fn from_py(other: PyErr, py: Python) -> Self {
other.instance(py).into()
}
}

impl FromPy<PyErr> for Py<exceptions::BaseException> {
fn from_py(other: PyErr, py: Python) -> Self {
other.instance(py)
}
Expand All @@ -378,14 +385,14 @@ impl FromPy<PyErr> for PyObject {
impl ToPyObject for PyErr {
fn to_object(&self, py: Python) -> PyObject {
let err = self.clone_ref(py);
err.instance(py)
err.instance(py).into()
}
}

impl<'a> IntoPy<PyObject> for &'a PyErr {
fn into_py(self, py: Python) -> PyObject {
let err = self.clone_ref(py);
err.instance(py)
err.instance(py).into()
}
}

Expand Down
171 changes: 170 additions & 1 deletion src/exceptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::type_object::PyTypeObject;
use crate::types::{PyAny, PyTuple};
use crate::Python;
use crate::{AsPyPointer, ToPyObject};
use crate::{AsPyRef, Py, PyDowncastError, PyTryFrom};
use std::ffi::CStr;
use std::os::raw::c_char;
use std::{self, ops};
Expand Down Expand Up @@ -208,11 +209,13 @@ macro_rules! impl_native_exception (
PyErr::new::<$name, _>(())
}
}

impl<T> std::convert::Into<$crate::PyResult<T>> for $name {
fn into(self) -> $crate::PyResult<T> {
PyErr::new::<$name, _>(()).into()
}
}

impl $name {
pub fn py_err<V: ToPyObject + 'static>(args: V) -> PyErr {
PyErr::new::<$name, V>(args)
Expand All @@ -221,11 +224,71 @@ macro_rules! impl_native_exception (
PyErr::new::<$name, V>(args).into()
}
}

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 👍

fn try_from<V: Into<&'v PyAny>>(value: V) -> Result<&'v Self, PyDowncastError> {
unsafe {
let value = value.into();
if ffi::PyObject_TypeCheck(value.as_ptr(), ffi::$exc_name as *mut _) != 0 {
Ok(PyTryFrom::try_from_unchecked(value))
} else {
Err(PyDowncastError)
}
}
}

fn try_from_exact<V: Into<&'v PyAny>>(value: V) -> Result<&'v Self, PyDowncastError> {
unsafe {
let value = value.into();
if (*value.as_ptr()).ob_type == ffi::$exc_name as *mut _ {
Ok(PyTryFrom::try_from_unchecked(value))
} else {
Err(PyDowncastError)
}
}
}

fn try_from_mut<V: Into<&'v PyAny>>(value: V) -> Result<&'v mut Self, PyDowncastError> {
unsafe {
let value = value.into();
if ffi::PyObject_TypeCheck(value.as_ptr(), ffi::$exc_name as *mut _) != 0 {
Ok(PyTryFrom::try_from_mut_unchecked(value))
} else {
Err(PyDowncastError)
}
}
}
fn try_from_mut_exact<V: Into<&'v PyAny>>(value: V) -> Result<&'v mut Self, PyDowncastError> {
unsafe {
let value = value.into();
if (*value.as_ptr()).ob_type == ffi::$exc_name as *mut _ {
Ok(PyTryFrom::try_from_mut_unchecked(value))
} else {
Err(PyDowncastError)
}
}
}

unsafe fn try_from_unchecked<V: Into<&'v PyAny>>(value: V) -> &'v Self {
&*(value.into().as_ptr() as *const _)
}

unsafe fn try_from_mut_unchecked<V: Into<&'v PyAny>>(value: V) -> &'v mut Self {
&mut *(value.into().as_ptr() as *mut _)
}
}

impl AsPyPointer for $name {
fn as_ptr(&self) -> *mut ffi::PyObject {
return self as *const _ as *const _ as *mut ffi::PyObject;
}
}
);
);

Expand Down Expand Up @@ -340,6 +403,72 @@ impl StopIteration {
}
}

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 gil = Python::acquire_gil();
// let _py = gil.python();
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()

.finish()
}
}

impl std::fmt::Display for BaseException {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let py_type_name = unsafe { CStr::from_ptr((*(*self.as_ptr()).ob_type).tp_name) };
let type_name = py_type_name.to_string_lossy();
write!(f, "{}", type_name)?;
let py_self: Py<PyAny> = unsafe { Py::from_borrowed_ptr(self.as_ptr()) };

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 : .

} else {
write!(f, ": <exception str() failed>")
}
}
}

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.

let cause_object = ffi::PyException_GetCause(self.as_ptr());
if cause_object == ffi::Py_None() {
None
} else {
// FIXME: PyException_GetCause returns a new reference to the cause object.
//
// While we know that `self` is "immutable" (`&self`!) it is also true that between
// now and when the return value is actually read the GIL could be unlocked and
// then concurrent threads could modify `self` to change its `__cause__`.
//
// If this was not a possibility, we could just `DECREF` here, instead, now, we
// 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…
Comment on lines +453 to +455
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.

//
// The only way to make this function to work sanely, without leaking, is to ensure
// that between a call to `Error::source` and drop of the reference there’s no
// possible way for for the object to be modified. Even if we had a way to prevent
// GIL from unlocking, people could modify the object through a different
// reference to the Exception.
//
// Sounds unsound, doesn’t it?
//
// ffi::Py_DECREF(cause_object);
Some(&*(cause_object as *const _ as *const BaseException))
}
}
}
}

/// Exceptions defined in `asyncio` module
pub mod asyncio {
import_exception!(asyncio, CancelledError);
Expand All @@ -363,7 +492,9 @@ mod test {
use crate::exceptions::Exception;
use crate::objectprotocol::ObjectProtocol;
use crate::types::{IntoPyDict, PyDict};
use crate::{PyErr, Python};
use crate::{AsPyPointer, FromPy, Py, PyErr, Python};
use std::error::Error;
use std::fmt::Write;

import_exception!(socket, gaierror);
import_exception!(email.errors, MessageError);
Expand Down Expand Up @@ -441,4 +572,42 @@ mod test {
)
.unwrap();
}

#[test]
fn native_exception_display() {
let mut out = String::new();
let gil = Python::acquire_gil();
let py = gil.python();
let result = py
.run("raise Exception('banana')", None, None)
.expect_err("raising should have given us an error");
let convert = Py::<super::BaseException>::from_py(result, py);
write!(&mut out, "{}", convert).expect("successful format");
assert_eq!(out, "Exception: banana");
}

#[test]
fn native_exception_chain() {
let mut out = String::new();
let gil = Python::acquire_gil();
let py = gil.python();
let result = py
.run(
"raise Exception('banana') from TypeError('peach')",
None,
None,
)
.expect_err("raising should have given us an error");
let convert = Py::<super::BaseException>::from_py(result, py);
write!(&mut out, "{}", convert).expect("successful format");
assert_eq!(out, "Exception: banana");
out.clear();
let convert_ref: &super::BaseException =
unsafe { &*(convert.as_ptr() as *const _ as *const _) };
let source = convert_ref.source().expect("cause should exist");
write!(&mut out, "{}", source).expect("successful format");
assert_eq!(out, "TypeError: peach");
let source_source = source.source();
assert!(source_source.is_none(), "source_source should be None");
}
}
61 changes: 57 additions & 4 deletions src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::objectprotocol::ObjectProtocol;
use crate::type_object::PyTypeCreate;
use crate::type_object::{PyTypeInfo, PyTypeObject};
use crate::types::PyAny;
use crate::{ffi, IntoPy};
use crate::{ffi, IntoPy, PyTryFrom};
use crate::{AsPyPointer, FromPyObject, FromPyPointer, IntoPyPointer, Python, ToPyObject};
use std::marker::PhantomData;
use std::mem;
Expand Down Expand Up @@ -54,7 +54,6 @@ pub unsafe trait PyNativeType: Sized {
/// let d = [("p", obj)].into_py_dict(py);
/// py.run("assert p.length() == 12", None, Some(d)).unwrap();
/// ```
#[derive(Debug)]
pub struct PyRef<'a, T: PyTypeInfo>(&'a T, Unsendable);

#[allow(clippy::cast_ptr_alignment)]
Expand Down Expand Up @@ -82,6 +81,24 @@ where
}
}

impl<'a, T: std::fmt::Display + PyTypeInfo> std::fmt::Display for PyRef<'a, T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
unsafe {
let gil = Python::acquire_gil();
let py = gil.python();
let object = self.to_object(py);
let obj_ref: &T = PyTryFrom::try_from_unchecked(object.as_ref(py));
std::fmt::Display::fmt(&obj_ref, f)
}
}
}

impl<'a, T: PyTypeInfo> std::fmt::Debug for PyRef<'a, T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_tuple("PyRef").field(&(self.0 as *const _)).finish()
}
}

impl<'a, T: PyTypeInfo> AsPyPointer for PyRef<'a, T> {
fn as_ptr(&self) -> *mut ffi::PyObject {
ref_to_ptr(self.0)
Expand Down Expand Up @@ -142,7 +159,6 @@ where
/// obj.x = 5; obj.y = 20;
/// py.run("assert p.length() == 100", None, Some(d)).unwrap();
/// ```
#[derive(Debug)]
pub struct PyRefMut<'a, T: PyTypeInfo>(&'a mut T, Unsendable);

impl<'a, T: PyTypeInfo> PyRefMut<'a, T> {
Expand All @@ -162,6 +178,26 @@ where
}
}

impl<'a, T: std::fmt::Display + PyTypeInfo> std::fmt::Display for PyRefMut<'a, T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
unsafe {
let gil = Python::acquire_gil();
let py = gil.python();
let object = self.to_object(py);
let obj_ref: &T = PyTryFrom::try_from_unchecked(object.as_ref(py));
std::fmt::Display::fmt(&obj_ref, f)
}
}
}

impl<'a, T: PyTypeInfo> std::fmt::Debug for PyRefMut<'a, T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_tuple("PyRefMut")
.field(&(self.0 as *const _))
.finish()
}
}

impl<'a, T: PyTypeInfo> AsPyPointer for PyRefMut<'a, T> {
fn as_ptr(&self) -> *mut ffi::PyObject {
ref_to_ptr(self.0)
Expand Down Expand Up @@ -265,7 +301,6 @@ pub trait AsPyRef<T: PyTypeInfo>: Sized {
/// Safe wrapper around unsafe `*mut ffi::PyObject` pointer with specified type information.
///
/// `Py<T>` is thread-safe, because any python related operations require a Python<'p> token.
#[derive(Debug)]
#[repr(transparent)]
pub struct Py<T>(NonNull<ffi::PyObject>, PhantomData<T>);

Expand Down Expand Up @@ -357,6 +392,24 @@ impl<T> Py<T> {
}
}

impl<T: std::fmt::Display + for<'a> PyTryFrom<'a>> std::fmt::Display for Py<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
unsafe {
let gil = Python::acquire_gil();
let py = gil.python();
let object = self.to_object(py);
let obj_ref: &T = PyTryFrom::try_from_unchecked(object.as_ref(py));
std::fmt::Display::fmt(&obj_ref, f)
}
}
}

impl<T> std::fmt::Debug for Py<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_tuple("Py").field(&self.0.as_ptr()).finish()
}
}

/// Specialization workaround
trait AsPyRefDispatch<T: PyTypeInfo>: AsPyPointer {
fn as_ref_dispatch(&self, _py: Python) -> &T;
Expand Down