Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: major rewrite for Bound API #3906

Merged
merged 18 commits into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 4 additions & 25 deletions Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,45 +59,24 @@ Those flags are set in [`build.rs`](#6-buildrs-and-pyo3-build-config).
[`src/types`] contains bindings to [built-in types](https://docs.python.org/3/library/stdtypes.html)
of Python, such as `dict` and `list`.
For historical reasons, Python's `object` is called `PyAny` in PyO3 and located in [`src/types/any.rs`].

Currently, `PyAny` is a straightforward wrapper of `ffi::PyObject`, defined as:

```rust
#[repr(transparent)]
pub struct PyAny(UnsafeCell<ffi::PyObject>);
```

All built-in types are defined as a C struct.
For example, `dict` is defined as:

```c
typedef struct {
/* Base object */
PyObject ob_base;
/* Number of items in the dictionary */
Py_ssize_t ma_used;
/* Dictionary version */
uint64_t ma_version_tag;
PyDictKeysObject *ma_keys;
PyObject **ma_values;
} PyDictObject;
```

However, we cannot access such a specific data structure with `#[cfg(Py_LIMITED_API)]` set.
Thus, all builtin objects are implemented as opaque types by wrapping `PyAny`, e.g.,:
Concrete Python objects are implemented by wrapping `PyAny`, e.g.,:

```rust
#[repr(transparent)]
pub struct PyDict(PyAny);
```

Note that `PyAny` is not a pointer, and it is usually used as a pointer to the object in the
Python heap, as `&PyAny`.
This design choice can be changed
(see the discussion in [#1056](https://github.com/PyO3/pyo3/issues/1056)).
These types are not intended to be accessed directly, and instead are used through the `Py<T>` and `Bound<T>` smart pointers.

Since we need lots of boilerplate for implementing common traits for these types
(e.g., `AsPyPointer`, `AsRef<PyAny>`, and `Debug`), we have some macros in
[`src/types/mod.rs`].
We have some macros in [`src/types/mod.rs`] which make it easier to implement APIs for concrete Python types.

## 3. `PyClass` and related functionalities

Expand Down
27 changes: 15 additions & 12 deletions guide/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,25 @@
---

- [Getting started](getting-started.md)
- [Python modules](module.md)
- [Python functions](function.md)
- [Function signatures](function/signature.md)
- [Error handling](function/error-handling.md)
- [Python classes](class.md)
- [Class customizations](class/protocols.md)
- [Basic object customization](class/object.md)
- [Emulating numeric types](class/numeric.md)
- [Emulating callable objects](class/call.md)
- [Using Rust from Python](rust-from-python.md)
- [Python modules](module.md)
- [Python functions](function.md)
- [Function signatures](function/signature.md)
- [Error handling](function/error-handling.md)
- [Python classes](class.md)
- [Class customizations](class/protocols.md)
- [Basic object customization](class/object.md)
- [Emulating numeric types](class/numeric.md)
- [Emulating callable objects](class/call.md)
- [Calling Python from Rust](python-from-rust.md)
- [Python object types](types.md)
- [Python exceptions](exception.md)
- [Calling Python functions](python-from-rust/function-calls.md)
- [Executing existing Python code](python-from-rust/calling-existing-code.md)
- [Type conversions](conversions.md)
- [Mapping of Rust types to Python types](conversions/tables.md)
- [Conversion traits](conversions/traits.md)
- [Python exceptions](exception.md)
- [Calling Python from Rust](python-from-rust.md)
- [Using `async` and `await`](async-await.md)
- [GIL, mutability and object types](types.md)
- [Parallelism](parallelism.md)
- [Debugging](debugging.md)
- [Features reference](features.md)
Expand Down
7 changes: 0 additions & 7 deletions guide/src/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,3 @@
PyO3 exposes much of Python's C API through the `ffi` module.

The C API is naturally unsafe and requires you to manage reference counts, errors and specific invariants yourself. Please refer to the [C API Reference Manual](https://docs.python.org/3/c-api/) and [The Rustonomicon](https://doc.rust-lang.org/nightly/nomicon/ffi.html) before using any function from that API.

## Memory management

PyO3's `&PyAny` "owned references" and `Py<PyAny>` smart pointers are used to
access memory stored in Python's heap. This memory sometimes lives for longer
than expected because of differences in Rust and Python's memory models. See
the chapter on [memory management](./memory.md) for more information.
12 changes: 8 additions & 4 deletions guide/src/async-await.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ async fn sleep(seconds: f64, result: Option<PyObject>) -> Option<PyObject> {

Resulting future of an `async fn` decorated by `#[pyfunction]` must be `Send + 'static` to be embedded in a Python object.

As a consequence, `async fn` parameters and return types must also be `Send + 'static`, so it is not possible to have a signature like `async fn does_not_compile(arg: &PyAny, py: Python<'_>) -> &PyAny`.
As a consequence, `async fn` parameters and return types must also be `Send + 'static`, so it is not possible to have a signature like `async fn does_not_compile<'py>(arg: Bound<'py, PyAny>) -> Bound<'py, PyAny>`.

However, there is an exception for method receiver, so async methods can accept `&self`/`&mut self`. Note that this means that the class instance is borrowed for as long as the returned future is not completed, even across yield points and while waiting for I/O operations to complete. Hence, other methods cannot obtain exclusive borrows while the future is still being polled. This is the same as how async methods in Rust generally work but it is more problematic for Rust code interfacing with Python code due to pervasive shared mutability. This strongly suggests to prefer shared borrows `&self` to exclusive ones `&mut self` to avoid racy borrow check failures at runtime.
However, there is an exception for method receivers, so async methods can accept `&self`/`&mut self`. Note that this means that the class instance is borrowed for as long as the returned future is not completed, even across yield points and while waiting for I/O operations to complete. Hence, other methods cannot obtain exclusive borrows while the future is still being polled. This is the same as how async methods in Rust generally work but it is more problematic for Rust code interfacing with Python code due to pervasive shared mutability. This strongly suggests to prefer shared borrows `&self` over exclusive ones `&mut self` to avoid racy borrow check failures at runtime.

## Implicit GIL holding

Even if it is not possible to pass a `py: Python<'_>` parameter to `async fn`, the GIL is still held during the execution of the future – it's also the case for regular `fn` without `Python<'_>`/`&PyAny` parameter, yet the GIL is held.
Even if it is not possible to pass a `py: Python<'py>` parameter to `async fn`, the GIL is still held during the execution of the future – it's also the case for regular `fn` without `Python<'py>`/`Bound<'py, PyAny>` parameter, yet the GIL is held.

It is still possible to get a `Python` marker using [`Python::with_gil`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.with_gil); because `with_gil` is reentrant and optimized, the cost will be negligible.

Expand All @@ -47,7 +47,11 @@ There is currently no simple way to release the GIL when awaiting a future, *but
Here is the advised workaround for now:

```rust,ignore
use std::{future::Future, pin::{Pin, pin}, task::{Context, Poll}};
use std::{
future::Future,
pin::{Pin, pin},
task::{Context, Poll},
};
use pyo3::prelude::*;

struct AllowThreads<F>(F);
Expand Down
40 changes: 19 additions & 21 deletions guide/src/class.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
RegularPolygon { side_count: u32, radius: f64 },
Nothing { },
Nothing {},
}
```

Expand Down Expand Up @@ -89,7 +89,7 @@ Currently, the best alternative is to write a macro which expands to a new `#[py
use pyo3::prelude::*;

struct GenericClass<T> {
data: T
data: T,
}

macro_rules! create_interface {
Expand All @@ -102,7 +102,9 @@ macro_rules! create_interface {
impl $name {
#[new]
pub fn new(data: $type) -> Self {
Self { inner: GenericClass { data: data } }
Self {
inner: GenericClass { data: data },
}
}
}
};
Expand Down Expand Up @@ -187,23 +189,21 @@ fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
}
```

## Bound and interior mutability
## Bound<T> and interior mutability

Often is is useful to turn a `#[pyclass]` type `T` into a Python object and access it from Rust code. The [`Py<T>`] and [`Bound<'py, T>`] smart pointers are the ways to represent a Python object in PyO3's API. More detail can be found about them [in the Python objects](./types.md#pyo3s-smart-pointers) section of the guide.

You sometimes need to convert your `#[pyclass]` into a Python object and access it
from Rust code (e.g., for testing it).
[`Bound`] is the primary interface for that.
Most Python objects do not offer exclusive (`&mut`) access (see the [section on Python's memory model](./python-from-rust.md#pythons-memory-model)). However, Rust structs wrapped as Python objects (called `pyclass` types) often *do* need `&mut` access. Due to the GIL, PyO3 *can* guarantee exclusive access to them.

To mutate data behind a `Bound<'_, T>` safely, PyO3 employs the
[Interior Mutability Pattern](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html)
like [`RefCell`].
The Rust borrow checker cannot reason about `&mut` references once an object's ownership has been passed to the Python interpreter. This means that borrow checking is done at runtime using with a scheme very similar to `std::cell::RefCell<T>`. This is known as [interior mutability](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html).

Users who are familiar with `RefCell` can use `Bound` just like `RefCell`.
Users who are familiar with `RefCell<T>` can use `Py<T>` and `Bound<'py, T>` just like `RefCell<T>`.

For users who are not very familiar with `RefCell`, here is a reminder of Rust's rules of borrowing:
For users who are not very familiar with `RefCell<T>`, here is a reminder of Rust's rules of borrowing:
- At any given time, you can have either (but not both of) one mutable reference or any number of immutable references.
- References must always be valid.
- References can never outlast the data they refer to.

`Bound`, like `RefCell`, ensures these borrowing rules by tracking references at runtime.
`Py<T>` and `Bound<'py, T>`, like `RefCell<T>`, ensure these borrowing rules by tracking references at runtime.

```rust
# use pyo3::prelude::*;
Expand Down Expand Up @@ -233,10 +233,8 @@ Python::with_gil(|py| {
});
```

A `Bound<'py, T>` is restricted to the GIL lifetime `'py`.
To make the object longer lived (for example, to store it in a struct on the
Rust side), you can use `Py<T>`, which stores an object longer than the GIL
lifetime, and therefore needs a `Python<'_>` token to access.
A `Bound<'py, T>` is restricted to the GIL lifetime `'py`. To make the object longer lived (for example, to store it in a struct on the
Rust side), use `Py<T>`. `Py<T>` needs a `Python<'_>` token to allow access:

```rust
# use pyo3::prelude::*;
Expand All @@ -252,7 +250,7 @@ fn return_myclass() -> Py<MyClass> {
let obj = return_myclass();

Python::with_gil(|py| {
let bound = obj.bind(py); // Py<MyClass>::bind returns &Bound<'_, MyClass>
let bound = obj.bind(py); // Py<MyClass>::bind returns &Bound<'py, MyClass>
let obj_ref = bound.borrow(); // Get PyRef<T>
assert_eq!(obj_ref.num, 1);
});
Expand Down Expand Up @@ -431,7 +429,7 @@ impl DictWithCounter {
Self::default()
}

fn set(slf: &Bound<'_, Self>, key: String, value: &PyAny) -> PyResult<()> {
fn set(slf: &Bound<'_, Self>, key: String, value: Bound<'_, PyAny>) -> PyResult<()> {
slf.borrow_mut().counter.entry(key.clone()).or_insert(0);
let dict = slf.downcast::<PyDict>()?;
dict.set_item(key, value)
Expand Down Expand Up @@ -1319,7 +1317,7 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass {
[`PyTypeInfo`]: {{#PYO3_DOCS_URL}}/pyo3/type_object/trait.PyTypeInfo.html

[`Py`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Py.html
[`Bound`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html
[`Bound<'_, T>`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html
[`PyClass`]: {{#PYO3_DOCS_URL}}/pyo3/pyclass/trait.PyClass.html
[`PyRef`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRef.html
[`PyRefMut`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRefMut.html
Expand Down
Loading
Loading