Skip to content

Commit c2986df

Browse files
Merge #3158
3158: Add Py::get for GIL-independent access to frozen classes. r=davidhewitt a=adamreichold `@davidhewitt` Is this what you had in mind for #3154? The name is an obvious candidate for bikeshedding. Trying to write an example, I noticed that making `PyCell::get_frozen` public is most likely not useful as there is no way to safely get a `&PyCell` without acquiring the GIL first? Co-authored-by: Adam Reichold <[email protected]>
2 parents 3b4c7d3 + b9766cf commit c2986df

8 files changed

+173
-9
lines changed

guide/pyclass_parameters.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
| `dict` | Gives instances of this class an empty `__dict__` to store custom attributes. |
77
| <span style="white-space: pre">`extends = BaseType`</span> | Use a custom baseclass. Defaults to [`PyAny`][params-1] |
88
| <span style="white-space: pre">`freelist = N`</span> | Implements a [free list][params-2] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. |
9-
| <span style="white-space: pre">`frozen`</span> | Declares that your pyclass is immutable. It removes the borrowchecker overhead when retrieving a shared reference to the Rust struct, but disables the ability to get a mutable reference. |
9+
| <span style="white-space: pre">`frozen`</span> | Declares that your pyclass is immutable. It removes the borrow checker overhead when retrieving a shared reference to the Rust struct, but disables the ability to get a mutable reference. |
1010
| `get_all` | Generates getters for all fields of the pyclass. |
1111
| `mapping` | Inform PyO3 that this class is a [`Mapping`][params-mapping], and so leave its implementation of sequence C-API slots empty. |
1212
| <span style="white-space: pre">`module = "module_name"`</span> | Python code will see the class as being defined in this module. Defaults to `builtins`. |

guide/src/class.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,32 @@ Python::with_gil(|py| {
211211
});
212212
```
213213

214+
### frozen classes: Opting out of interior mutability
215+
216+
As detailed above, runtime borrow checking is currently enabled by default. But a class can opt of out it by declaring itself `frozen`. It can still use interior mutability via standard Rust types like `RefCell` or `Mutex`, but it is not bound to the implementation provided by PyO3 and can choose the most appropriate strategy on field-by-field basis.
217+
218+
Classes which are `frozen` and also `Sync`, e.g. they do use `Mutex` but not `RefCell`, can be accessed without needing the Python GIL via the `PyCell::get` and `Py::get` methods:
219+
220+
```rust
221+
use std::sync::atomic::{AtomicUsize, Ordering};
222+
# use pyo3::prelude::*;
223+
224+
#[pyclass(frozen)]
225+
struct FrozenCounter {
226+
value: AtomicUsize,
227+
}
228+
229+
let py_counter: Py<FrozenCounter> = Python::with_gil(|py| {
230+
let counter = FrozenCounter { value: AtomicUsize::new(0) };
231+
232+
Py::new(py, counter).unwrap()
233+
});
234+
235+
py_counter.get().value.fetch_add(1, Ordering::Relaxed);
236+
```
237+
238+
Frozen classes are likely to become the default thereby guiding the PyO3 ecosystem towards a more deliberate application of interior mutability. Eventually, this should enable further optimizations of PyO3's internals and avoid downstream code paying the cost of interior mutability when it is not actually required.
239+
214240
## Customizing the class
215241

216242
{{#include ../pyclass_parameters.md}}

newsfragments/3158.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `PyClass::get` and `Py::get` for GIL-indepedent access to internally synchronized frozen classes.

src/instance.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use crate::pyclass::boolean_struct::False;
21
// Copyright (c) 2017-present PyO3 Project and Contributors
32
use crate::conversion::PyTryFrom;
43
use crate::err::{self, PyDowncastError, PyErr, PyResult};
54
use crate::gil;
65
use crate::pycell::{PyBorrowError, PyBorrowMutError, PyCell};
6+
use crate::pyclass::boolean_struct::{False, True};
77
use crate::types::{PyDict, PyString, PyTuple};
88
use crate::{
99
ffi, AsPyPointer, FromPyObject, IntoPy, IntoPyPointer, PyAny, PyClass, PyClassInitializer,
@@ -366,6 +366,8 @@ where
366366
/// This borrow lasts while the returned [`PyRef`] exists.
367367
/// Multiple immutable borrows can be taken out at the same time.
368368
///
369+
/// For frozen classes, the simpler [`get`][Self::get] is available.
370+
///
369371
/// Equivalent to `self.as_ref(py).borrow()` -
370372
/// see [`PyCell::borrow`](crate::pycell::PyCell::borrow).
371373
///
@@ -444,6 +446,8 @@ where
444446
///
445447
/// This is the non-panicking variant of [`borrow`](#method.borrow).
446448
///
449+
/// For frozen classes, the simpler [`get`][Self::get] is available.
450+
///
447451
/// Equivalent to `self.as_ref(py).borrow_mut()` -
448452
/// see [`PyCell::try_borrow`](crate::pycell::PyCell::try_borrow).
449453
pub fn try_borrow<'py>(&'py self, py: Python<'py>) -> Result<PyRef<'py, T>, PyBorrowError> {
@@ -467,6 +471,41 @@ where
467471
{
468472
self.as_ref(py).try_borrow_mut()
469473
}
474+
475+
/// Provide an immutable borrow of the value `T` without acquiring the GIL.
476+
///
477+
/// This is available if the class is [`frozen`][macro@crate::pyclass] and [`Sync`].
478+
///
479+
/// # Examples
480+
///
481+
/// ```
482+
/// use std::sync::atomic::{AtomicUsize, Ordering};
483+
/// # use pyo3::prelude::*;
484+
///
485+
/// #[pyclass(frozen)]
486+
/// struct FrozenCounter {
487+
/// value: AtomicUsize,
488+
/// }
489+
///
490+
/// let cell = Python::with_gil(|py| {
491+
/// let counter = FrozenCounter { value: AtomicUsize::new(0) };
492+
///
493+
/// Py::new(py, counter).unwrap()
494+
/// });
495+
///
496+
/// cell.get().value.fetch_add(1, Ordering::Relaxed);
497+
/// ```
498+
pub fn get(&self) -> &T
499+
where
500+
T: PyClass<Frozen = True> + Sync,
501+
{
502+
let any = self.as_ptr() as *const PyAny;
503+
// SAFETY: The class itself is frozen and `Sync` and we do not access anything but `cell.contents.value`.
504+
unsafe {
505+
let cell: &PyCell<T> = PyNativeType::unchecked_downcast(&*any);
506+
&*cell.get_ptr()
507+
}
508+
}
470509
}
471510

