Skip to content
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
646 changes: 346 additions & 300 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ resolver = "2"
anyhow = "1"
duct = "0.13"
primes = "0.4"
pyo3 = "0.23.3"
pyo3 = "0.26.0"
rayon = "1"
semver = "1.0.23"
serde = "1.0.204"
serde_json = "1.0.120"
semver = "1.0.27"
serde = "1.0.228"
serde_json = "1.0.145"
6 changes: 3 additions & 3 deletions book/src/01_intro/04_arguments.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

```rust
use pyo3::prelude::*;

#[pyfunction]
fn no_op() {
// Do nothing
Expand Down Expand Up @@ -35,7 +35,7 @@ Python objects into Rust types.
## Available implementations

`pyo3` provides implementations of `FromPyObject` for a large number of types—e.g. `i32`, `f64`, `String`, `Vec`, etc.
You can find an exhaustive list in [`pyo3`'s guide](https://pyo3.rs/v0.23.3/conversions/tables#argument-types),
You can find an exhaustive list in [`pyo3`'s guide](https://pyo3.rs/v0.26.0/conversions/tables#argument-types),
under the "Rust" table column.

## Conversion cost
Expand All @@ -53,4 +53,4 @@ Don't try to use them to solve the exercise for this section: we'll cover them i

## References

- [The `FromPyObject` trait](https://docs.rs/pyo3/0.23.3/pyo3/conversion/trait.FromPyObject.html)
- [The `FromPyObject` trait](https://docs.rs/pyo3/0.26.0/pyo3/conversion/trait.FromPyObject.html)
8 changes: 4 additions & 4 deletions book/src/01_intro/05_gil.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ error[E0277]: the trait bound `&PyList: PyFunctionArgument<'_, '_>` is not satis
--> src/lib.rs:7:28
|
7 | fn print_number_list(list: &PyList) {
| ^
| the trait `PyClass` is not implemented for `&PyList`,
| ^
| the trait `PyClass` is not implemented for `&PyList`,
| which is required by `&PyList: PyFunctionArgument<'_, '_>`
|
= help: the following other types implement trait `PyFunctionArgument<'a, 'py>`:
Expand Down Expand Up @@ -134,8 +134,8 @@ when we're interacting with the Python object during the conversion.

## References

- [`FromPyObject`](https://docs.rs/pyo3/0.23.3/pyo3/conversion/trait.FromPyObject.html)
- [`Python<'py>`](https://docs.rs/pyo3/0.23.3/pyo3/marker/struct.Python.html)
- [`FromPyObject`](https://docs.rs/pyo3/0.26.0/pyo3/conversion/trait.FromPyObject.html)
- [`Python<'py>`](https://docs.rs/pyo3/0.26.0/pyo3/marker/struct.Python.html)
- [Global Interpreter Lock](https://docs.python.org/3/c-api/init.html#thread-state-and-the-global-interpreter-lock)
- [Official guidance on Python-native vs Rust-native types](https://pyo3.rs/v0.23.3/conversions/tables#using-rust-library-types-vs-python-native-types)

Expand Down
10 changes: 5 additions & 5 deletions book/src/01_intro/06_output.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl<'py> IntoPyObject<'py> for MyType {
/// It captures the ownership relationship between the Python object
/// and the Python runtime.
/// In this case, we're using a `Bound` smart pointer to a `PyInt`.
/// The `'py` lifetime ensures that the Python object is owned
/// The `'py` lifetime ensures that the Python object is owned
/// by the Python runtime.
type Output = Bound<'py, PyInt>;
/// Since the conversion can fail, we need to specify an error type.
Expand All @@ -63,7 +63,7 @@ impl<'py> IntoPyObject<'py> for MyType {
type Error = Infallible;

fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
// `u64` already implements `IntoPyObject`, so we delegate
// `u64` already implements `IntoPyObject`, so we delegate
// to its implementation to do the actual conversion.
self.value.into_pyobject(py)
}
Expand All @@ -76,21 +76,21 @@ Let's focus on the `Output` associated type for a moment.\
In almost all cases, you'll be setting `Output` to `Bound<'py, Self::Target>`[^syntax]. You're creating a new Python
object and its lifetime is tied to the Python runtime.

In a few cases, you might be able to rely on [`Borrowed<'a, 'py, Self::Target>`](https://docs.rs/pyo3/0.23.3/pyo3/prelude/struct.Borrowed.html)
In a few cases, you might be able to rely on [`Borrowed<'a, 'py, Self::Target>`](https://docs.rs/pyo3/0.26.0/pyo3/prelude/struct.Borrowed.html)
instead.
It's slightly faster[^conversation], but it's limited to scenarios where you are borrowing from an existing Python object—fairly
rare for an `IntoPyObject` implementation.

There are no other options for `Output`, since `Output` must implement
[the `BoundObject` trait](https://docs.rs/pyo3/0.23.3/pyo3/trait.BoundObject.html),
[the `BoundObject` trait](https://docs.rs/pyo3/0.26.0/pyo3/trait.BoundObject.html),
the trait is [sealed](https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/) and
those two types are the only implementors within `pyo3`.\
If it helps, think of `Output` as an enum with two variants: `Bound` and `Borrowed`.

## Provided implementations

`pyo3` provides out-of-the-box implementations of `IntoPyObject` for many Rust types, as well as for all `Py*` types.
Check out [its documentation](https://docs.rs/pyo3/0.23.3/pyo3/conversion/trait.IntoPyObject.html#foreign-impls)
Check out [its documentation](https://docs.rs/pyo3/0.26.0/pyo3/conversion/trait.IntoPyObject.html#foreign-impls)
for an exhaustive list.

[^syntax]: The actual syntax is a bit more complex: `type Output = Bound<'py, <Self as IntoPyObject<'py>>::Target>>;`.
Expand Down
8 changes: 4 additions & 4 deletions book/src/02_classes/06_parent.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ impl Child {
fn new(name: String, age: u8) -> PyClassInitializer<Self> {
// [...]
}

fn greet(&self) {
println!("Hi, I'm {} and I'm {} years old!", self.name, self.age);
}
Expand All @@ -71,22 +71,22 @@ This can be done using another one of `pyo3`'s smart pointers: `PyRef`.
#[pymethods]
impl Child {
// [...]

fn greet(self_: PyRef<'_, Self>) {
todo!()
}
}
```

`PyRef` represents an immutable reference to the Python object.\
It allows us, in particular, to call the [`as_super`](https://docs.rs/pyo3/0.23.3/pyo3/pycell/struct.PyRef.html#method.as_super) method,
It allows us, in particular, to call the [`as_super`](https://docs.rs/pyo3/0.26.0/pyo3/pycell/struct.PyRef.html#method.as_super) method,
which returns a reference to the parent class.

```rust
#[pymethods]
impl Child {
// [...]

fn greet(self_: PyRef<'_, Self>) {
// This is now a reference to a `Parent` instance!
let parent = self_.as_super();
Expand Down
18 changes: 9 additions & 9 deletions book/src/03_concurrency/03_releasing_the_gil.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ which internally hold a `Python<'py>` instance.\
There's no way around it: any interaction with Python objects must be serialized. But, here's the kicker: not all Rust code needs to
interact with Python objects!

## `Python::allow_threads`
## `Python::detach`

For example, consider a Rust function that calculates the nth Fibonacci number:

Expand Down Expand Up @@ -84,7 +84,7 @@ We can fix it by explicitly releasing the GIL:
```rust
#[pyfunction]
fn fibonacci(py: Python<'_>, n: u64) -> u64 {
py.allow_threads(|| {
py.detach(|| {
let mut a = 0;
let mut b = 1;
for _ in 0..n {
Expand All @@ -97,7 +97,7 @@ fn fibonacci(py: Python<'_>, n: u64) -> u64 {
}
```

`Python::allow_threads` releases the GIL while executing the closure passed to it.\
`Python::detach` releases the GIL while executing the closure passed to it.\
This frees up the Python interpreter to run other Python code, such as the `other_work` function in our example, while the Rust
thread is busy calculating the nth Fibonacci number.

Expand All @@ -122,18 +122,18 @@ Using the same line diagram as before, we have the following:

## `Ungil`

`Python::allow_threads` is only sound **if the closure doesn't interact with Python objects**.\
`Python::detach` is only sound **if the closure doesn't interact with Python objects**.\
If that's not the case, we end up with undefined behavior: Rust code touching Python objects while the Python interpreter is running
other Python code, assuming nothing else is happening to those objects thanks to the GIL. A recipe for disaster!

It'd be ideal to rely on the type system to enforce this constraint for us at compile-time, in true Rust fashion—"if it compiles, it's
safe."\
`pyo3` _tries_ to follow this principle with the [`Ungil` marker trait](https://docs.rs/pyo3/0.23.3/pyo3/marker/trait.Ungil.html):
`pyo3` _tries_ to follow this principle with the [`Ungil` marker trait](https://docs.rs/pyo3/0.26.0/pyo3/marker/trait.Ungil.html):
only types that are safe to access without the GIL can implement `Ungil`. The trait is then used to constrain the arguments of
`Python::allow_threads`:
`Python::detach`:

```rust
pub fn allow_threads<T, F>(self, f: F) -> T
pub fn detach<T, F>(self, f: F) -> T
where
F: Ungil + FnOnce() -> T,
T: Ungil,
Expand All @@ -145,9 +145,9 @@ where
Unfortunately, `Ungil` is not perfect.
On stable Rust, it leans on the `Send` trait, but that allows for some
[unsafe interactions with Python objects](https://github.com/PyO3/pyo3/issues/2141). The tracking is more precise on `nightly` Rust[^nightly],
but it doesn't catch [every possible misuse of `Python::allow_threads`](https://github.com/PyO3/pyo3/issues/3640).
but it doesn't catch [every possible misuse of `Python::detach`](https://github.com/PyO3/pyo3/issues/3640).

My recommendation: if you're using `Python::allow_threads`, trigger an additional run of your CI pipeline using the `nightly` Rust compiler
My recommendation: if you're using `Python::detach`, trigger an additional run of your CI pipeline using the `nightly` Rust compiler
to catch more issues. On top of that, review your code carefully.

[^nightly]: See the [`nightly` feature flag exposed by `pyo3`](https://pyo3.rs/v0.23.3/features.html#nightly).
26 changes: 13 additions & 13 deletions book/src/03_concurrency/04_minimize_gil_locking.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
All our examples so far fall into two categories:

- The Rust function holds the GIL for the entire duration of its execution.
- The Rust function doesn't hold the GIL at all, going straight into `Python::allow_threads` mode.
- The Rust function doesn't hold the GIL at all, going straight into `Python::detach` mode.

Real-world applications are often more nuanced, though.\
You'll need to hold the GIL for some operations (e.g. passing data back to Python), but you're able to release it
Expand All @@ -28,7 +28,7 @@ fn update_in_place<'py>(
) -> PyResult<()> {
// Holding the GIL
let v: Vec<i32> = numbers.extract()?;
let updated_v: Vec<_> = python.allow_threads(|| {
let updated_v: Vec<_> = python.detach(|| {
v.iter().map(|&n| expensive_computation(n)).collect()
});
// Back to holding the GIL
Expand Down Expand Up @@ -61,7 +61,7 @@ fn update_in_place<'py>(
python: Python<'py>,
numbers: Bound<'py, PyList>
) -> PyResult<()> {
python.allow_threads(|| -> PyResult<()> {
python.detach(|| -> PyResult<()> {
let n_numbers = numbers.len();
for i in 0..n_numbers {
let n = numbers.get_item(i)?.extract::<i64>()?;
Expand All @@ -73,7 +73,7 @@ fn update_in_place<'py>(
}
```

It won't compile, though. We're using a GIL-bound object (`numbers`) in a GIL-free section (inside `python.allow_threads`).
It won't compile, though. We're using a GIL-bound object (`numbers`) in a GIL-free section (inside `python.detach`).
We need to **unbind** it first.

### `Py<T>` and `Bound<'py, T>`
Expand All @@ -88,7 +88,7 @@ fn update_in_place<'py>(
numbers: Bound<'py, PyList>
) -> PyResult<()> {
let numbers = numbers.unbind();
python.allow_threads(|| -> PyResult<()> {
python.detach(|| -> PyResult<()> {
let n_numbers = numbers.len();
for i in 0..n_numbers {
let n = numbers.get_item(i)?.extract::<i64>()?;
Expand All @@ -104,8 +104,8 @@ But it won't compile either. `numbers.len()`, `numbers.get_item(i)`, and `number
`Py<T>` is just a pointer to a Python object, it won't allow us to access it if we're not holding the GIL.

We need to **re-bind** it using a `Python<'py>` token, thus getting a `Bound<'py, PyList>` back.
How do we get a `Python<'py>` token inside the closure, though? Using `Python::with_gil`: it's the opposite
of `Python::allow_threads`, it makes sure to acquire the GIL before executing the closure and release it afterwards.
How do we get a `Python<'py>` token inside the closure, though? Using `Python::attach`: it's the opposite
of `Python::detach`, it makes sure to acquire the GIL before executing the closure and release it afterwards.
The closure is given a `Python` token as argument, which we can use to re-bind the `PyList` object:

```rust
Expand All @@ -117,11 +117,11 @@ fn update_in_place<'py>(
let n_numbers = numbers.len();
let numbers_ref = numbers.unbind();
// Release the GIL
python.allow_threads(|| -> PyResult<()> {
python.detach(|| -> PyResult<()> {
for i in 0..n_numbers {
// Acquire the GIL again, to access the
// i-th element of the list
let n = Python::with_gil(|inner_py| {
let n = Python::attach(|inner_py| {
numbers_ref
.bind(inner_py)
.get_item(i)?
Expand All @@ -130,7 +130,7 @@ fn update_in_place<'py>(
// Run the computation without holding the GIL
let result = expensive_computation(n);
// Re-acquire the GIL to update the list in place
Python::with_gil(|inner_py| {
Python::attach(|inner_py| {
numbers_ref.bind(inner_py).set_item(i, result)
})?;
}
Expand All @@ -156,6 +156,6 @@ you get with the GIL.

## References

- [`Py<T>` struct](https://docs.rs/pyo3/0.23.3/pyo3/struct.Py.html)
- [`Python::with_gil` method](https://docs.rs/pyo3/0.23.3/pyo3/marker/struct.Python.html#method.with_gil)
- [`Bound<'py, T>::unbind` method](https://docs.rs/pyo3/0.23.3/pyo3/prelude/struct.Bound.html#method.unbind)
- [`Py<T>` struct](https://docs.rs/pyo3/0.26.0/pyo3/struct.Py.html)
- [`Python::attach` method](https://docs.rs/pyo3/0.26.0/pyo3/marker/struct.Python.html#method.attach)
- [`Bound<'py, T>::unbind` method](https://docs.rs/pyo3/0.26.0/pyo3/prelude/struct.Bound.html#method.unbind)
2 changes: 1 addition & 1 deletion book/src/03_concurrency/05_immutable_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ holding the GIL—i.e. via `Py<T>` instead of `Bound<'py, T>`
#[pyfunction]
fn print_point<'py>(python: Python<'py>, point: Bound<'py, Point>) {
let point: Py<Point> = point.unbind();
python.allow_threads(|| {
python.detach(|| {
// We can now access the fields of the Point struct
// even though we are not holding the GIL
let point: &Point = point.get();
Expand Down
6 changes: 3 additions & 3 deletions exercises/03_concurrency/05_immutable_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ impl Rectangle {
///
/// # Constraints
///
/// Do NOT remove the `allow_threads` call. The computation must be done inside
/// the closure passed to `allow_threads`.
/// Do NOT remove the `detach` call. The computation must be done inside
/// the closure passed to `detach`.
fn compute_area<'py>(python: Python<'py>, shape: Bound<'py, Rectangle>) -> u32 {
python.allow_threads(|| {
python.detach(|| {
let area: u32 = todo!();
area
})
Expand Down