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

pymethods: support gc protocol #2159

Merged
merged 2 commits into from
Feb 15, 2022
Merged

Conversation

davidhewitt
Copy link
Member

This is the last step for #1884. Copies the same design for __traverse__ and __clear__ from PyGCProtocol onto #[pymethods].

It's not the cleanest implementation, but it works fine. Once merged, we can mark #[pyproto] as deprecated and get on with releasing 0.16 😄

Comment on lines -984 to -1003

/// Enforce at compile time that PyGCProtocol is implemented
fn impl_gc(&self) -> TokenStream {
let cls = self.cls;
let attr = self.attr;
if attr.is_gc {
let closure_name = format!("__assertion_closure_{}", cls);
let closure_token = syn::Ident::new(&closure_name, Span::call_site());
quote! {
fn #closure_token() {
use _pyo3::class;

fn _assert_implements_protocol<'p, T: _pyo3::class::PyGCProtocol<'p>>() {}
_assert_implements_protocol::<#cls>();
}
}
} else {
quote! {}
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This check isn't needed on Python 3.11, however it is needed on older Python versions else #[pyclass(gc)] without __traverse__ will cause segfaults.

I think a better solution might be to deprecate #[pyclass(gc)] and then in src/pyclass.rs we can check for the presence of __traverse__ to decide whether to set the Py_TPFLAGS_HAVE_GC flag.

Copy link
Member Author

Choose a reason for hiding this comment

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

I pushed a commit which deprecates #[pyclass(gc)] as suggested.

@davidhewitt
Copy link
Member Author

I'll leave this one up over the weekend in case anyone wants to review! Then some time next week I'll merge, deprecate #[pyproto], and begin preparing the 0.16 release.

@birkenfeld
Copy link
Member

It seems like you're generating a normal slot for __clear__, while under pyproto, pyo3::class::gc::clear did not have the callback mechanism, and no way to transport exceptions. Are you sure that exceptions can be raised normally in tp_clear? If yes, why do we have the special case for tp_traverse (PyTraverseError could be a normal PyError)?

@davidhewitt
Copy link
Member Author

davidhewitt commented Feb 11, 2022

Good question. I'm not 100% on this; but what I do see is in the usage of tp_clear in the gc module, there's immediately following a check for errors.

https://github.com/python/cpython/blob/af32b3ef1fbad3c2242627a14398320960a0cb45/Modules/gcmodule.c#L1014

Whereas uses of tp_traverse never seem to check for errors. I get the impression that it's not expected for it to be possible to error during GC traversal.

@mejrs
Copy link
Member

mejrs commented Feb 11, 2022

This piece of documentation still mentions pyclass(gc) and needs deleting: https://github.com/PyO3/pyo3/blob/main/pyo3-macros/src/lib.rs#L90

@davidhewitt
Copy link
Member Author

Regarding error messages, it does indeed look like errors in tp_clear are allowed. With this test class

#[pyclass]
struct ErrorDuringClear {
    self_ref: PyObject,
}

#[pymethods]
impl ErrorDuringClear {
    fn __traverse__(&self, visit: PyVisit) -> Result<(), PyTraverseError> {
        visit.call(&self.self_ref)
    }

    fn __clear__(&mut self, py: Python) -> PyResult<()> {
        self.self_ref = py.None();
        Err(exceptions::PyValueError::new_err("clear failed"))
    }
}

I get the following output when the GC runs:

Exception ignored in tp_clear of: <class 'builtins.ErrorDuringClear'>
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ValueError: clear failed

@davidhewitt
Copy link
Member Author

This piece of documentation still mentions pyclass(gc) and needs deleting: https://github.com/PyO3/pyo3/blob/main/pyo3-macros/src/lib.rs#L90

Thanks, pushed!

@birkenfeld
Copy link
Member

Ok, nice. The other question is then if tp_traverse can still execute any Python code (and then ignore the exception), otherwise it probably should be marked unsafe.

@davidhewitt
Copy link
Member Author

Ok, nice. The other question is then if tp_traverse can still execute any Python code (and then ignore the exception), otherwise it probably should be marked unsafe.

I would argue even if it can't execute Python code, it doesn't need to be unsafe. I understand unsafe fn means it is not safe to call; the implementation could always use unsafe internally. In this case it isn't unsafe for other Rust code to call a correct implementation of __traverse__.

We should definitely document that a correct implementation of __traverse__ should do nothing more than use visit.call on all Python objects contained within the struct.

@davidhewitt
Copy link
Member Author

I'm going to merge this shortly and proceed forwards with a final PR to deprecate #[pyproto].

@birkenfeld
Copy link
Member

All the docs still need to be moved from proto-centric to method-centric, with (maybe?) mentioning the old proto stuff.

@davidhewitt
Copy link
Member Author

Agreed, I'll do that in the next PR 👍

@davidhewitt davidhewitt mentioned this pull request Feb 15, 2022
10 tasks
@davidhewitt davidhewitt merged commit 9704c86 into PyO3:main Feb 15, 2022
@davidhewitt davidhewitt deleted the gc-protocol branch February 15, 2022 22:52
@birkenfeld
Copy link
Member

birkenfeld commented Feb 17, 2022

Are you already working on the docs PR? If not I'd make a start today.

@davidhewitt
Copy link
Member Author

See #2173 (comment) - please feel free to take over that branch!

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