What's the memory management strategy when using PyO3?

I am a beginner to Rust and PyO3, I read the PyO3 User Guide and got some information as shown below:

  1. When Rust calls Python, the memory of Python part is allocated in Python heap and is taken care by the Python garbage collector.
  2. When Python calls rust, the arguments of rust functions can be either rust type or python native type, the former will incur a conversion but it's faster, the later is almost zero-cost.
  3. Rust can access python heap through reference, PyCell is always allocated in the Python heap, Rust doesn't have ownership of it, but Rust can benefit from it's interior mutability pattern.

But I still have the following questions

  1. How is memory passed/copied in multiple data interaction scenarios?
    a. When Python calls Rust
    i. Python passes some arguments to Rust func
    ii. Rust func creates some data and return to python
    iii. rust accepts arguments from python, do some calculation and return to Python
    b. When Rust calls Python
    i. Rust passes some arguments to Python func
    ii. Python func creates some data and return to python
    iii. Python accepts arguments from Rust, do some calculation and return to Rust
  2. If I create a PyCell with a Rust value which implements Into<PyClassInitializer>, will there be a memory copy?
  3. When Python calls Rust, does the conversion from Python Native Type to Rust Type mean that there is a memory copy from Python heap to rust heap?
2 Likes

I think the best way to answer these questions would be to read the PyO3 source code yourself.

Navigating a foreign codebase is an incredibly important skill, and for me, needing to read/understand code in the real world has been a much better teacher than documentation or anything written in English. By its very nature, prose tends to be be imprecise and inaccurate, whereas the source code gives you a chance to follow through the functions that are called and understand how the various parts fit together.

To help you get started, I'd check out the Py::new() constructor because that's what instantiates classes on the Python heap and gives you access to them.

You can also check out the ToPyObject and FromPyObject implementations for various types to see whether they would involve copies or not.

For example, this is the implementation for HashMap:

6 Likes

It depends on the type where and if copying and allocation happens on ffi boundaries. For example going from PyBytes to a &[u8], or from a PyString to a &str (if it is utf8) just makes a reference pointing into the object, and no copying or allocating happens. And small integers (iirc, -5 to 255 or something?) are interned by the Python interpreter, so if you create one of those it should just increase a reference count.

PyCell's purpose is to provide runtime-checked access to Rust types that are owned by Python. Because Python types are reference counted we can't just hand out mutable references to them - it has to be checked at runtime that there is no aliasing.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.