472511
impl<T> Py<T> {

src/pycell.rs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,10 @@ use crate::exceptions::PyRuntimeError;
195195
use crate::impl_::pyclass::{
196196
PyClassBaseType, PyClassDict, PyClassImpl, PyClassThreadChecker, PyClassWeakRef,
197197
};
198-
use crate::pyclass::{boolean_struct::False, PyClass};
198+
use crate::pyclass::{
199+
boolean_struct::{False, True},
200+
PyClass,
201+
};
199202
use crate::pyclass_init::PyClassInitializer;
200203
use crate::type_object::{PyLayout, PySizedLayout};
201204
use crate::types::PyAny;
@@ -290,6 +293,8 @@ impl<T: PyClass> PyCell<T> {
290293

291294
/// Immutably borrows the value `T`. This borrow lasts as long as the returned `PyRef` exists.
292295
///
296+
/// For frozen classes, the simpler [`get`][Self::get] is available.
297+
///
293298
/// # Panics
294299
///
295300
/// Panics if the value is currently mutably borrowed. For a non-panicking variant, use
@@ -316,6 +321,8 @@ impl<T: PyClass> PyCell<T> {
316321
///
317322
/// This is the non-panicking variant of [`borrow`](#method.borrow).
318323
///
324+
/// For frozen classes, the simpler [`get`][Self::get] is available.
325+
///
319326
/// # Examples
320327
///
321328
/// ```
@@ -410,6 +417,41 @@ impl<T: PyClass> PyCell<T> {
410417
.map(|_: ()| &*self.contents.value.get())
411418
}
412419

420+
/// Provide an immutable borrow of the value `T` without acquiring the GIL.
421+
///
422+
/// This is available if the class is [`frozen`][macro@crate::pyclass] and [`Sync`].
423+
///
424+
/// While the GIL is usually required to get access to `&PyCell<T>`,
425+
/// compared to [`borrow`][Self::borrow] or [`try_borrow`][Self::try_borrow]
426+
/// this avoids any thread or borrow checking overhead at runtime.
427+
///
428+
/// # Examples
429+
///
430+
/// ```
431+
/// use std::sync::atomic::{AtomicUsize, Ordering};
432+
/// # use pyo3::prelude::*;
433+
///
434+
/// #[pyclass(frozen)]
435+
/// struct FrozenCounter {
436+
/// value: AtomicUsize,
437+
/// }
438+
///
439+
/// Python::with_gil(|py| {
440+
/// let counter = FrozenCounter { value: AtomicUsize::new(0) };
441+
///
442+
/// let cell = PyCell::new(py, counter).unwrap();
443+
///
444+
/// cell.get().value.fetch_add(1, Ordering::Relaxed);
445+
/// });
446+
/// ```
447+
pub fn get(&self) -> &T
448+
where
449+
T: PyClass<Frozen = True> + Sync,
450+
{
451+
// SAFETY: The class itself is frozen and `Sync` and we do not access anything but `self.contents.value`.
452+
unsafe { &*self.get_ptr() }
453+
}
454+
413455
/// Replaces the wrapped value with a new one, returning the old value.
414456
///
415457
/// # Panics
@@ -450,7 +492,7 @@ impl<T: PyClass> PyCell<T> {
450492
std::mem::swap(&mut *self.borrow_mut(), &mut *other.borrow_mut())
451493
}
452494

453-
fn get_ptr(&self) -> *mut T {
495+
pub(crate) fn get_ptr(&self) -> *mut T {
454496
self.contents.value.get()
455497
}
456498

tests/test_class_basics.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,3 +502,27 @@ fn inherited_weakref() {
502502
);
503503
});
504504
}
505+
506+
#[test]
507+
fn access_frozen_class_without_gil() {
508+
use std::sync::atomic::{AtomicUsize, Ordering};
509+
510+
#[pyclass(frozen)]
511+
struct FrozenCounter {
512+
value: AtomicUsize,
513+
}
514+
515+
let py_counter: Py<FrozenCounter> = Python::with_gil(|py| {
516+
let counter = FrozenCounter {
517+
value: AtomicUsize::new(0),
518+
};
519+
520+
let cell = PyCell::new(py, counter).unwrap();
521+
522+
cell.get().value.fetch_add(1, Ordering::Relaxed);
523+
524+
cell.into()
525+
});
526+
527+
assert_eq!(py_counter.get().value.load(Ordering::Relaxed), 1);
528+
}

tests/ui/invalid_frozen_pyclass_borrow.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pub struct Foo {
66
field: u32,
77
}
88

9-
fn borrow_mut_fails(foo: Py<Foo>, py: Python){
9+
fn borrow_mut_fails(foo: Py<Foo>, py: Python) {
1010
let borrow = foo.as_ref(py).borrow_mut();
1111
}
1212

@@ -16,8 +16,16 @@ struct MutableBase;
1616
#[pyclass(frozen, extends = MutableBase)]
1717
struct ImmutableChild;
1818

19-
fn borrow_mut_of_child_fails(child: Py<ImmutableChild>, py: Python){
19+
fn borrow_mut_of_child_fails(child: Py<ImmutableChild>, py: Python) {
2020
let borrow = child.as_ref(py).borrow_mut();
2121
}
2222

23-
fn main(){}
23+
fn py_get_of_mutable_class_fails(class: Py<MutableBase>) {
24+
class.get();
25+
}
26+
27+
fn pyclass_get_of_mutable_class_fails(class: &PyCell<MutableBase>) {
28+
class.get();
29+
}
30+
31+
fn main() {}

tests/ui/invalid_frozen_pyclass_borrow.stderr

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ error[E0271]: type mismatch resolving `<Foo as PyClass>::Frozen == False`
44
10 | let borrow = foo.as_ref(py).borrow_mut();
55
| ^^^^^^^^^^ expected `False`, found `True`
66
|
7-
note: required by a bound in `PyCell::<T>::borrow_mut`
7+
note: required by a bound in `pyo3::PyCell::<T>::borrow_mut`
88
--> src/pycell.rs
99
|
1010
| T: PyClass<Frozen = False>,
@@ -16,8 +16,32 @@ error[E0271]: type mismatch resolving `<ImmutableChild as PyClass>::Frozen == Fa
1616
20 | let borrow = child.as_ref(py).borrow_mut();
1717
| ^^^^^^^^^^ expected `False`, found `True`
1818
|
19-
note: required by a bound in `PyCell::<T>::borrow_mut`
19+
note: required by a bound in `pyo3::PyCell::<T>::borrow_mut`
2020
--> src/pycell.rs
2121
|
2222
| T: PyClass<Frozen = False>,
2323
| ^^^^^^^^^^^^^^ required by this bound in `PyCell::<T>::borrow_mut`
24+
25+
error[E0271]: type mismatch resolving `<MutableBase as PyClass>::Frozen == True`
26+
--> tests/ui/invalid_frozen_pyclass_borrow.rs:24:11
27+
|
28+
24 | class.get();
29+
| ^^^ expected `True`, found `False`
30+
|
31+
note: required by a bound in `pyo3::Py::<T>::get`
32+
--> src/instance.rs
33+
|
34+
| T: PyClass<Frozen = True> + Sync,
35+
| ^^^^^^^^^^^^^ required by this bound in `Py::<T>::get`
36+
37+
error[E0271]: type mismatch resolving `<MutableBase as PyClass>::Frozen == True`
38+
--> tests/ui/invalid_frozen_pyclass_borrow.rs:28:11
39+
|
40+
28 | class.get();
41+
| ^^^ expected `True`, found `False`
42+
|
43+
note: required by a bound in `pyo3::PyCell::<T>::get`
44+
--> src/pycell.rs
45+
|
46+
| T: PyClass<Frozen = True> + Sync,
47+
| ^^^^^^^^^^^^^ required by this bound in `PyCell::<T>::get`

0 commit comments

Comments
 (0)