|
1 | 1 | // Copyright (c) 2017-present PyO3 Project and Contributors |
2 | 2 | //! Python type object information |
3 | 3 |
|
| 4 | +use crate::conversion::IntoPyPointer; |
4 | 5 | use crate::once_cell::GILOnceCell; |
5 | | -use crate::pyclass::{initialize_type_object, PyClass}; |
| 6 | +use crate::pyclass::{initialize_type_object, py_class_attributes, PyClass}; |
6 | 7 | use crate::pyclass_init::PyObjectInit; |
7 | 8 | use crate::types::{PyAny, PyType}; |
8 | | -use crate::{ffi, AsPyPointer, PyNativeType, Python}; |
| 9 | +use crate::{ffi, AsPyPointer, PyErr, PyNativeType, PyObject, PyResult, Python}; |
9 | 10 | use parking_lot::{const_mutex, Mutex}; |
10 | 11 | use std::thread::{self, ThreadId}; |
11 | 12 |
|
@@ -139,51 +140,108 @@ where |
139 | 140 | pub struct LazyStaticType { |
140 | 141 | // Boxed because Python expects the type object to have a stable address. |
141 | 142 | 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. |
143 | 145 | initializing_threads: Mutex<Vec<ThreadId>>, |
| 146 | + tp_dict_filled: GILOnceCell<PyResult<()>>, |
144 | 147 | } |
145 | 148 |
|
146 | 149 | impl LazyStaticType { |
147 | 150 | pub const fn new() -> Self { |
148 | 151 | LazyStaticType { |
149 | 152 | value: GILOnceCell::new(), |
150 | 153 | initializing_threads: const_mutex(Vec::new()), |
| 154 | + tp_dict_filled: GILOnceCell::new(), |
151 | 155 | } |
152 | 156 | } |
153 | 157 |
|
154 | 158 | 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, || { |
173 | 160 | let mut type_object = Box::new(ffi::PyTypeObject_INIT); |
174 | | - |
175 | 161 | initialize_type_object::<T>(py, T::MODULE, type_object.as_mut()).unwrap_or_else(|e| { |
176 | 162 | e.print(py); |
177 | 163 | panic!("An error occurred while initializing class {}", T::NAME) |
178 | 164 | }); |
| 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) }; |
179 | 211 |
|
180 | 212 | // 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.) |
182 | 214 | *self.initializing_threads.lock() = Vec::new(); |
| 215 | + result |
| 216 | + }); |
183 | 217 |
|
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 | + } |
186 | 243 | } |
| 244 | + Ok(()) |
187 | 245 | } |
188 | 246 |
|
189 | 247 | // This is necessary for making static `LazyStaticType`s |
|
0 commit comments