Search code examples
pythonrustpyo3

Return reference to member field in PyO3


Suppose I have a Rust struct like this

struct X{...}

struct Y{
   x:X
}

I'd like to be able to write python code that accesses X through Y

y = Y()
y.x.some_method()

What would be the best way to implement it in PyO3? Currently I made two wrapper classes

#[pyclass]
struct XWrapper{
   x:X
}
#[pyclass]
struct YWrapper{
   y:Y
}
#[pymethods]
impl YWrapper{
   #[getter]
   pub fn x(&self)->XWrapper{
      XWrapper{x:self.y.clone()}
   }
}

However, this requires clone(). I'd rather want to return reference. Of course I know that if X was a pyclass, then I could easily return PyRef to it. But the problem is that X and Y come from a Rust library and I cannot nilly-wily add #[pyclass] to them.


Solution

  • I don't think what you say is possible without some rejigging of the interface:

    Your XWrapper owns the x and your Y owns its x as well. That means creating an XWrapper will always involve a clone (or a new).

    Could we change XWrapper so that it merely contains a reference to an x? Not really, because that would require giving XWrapper a lifetime annotation, and PyO3 afaik doesn't allow pyclasses with lifetime annotation. Makes sense, because passing an object to python puts it on the python heap, at which point rust loses control over the object.

    So what can we do?

    Some thoughts: Do you really need to expose the composition structure of y to the python module? Just because that's the way it's organized within Rust doesn't mean it needs to be that way in Python. Your YWrapper could provide methods to the python interface that behind the scenes forward the request to the x instance:

    #[pymethods]
    impl YWrapper{
       pub fn some_method(&self) {
         self.y.x.some_method();
       }
    }
    

    This would also be a welcome sight to strict adherents of the Law of Demeter ;)

    I'm trying to think of other clever ways. Depending on some of the details of how y.x is accessed and modified by the methods of y itself, it might be possible to add a field x: XWrapper to the YWrapper. Then you create the XWrapper (including a clone of y.x) once when YWrapper is created, and from then on you can return references to that XWrapper in your pub fn x. Of course that becomes much more cumbersome when x gets frequently changed and updated via the methods of y...

    In a way, this demonstrates the clash between Python's ref-counted object model and Rust's ownership object model. Rust enforces that you can't arbitrarily mess with objects unless you're their owner.