Repository navigation
Basic object abstraction #5
Description
Activity
Oh there's also details like owned and borrowed references to consider in the overall design.
I'm not actually sure if the PyO3 definition of
PyAnyis strictly needed any more. In the past we had&PyAnyreferences as the main API, where the actual structure layout and size did matter more (we would cast back and forth&PyAny<->*mut ffi::PyObject.That design didn't work very well for conveying ownership, which is why we replaced
&PyAnywith the currentBound<'py, PyAny>smart pointer.One possible alternative is to have
struct PyAny(*mut ffi:: PyObject), however the lack of lifetime to prove the attachment of the thread to interpreter makes reference counting more awkward.Reacted by Jeong, YunWonAs an aside, if reference counting operations were defined to be legal at any time, even without the thread attached, then the lifetime on the type wouldn't be necessary for refcounting (via Clone & Drop).
I think in such a world all refcounting operations would be treated like "remote" thread decrefs on the free threaded build and not be reconciled until GC.
That design didn't work very well for conveying ownership, which is why we replaced &PyAny with the current Bound<'py, PyAny> smart pointer.
One possible alternative is to have struct PyAny(*mut ffi:: PyObject), however the lack of lifetime to prove the attachment of the thread to interpreter makes reference counting more awkward.
Interesting! Perhaps everything would be implemented for
Bound<'py, PyAny>to make sure users are keeping to the safe APIs?I think that seems reasonable.
Regarding
As an aside, if reference counting operations were defined to be legal at any time, even without the thread attached, then the lifetime on the type wouldn't be necessary for refcounting (via Clone & Drop).
I doubt this is possible, unless the free-threaded GC changes significantly. The current free-threaded GC relies on stop-the-world mechanisms that halt all attached thread states and that would have to change to handle this otherwise user refcounting could race with the GC.
I doubt this is possible, unless the free-threaded GC changes significantly.
Indeed, it's a dream for the far future 😂
Interesting! Perhaps everything would be implemented for
Bound<'py, PyAny>to make sure users are keeping to the safe APIs?I think this is exactly where PyO3 ended up, yes.
Also FWIW, I would love to see a concurrent free-threaded GC, which would open the door to unattached reference counting, I think.
One possible alternative is to have struct PyAny(*mut ffi:: PyObject), however the lack of lifetime to prove the attachment of the thread to interpreter makes reference counting more awkward.
Hm, would
PyAny<'py>(*mut ffi::PyObject, PhantomData<'py>)work? I think there might be issues about sending the object between thread states, right?Also, I wonder if we could use
NonNull<ffi::PyObject>over*mut ffi::PyObjectboth because it means we are less likely to de-reference a null pointer if we make a mistake, and also because it would provide nice symmetry if we have aResult<PyAny, ExecutedErr>whereExecutedErralways becomes null, and PyAny always is a valid object. (see #8 for more discussion of an error handling API)Because we must care about performance, NonNull will be helpful.
@davidhewitt must have the best idea about the this topic. Could you share a few options if you design it again from scratch? Do you suggest to follow PyO3 Bound as the best option?
I doubt about
PyAny<'py>(*mut ffi::PyObject, PhantomData<'py>)a bit.There are 2 axis of abstraction about this.
Concrete type is one.
PyObjectneed to be downcasted as str, int and other types. Ownership is another one.PyAnymust have an interface to bePyStringBorrow type is another one. CPython represent both borrowed pointer and RC-ed pointer as
PyObject*. ProbablyBoundandPyalso be related because it is also about the type of reference. this version of PyAny cannot distinguish these properties.In PyO3,
PyAnyisstruct PyAny(UnsafeCell<ffi::PyObject>)instead of *mut PyObject.
Then both&'py PyAnyorBound<'py, PyAny>makes sense.Hm, would
PyAny<'py>(*mut ffi::PyObject, PhantomData<'py>)work? I think there might be issues about sending the object between thread states, right?Yes, an "unattached" form without the lifetime is often necessary (e.g. for holding references when not attached to a thread state, as you note). This is why we have the
Bound<'py, PyAny>abstraction, we also have an unattached form without the lifetime which isPy<PyAny>.Because we must care about performance, NonNull will be helpful.
@davidhewitt must have the best idea about the this topic. Could you share a few options if you design it again from scratch? Do you suggest to follow PyO3 Bound as the best option?
100% we should use
NonNull, it gives the Rust compiler opportunity to reuse the null pointer value e.g. for the "tag" of the result. My mistake for not writing this originally.With the current functionality available in the Rust language, yes, I believe this
Py<T>/Bound<'py, T>smart pointer pair with the object type as a generic argument is still the best form in my opinion.Borrow type is another one. CPython represent both borrowed pointer and RC-ed pointer as
PyObject*. ProbablyBoundandPyalso be related because it is also about the type of reference. this version of PyAny cannot distinguish these properties.This is great to mention. PyO3 models this as a third smart pointer
Borrowed<'a, 'py, T>.'ais the lifetime the borrowed pointer is known to be valid for, e.g. borrow on a tuple item is easy to relate back to a smart pointer referring to the full tuple.It turns out that
Borrowed<'a, 'py, T>is occasionally helpful compared to&'a Bound<'py, T>because:Borrowedis just a pointer rather than a reference-to-pointer, needed for some cases in the Python C API.- the alternative
&'a Bound<'py, T>adds an implicit'py: 'alifetime bound (because the reference lifetime'acannot be longer than the lifetime 'pyof the data it references).Borrowed<'a, 'py, T>` has no such implicit constraint, sometimes this is useful to implement valid code.
Technically in PyO3's current design there is space for a hypothetical
BorrowedUnattached<'a, T>smart pointer which does not have attachment to a Python thread, but that hasn't seen practical need so we have not implemented it.
In a hypothetical future Rust language, I would LOVE there to be an alternative way to model thread attachment than with the
Python<'py>token / lifetime polluting the types. I believe that an idea called "contexts" previously most discussed in the space of async runtimes could provide a way to make certain functions only callable within a given context.In such a hypothetical future, I wonder if it would be possible to have only
Py<T>andPyBorrowed<'a, T>and remove the lifetime-attached types.Probably not a practical solution for us right now, I would love to present this case to the Rust project at one of our future meetings.
Reacted by Jeong, YunWon, Emma Smith and Kirill PodoprigoraAnother possible problem is that many CPython internal APIs take the thread state directly, not fetching it from TLS. The
Python<'py>token does not carry the actual thread state pointer, and I doubt we would want everyBound<'py, T>smart pointer to be a pair of (thread_state, object) pointers.PyO3 using the public API has not hit this problem (yet, subinterpreters might break it eventually).
We need a basic abstraction over the object layer.
This includes:
PyObject- abstract objectsstr,bytes,list,tuple, etc. - concrete objectsCreating custom objects (classes) is a separate task
Things to keep in mind:
&mut PyObject.&references are immutable we need to specify that the object abstraction has interior mutabilityTherefore I propose we use the definition from PyO3:
https://github.com/PyO3/pyo3/blob/ab7b0d303be0c3948866519583dd32332f6c3d22/src/types/any.rs#L33
i.e. a transparent wrapper around
UnsafeCell<ffi::PyObject>