introduce PyClassGuard(Mut) as future replacement for PyRef(Mut)#5233
introduce PyClassGuard(Mut) as future replacement for PyRef(Mut)#5233davidhewitt merged 8 commits intoPyO3:mainfrom
PyClassGuard(Mut) as future replacement for PyRef(Mut)#5233Conversation
2b545b3 to
2e8d77d
Compare
davidhewitt
left a comment
There was a problem hiding this comment.
Thanks for experimenting with this, it seems pretty promising to me.
A few initial observations as comments.
I think also the most interesting bit will be replacing extract_pyclass_ref[_mut] in the macro machinery with this new type, I think that would manifest the performance gains we saw in #4720
src/pyclassguard.rs
Outdated
| //! tracked dynamically at runtime, using [`PyClassGuard`] and the other types | ||
| //! defined in this module. This works similar to std's | ||
| //! [`RefCell`](std::cell::RefCell) type. | ||
| //! |
There was a problem hiding this comment.
I wonder whether it's worth adding to this comment some notes around how PyClassGuard is somewhat prone to errors under free-threading, and using #[pyclass(frozen)] with blocking access of something like a Mutex to internal state may be better depending on the program requirements.
There was a problem hiding this comment.
I added a few words here, let me know what you think
32ab27b to
c0d6a2f
Compare
cd4fbcc to
824034d
Compare
|
I think this is mostly ready now. A couple of points:
|
One idea which struck me is maybe we can have something like The idea would be that this could maybe replace the current (frozen) keyword:
I guess it depends if we like the above semantics, i.e.
I'm ok for not including them for now, we had reasons that we couldn't do Also, I guess we need to apply |
That sounds like a super interesting idea!, Definitely worth exploring. Something that's unclear is how this interacts with the ability to directly extract a
Yeah, we should probably do that. I haven't checked yet, but I have the hope that we can simply apply the same fix there, which should be relatively straight forward, |
|
Should we wait with this until we figured |
|
I think making this type internally available so we can use it as the macros is a great first step to landing it, and probably helps sidestep the problem we have with the I'd be keen to figure out the |
Great, I will clean this up then. I think I have to remove the
I think it makes sense to get the |
887e769 to
5c89cad
Compare
|
I think this should be a MVP now. I had to change the internals because I discovered some unsoundness with my previous approach. I think this version should be good now, but I would appreciate if you could double check my reasonings on the unsafe part specifically. I added a safety comment to all blocks, which hopefully make sense. |
davidhewitt
left a comment
There was a problem hiding this comment.
Thanks, this is looking great! An initial wave of comments / thoughts... 👀
There was a problem hiding this comment.
What were the changes in this file motivated by? They seem somewhat reasonable but also I think if there's no special behaviour to the NonNullObject case we should just remove it? That said if there's no need to change this maybe just revert the file diff?
There was a problem hiding this comment.
This from ee55a8b. I would have to check again for the exact error, but there was something like a lifetime error because the pointer extracted from the NonNull would for some reason not live long enough.
There was a problem hiding this comment.
So the failing example is this. It fails on the #[pymethod] expansion.
#[pyclass]
struct DescrCounter {}
#[pymethods]
impl DescrCounter {
fn __set__(&self, _instance: &Bound<'_, PyAny>, _new_value: &mut Self) {}
}The relevant part is:
#[allow(non_snake_case)]
unsafe fn __pymethod___set____(
py: ::pyo3::Python,
_raw_slf: *mut ::pyo3::ffi::PyObject,
arg0: *mut ::pyo3::ffi::PyObject,
arg1: ::std::ptr::NonNull<::pyo3::ffi::PyObject>,
) -> ::pyo3::PyResult<()> {
let _slf = _raw_slf;
#[allow(clippy::let_unit_value)]
let mut holder_0 = ::pyo3::impl_::extract_argument::FunctionArgumentHolder::INIT;
let mut holder_1 = ::pyo3::impl_::extract_argument::FunctionArgumentHolder::INIT;
let mut holder_2 = ::pyo3::impl_::extract_argument::FunctionArgumentHolder::INIT;
let result = DescrCounter::__set__(
::pyo3::impl_::extract_argument::extract_pyclass_ref::<DescrCounter>(
unsafe { ::pyo3::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf) }.0,
&mut holder_0,
)?,
{
#[allow(unused_imports)]
use ::pyo3::impl_::pyclass::Probe as _;
::pyo3::impl_::extract_argument::extract_argument::<
_,
{ ::pyo3::impl_::pyclass::IsOption::<&Bound<'_, PyAny>>::VALUE },
>(
unsafe { ::pyo3::impl_::pymethods::BoundRef::ref_from_ptr(py, &arg0).0 },
&mut holder_1,
"_instance",
)
}?,
{
#[allow(unused_imports)]
use ::pyo3::impl_::pyclass::Probe as _;
::pyo3::impl_::extract_argument::extract_argument::<
_,
{ ::pyo3::impl_::pyclass::IsOption::<&mut Self>::VALUE },
>(
unsafe {
::pyo3::impl_::pymethods::BoundRef::ref_from_ptr(py, &arg1.as_ptr()).0
// temporary value dropped while borrowed^
},
&mut holder_2,
"_new_value",
)
}?,
);
::pyo3::impl_::callback::convert(py, result)
}There was a problem hiding this comment.
I think rather than removing the advantages of the non-null type what I'd rather we did is have a BoundRef::ref_from_non_null(py, &arg1) on that problematic line.
While it's a duplication of the ref_from_ptr API, it might be a good thing for type safety given the non-null.
In the long run I wonder if we will replace BoundRef with Borrowed more widely across the macros, probably that would benefit from the FromPyObject changes being landed first.
There was a problem hiding this comment.
Agreed, I replaced that commit with one that adds these helpers
| holder: &'holder mut Option<PyClassGuard<'a, T>>, | ||
| ) -> PyResult<&'holder T> { |
There was a problem hiding this comment.
I'm trying to understand the need for the &'holder lifetime here. I suspect it's something about &'a mut Option<PyClassGuard<'a, T>> forcing 'a to be invariant?
I wonder, to avoid the trait picking up so many lifetimes, is there a way out with GATs?
There was a problem hiding this comment.
I have to think about if there is something we can do with GATs here. I would need to try again to reproduce the exact error, but tying these lifetimes together was causing problems down the line.
There was a problem hiding this comment.
If we tie 'holder and 'a together, we get the following error:
9 | #[crate::pymethods]
| ^^^^^^^^^^^^^^^^^^-
| | |
| | temporary value is freed at the end of this statement
| | borrow might be used here, when `holder_1` is dropped and runs the destructor for type `Option<PyClassGuard<'_, Dummy>>`
| creates a temporary value which is freed while still in useI think this is due to 'a being the lifetime of a function argument, while holder is defines inside that function and thus dropped first, so the holder lifetime must be shorter and can't be the same as 'a
There was a problem hiding this comment.
Understood. I think let's accept this complexity here for now, I think once we make the changes to FromPyObject we might want to use Borrowed<'a, 'py, T> in these functions and it might be that we get some refactoring opportunities then.
| pub(crate) fn as_class_object(&self) -> &'a PyClassObject<T> { | ||
| // SAFETY: `ptr` by construction points to a `PyClassObject<T>` and is | ||
| // valid for at least 'a | ||
| unsafe { self.ptr.cast().as_ref() } |
There was a problem hiding this comment.
These methods on NonNull are super nice, I wonder if we can use them more internally instead in places where we know the pointer is non-null but just haven't (yet) told the type system that 👍
davidhewitt
left a comment
There was a problem hiding this comment.
This is looking great, I think we should ship it in 0.26 👍
Before merge, I think it would be great to adjust the changes to the NonNull stuff in the macros as per #5233 (comment), if you agree with that idea?
Co-authored-by: David Hewitt <mail@davidhewitt.dev>
davidhewitt
left a comment
There was a problem hiding this comment.
This looks perfect to me, thank you for the hard work here! Looks like this makes a meaningful improvement (~5%) to our method call performance, always great to land those without breaking any user code!
…yO3#5233) * introduce `PyClassGuard(Mut)` as future replacement for `PyRef(Mut)` * use `PyClassGuard(Mut)` in the macros * add `NonNull` macro helpers * add `IntoPyObject` impls * impl `FromPyObjectBound` instead of `PyFunctionArgument` * add newsfragment * add Ungil, Send, Sync impls * fix leaked borrow in `PyClassGuard::into_super` Co-authored-by: David Hewitt <mail@davidhewitt.dev> --------- Co-authored-by: David Hewitt <mail@davidhewitt.dev>
Ref #4720
Introduces
PyClassGuard(Mut)as future replacement forPyRef(Mut). It switches the lifetime from'pyto'a, borrowing from the input pointer directly regardless of whether we're attached or not. In this experiment I build it around&'a Py<T>which seems to work (at least locally), let's see what CI says.