Open JRRudy1 opened 4 months ago
Also, the current implementation of pyclass and pyref/pyrefmut is fairly complicated. We'd be open to changes to it if that would make this simpler or more efficient.
Thank you for working on this! I am 👍 on this feature although just wanted to give a heads up it might take me a few days to contribute to any review given it's a tricky one from soundness angle and it's not something I'd actively had in my head until you pushed the PR! 😂
No problem at all David! I know I've been spamming you with PR's this week 😉
However, I'd actually like to put this PR on a brief hold until the following PR's get resolved:
PyRef
and PyRefMut
that should simplify things quite a bit, including the PyRefMap
implementation proposed in this PR. It should also play nicely with a post-gil-refs transition to PyRef
wrapping Borrowed<T>
instead of Bound<T>
, which I would be interested in working on when the time comes.
This draft PR proposes a possible solution for issue #2300 and PR #4195, which involves adding a
Ref::map
-like mechanism for borrowing data nested within aPyRef
orPyRefMut
.Unlike some of the approaches proposed in #2300 which attempt to create a
PyRef<U>
to some typeU
nested within aPyRef<T: PyClass>
, this implementation instead defines a wrapper type that encapsulates the originalPyRef
alongside a raw pointer toU
which it then dereferences to. This has the benefit of not relying on any of thepyo3
internals that may be in flux, and makes the safety relative easy to reason about since there is nothing magic going on.Simplified version
In the simplest case of supporting only immutable borrows from immutable
PyRef
's, the approach boils down to this:This can then be used like so:
Full version
The above method should extend seamlessly to mutable borrows through
PyRefMut
, however this requires adding another wrapper type (e.g.PyRefMutMap
) and all the associatedimpl
's. Then another wrapper may be needed if you want to support shared borrows from aPyRefMut
... this could be somewhat be avoided by making a single wrapper with some extra generic parameters, but then naming the types gets tedious. So, I ultimately decided to use a single base wrapper typePyRefMapBase<'py, U, Mut>
where the containedPyRef<'py, T>
orPyRefMut<'py, T>
is stored as an opaque trait objectBox<dyn OpaquePyRef<'py>>
and the pointer is stored asNonNull<U>
(which can be derived from either&U
or&mut U
). The mutability is then handled with the generic parameterMut: Boolean
which is used in type bounds to statically enforce that mutable dereferences only occur when derived fromPyRefMut<T>
and&mut U
. Finally, two type aliasesPyRefMap<'py, U>
andPyRefMapMut<'py, U>
are defined for the immutable and mutable cases, which are the actual names that users will reference (instead ofPyRefMapBase
directly).This approach of
Box
ing thePyRef
/PyRefMut
has the added benefit of allowing the pyclass typeT
to be erased from the type name so that only the lifetime and the target type need to be included in signatures (i.e. instead ofPyRefMap<'py, MyClass, [i32; 100]>
you simply havePyRefMap<'py, [i32; 100]>
. This comes at the cost of a pointer-sizedBox
allocation when thePyRef
/PyRefMut
is converted to aPyRefMap
/PyRefMapMut
, but I would imagine that is negligible in the context of interfacing with Python.ToDo
pycell
module.PyRef
->PyRefMap
conversions should be associated methods; I know they probably should due to deref conflicts, but I am in denial because standard methods are just so much nicer.compile_fail
tests to make sure it can't be abused.AsRef
/AsMut
impls