Skip to content

Commit 10306bd

Browse files
authored
Merge pull request #995 from scalexm/recursive-attr
Re-enable recursive class attributes
2 parents a5e3d4e + f5e1dff commit 10306bd

File tree

6 files changed

+112
-59
lines changed

6 files changed

+112
-59
lines changed

CHANGELOG.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
3131

3232
### Removed
3333
- Remove `ManagedPyRef` (unused, and needs specialization) [#930](https://github.com/PyO3/pyo3/pull/930)
34-
- Disable `#[classattr]` where the class attribute is the same type as the class. (This may be re-enabled in the future; the previous implemenation was unsound.) [#975](https://github.com/PyO3/pyo3/pull/975)
3534

3635
### Fixed
3736
- Fix passing explicit `None` to `Option<T>` argument `#[pyfunction]` with a default value. [#936](https://github.com/PyO3/pyo3/pull/936)

guide/src/class.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -565,11 +565,6 @@ impl MyClass {
565565
}
566566
```
567567

568-
Note that defining a class attribute of the same type as the class will make the class unusable.
569-
Attempting to use the class will cause a panic reading `Recursive evaluation of type_object`.
570-
As an alternative, if having the attribute on instances is acceptable, create a `#[getter]` which
571-
uses a `GILOnceCell` to cache the attribute value. Or add the attribute to a module instead.
572-
573568
## Callable objects
574569

575570
To specify a custom `__call__` method for a custom class, the method needs to be annotated with

src/once_cell.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ impl<T> GILOnceCell<T> {
5858
/// calling `f()`. Even when this happens `GILOnceCell` guarantees that only **one** write
5959
/// to the cell ever occurs - other threads will simply discard the value they compute and
6060
/// return the result of the first complete computation.
61+
/// 3) if f() does not release the GIL and does not panic, it is guaranteed to be called
62+
/// exactly once, even if multiple threads attempt to call `get_or_init`
63+
/// 4) if f() can panic but still does not release the GIL, it may be called multiple times,
64+
/// but it is guaranteed that f() will never be called concurrently
6165
pub fn get_or_init<F>(&self, py: Python, f: F) -> &T
6266
where
6367
F: FnOnce() -> T,

src/pyclass.rs

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
//! `PyClass` trait
22
use crate::class::methods::{PyClassAttributeDef, PyMethodDefType, PyMethods};
33
use crate::class::proto_methods::PyProtoMethods;
4-
use crate::conversion::{AsPyPointer, FromPyPointer, IntoPyPointer, ToPyObject};
4+
use crate::conversion::{AsPyPointer, FromPyPointer};
55
use crate::pyclass_slots::{PyClassDict, PyClassWeakRef};
66
use crate::type_object::{type_flags, PyLayout};
7-
use crate::types::{PyAny, PyDict};
7+
use crate::types::PyAny;
88
use crate::{class, ffi, PyCell, PyErr, PyNativeType, PyResult, PyTypeInfo, Python};
99
use std::ffi::CString;
1010
use std::os::raw::c_void;
@@ -188,23 +188,14 @@ where
188188
// buffer protocol
189189
type_object.tp_as_buffer = T::buffer_methods().map_or_else(ptr::null_mut, |p| p.as_ptr());
190190

191-
let (new, call, mut methods, attrs) = py_class_method_defs::<T>();
191+
let (new, call, mut methods) = py_class_method_defs::<T>();
192192

193193
// normal methods
194194
if !methods.is_empty() {
195195
methods.push(ffi::PyMethodDef_INIT);
196196
type_object.tp_methods = Box::into_raw(methods.into_boxed_slice()) as _;
197197
}
198198

199-
// class attributes
200-
if !attrs.is_empty() {
201-
let dict = PyDict::new(py);
202-
for attr in attrs {
203-
dict.set_item(attr.name, (attr.meth)(py))?;
204-
}
205-
type_object.tp_dict = dict.to_object(py).into_ptr();
206-
}
207-
208199
// __new__ method
209200
type_object.tp_new = new;
210201
// __call__ method
@@ -248,14 +239,19 @@ fn py_class_flags<T: PyTypeInfo>(type_object: &mut ffi::PyTypeObject) {
248239
}
249240
}
250241

242+
pub(crate) fn py_class_attributes<T: PyMethods>() -> impl Iterator<Item = PyClassAttributeDef> {
243+
T::py_methods().into_iter().filter_map(|def| match def {
244+
PyMethodDefType::ClassAttribute(attr) => Some(*attr),
245+
_ => None,
246+
})
247+
}
248+
251249
fn py_class_method_defs<T: PyMethods>() -> (
252250
Option<ffi::newfunc>,
253251
Option<ffi::PyCFunctionWithKeywords>,
254252
Vec<ffi::PyMethodDef>,
255-
Vec<PyClassAttributeDef>,
256253
) {
257254
let mut defs = Vec::new();
258-
let mut attrs = Vec::new();
259255
let mut call = None;
260256
let mut new = None;
261257

@@ -278,14 +274,11 @@ fn py_class_method_defs<T: PyMethods>() -> (
278274
| PyMethodDefType::Static(ref def) => {
279275
defs.push(def.as_method_def());
280276
}
281-
PyMethodDefType::ClassAttribute(def) => {
282-
attrs.push(def);
283-
}
284277
_ => (),
285278
}
286279
}
287280

288-
(new, call, defs, attrs)
281+
(new, call, defs)
289282
}
290283

291284
fn py_class_properties<T: PyMethods>() -> Vec<ffi::PyGetSetDef> {

src/type_object.rs

Lines changed: 83 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// Copyright (c) 2017-present PyO3 Project and Contributors
22
//! Python type object information
33
4+
use crate::conversion::IntoPyPointer;
45
use crate::once_cell::GILOnceCell;
5-
use crate::pyclass::{initialize_type_object, PyClass};
6+
use crate::pyclass::{initialize_type_object, py_class_attributes, PyClass};
67
use crate::pyclass_init::PyObjectInit;
78
use crate::types::{PyAny, PyType};
8-
use crate::{ffi, AsPyPointer, PyNativeType, Python};
9+
use crate::{ffi, AsPyPointer, PyErr, PyNativeType, PyObject, PyResult, Python};
910
use parking_lot::{const_mutex, Mutex};
1011
use std::thread::{self, ThreadId};
1112

@@ -139,51 +140,108 @@ where
139140
pub struct LazyStaticType {
140141
// Boxed because Python expects the type object to have a stable address.
141142
value: GILOnceCell<*mut ffi::PyTypeObject>,
142-
// Threads which have begun initialization. Used for reentrant initialization detection.
143+
// Threads which have begun initialization of the `tp_dict`. Used for
144+
// reentrant initialization detection.
143145
initializing_threads: Mutex<Vec<ThreadId>>,
146+
tp_dict_filled: GILOnceCell<PyResult<()>>,
144147
}
145148

146149
impl LazyStaticType {
147150
pub const fn new() -> Self {
148151
LazyStaticType {
149152
value: GILOnceCell::new(),
150153
initializing_threads: const_mutex(Vec::new()),
154+
tp_dict_filled: GILOnceCell::new(),
151155
}
152156
}
153157

154158
pub fn get_or_init<T: PyClass>(&self, py: Python) -> *mut ffi::PyTypeObject {
155-
*self.value.get_or_init(py, || {
156-
{
157-
// Code evaluated at class init time, such as class attributes, might lead to
158-
// recursive initalization of the type object if the class attribute is the same
159-
// type as the class.
160-
//
161-
// That could lead to all sorts of unsafety such as using incomplete type objects
162-
// to initialize class instances, so recursive initialization is prevented.
163-
let thread_id = thread::current().id();
164-
let mut threads = self.initializing_threads.lock();
165-
if threads.contains(&thread_id) {
166-
panic!("Recursive initialization of type_object for {}", T::NAME);
167-
} else {
168-
threads.push(thread_id)
169-
}
170-
}
171-
172-
// Okay, not recursive initialization - can proceed safely.
159+
let type_object = *self.value.get_or_init(py, || {
173160
let mut type_object = Box::new(ffi::PyTypeObject_INIT);
174-
175161
initialize_type_object::<T>(py, T::MODULE, type_object.as_mut()).unwrap_or_else(|e| {
176162
e.print(py);
177163
panic!("An error occurred while initializing class {}", T::NAME)
178164
});
165+
Box::into_raw(type_object)
166+
});
167+
168+
// We might want to fill the `tp_dict` with python instances of `T`
169+
// itself. In order to do so, we must first initialize the type object
170+
// with an empty `tp_dict`: now we can create instances of `T`.
171+
//
172+
// Then we fill the `tp_dict`. Multiple threads may try to fill it at
173+
// the same time, but only one of them will succeed.
174+
//
175+
// More importantly, if a thread is performing initialization of the
176+
// `tp_dict`, it can still request the type object through `get_or_init`,
177+
// but the `tp_dict` may appear empty of course.
178+
179+
if self.tp_dict_filled.get(py).is_some() {
180+
// `tp_dict` is already filled: ok.
181+
return type_object;
182+
}
183+
184+
{
185+
let thread_id = thread::current().id();
186+
let mut threads = self.initializing_threads.lock();
187+
if threads.contains(&thread_id) {
188+
// Reentrant call: just return the type object, even if the
189+
// `tp_dict` is not filled yet.
190+
return type_object;
191+
}
192+
threads.push(thread_id);
193+
}
194+
195+
// Pre-compute the class attribute objects: this can temporarily
196+
// release the GIL since we're calling into arbitrary user code. It
197+
// means that another thread can continue the initialization in the
198+
// meantime: at worst, we'll just make a useless computation.
199+
let mut items = vec![];
200+
for attr in py_class_attributes::<T>() {
201+
items.push((attr.name, (attr.meth)(py)));
202+
}
203+
204+
// Now we hold the GIL and we can assume it won't be released until we
205+
// return from the function.
206+
let result = self.tp_dict_filled.get_or_init(py, move || {
207+
let tp_dict = unsafe { (*type_object).tp_dict };
208+
let result = initialize_tp_dict(py, tp_dict, items);
209+
// See discussion on #982 for why we need this.
210+
unsafe { ffi::PyType_Modified(type_object) };
179211

180212
// Initialization successfully complete, can clear the thread list.
181-
// (No futher calls to get_or_init() will try to init, on any thread.)
213+
// (No further calls to get_or_init() will try to init, on any thread.)
182214
*self.initializing_threads.lock() = Vec::new();
215+
result
216+
});
183217

184-
Box::into_raw(type_object)
185-
})
218+
if let Err(err) = result {
219+
err.clone_ref(py).print(py);
220+
panic!("An error occured while initializing `{}.__dict__`", T::NAME);
221+
}
222+
223+
type_object
224+
}
225+
}
226+
227+
fn initialize_tp_dict(
228+
py: Python,
229+
tp_dict: *mut ffi::PyObject,
230+
items: Vec<(&'static str, PyObject)>,
231+
) -> PyResult<()> {
232+
use std::ffi::CString;
233+
234+
// We hold the GIL: the dictionary update can be considered atomic from
235+
// the POV of other threads.
236+
for (key, val) in items {
237+
let ret = unsafe {
238+
ffi::PyDict_SetItemString(tp_dict, CString::new(key)?.as_ptr(), val.into_ptr())
239+
};
240+
if ret < 0 {
241+
return Err(PyErr::fetch(py));
242+
}
186243
}
244+
Ok(())
187245
}
188246

189247
// This is necessary for making static `LazyStaticType`s

tests/test_class_attributes.rs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ impl Foo {
3434
fn bar() -> Bar {
3535
Bar { x: 2 }
3636
}
37+
38+
#[classattr]
39+
fn foo() -> Foo {
40+
Foo { x: 1 }
41+
}
3742
}
3843

3944
#[test]
@@ -54,23 +59,22 @@ fn class_attributes_are_immutable() {
5459
py_expect_exception!(py, foo_obj, "foo_obj.a = 6", TypeError);
5560
}
5661

57-
#[pyclass]
58-
struct SelfClassAttribute {
59-
#[pyo3(get)]
60-
x: i32,
61-
}
62-
6362
#[pymethods]
64-
impl SelfClassAttribute {
63+
impl Bar {
6564
#[classattr]
66-
const SELF: SelfClassAttribute = SelfClassAttribute { x: 1 };
65+
fn foo() -> Foo {
66+
Foo { x: 3 }
67+
}
6768
}
6869

6970
#[test]
70-
#[should_panic(expected = "Recursive initialization of type_object for SelfClassAttribute")]
7171
fn recursive_class_attributes() {
7272
let gil = Python::acquire_gil();
7373
let py = gil.python();
7474

75-
py.get_type::<SelfClassAttribute>();
75+
let foo_obj = py.get_type::<Foo>();
76+
let bar_obj = py.get_type::<Bar>();
77+
py_assert!(py, foo_obj, "foo_obj.foo.x == 1");
78+
py_assert!(py, foo_obj, "foo_obj.bar.x == 2");
79+
py_assert!(py, bar_obj, "bar_obj.foo.x == 3");
7680
}

0 commit comments

Comments
 (0)