1#[cfg(pyo3_disable_reference_pool)]
4use crate::impl_::panic::PanicTrap;
5use crate::{ffi, Python};
6
7use std::cell::Cell;
8#[cfg(not(pyo3_disable_reference_pool))]
9use std::sync::OnceLock;
10#[cfg_attr(pyo3_disable_reference_pool, allow(unused_imports))]
11use std::{mem, ptr::NonNull, sync};
12
13std::thread_local! {
14 static ATTACH_COUNT: Cell<isize> = const { Cell::new(0) };
24}
25
26const ATTACH_FORBIDDEN_DURING_TRAVERSE: isize = -1;
27
28#[inline(always)]
35pub(crate) fn thread_is_attached() -> bool {
36 ATTACH_COUNT.try_with(|c| c.get() > 0).unwrap_or(false)
37}
38
39pub(crate) enum AttachGuard {
41 Assumed,
43 Ensured { gstate: ffi::PyGILState_STATE },
45}
46
47pub(crate) enum AttachError {
49 ForbiddenDuringTraverse,
51 NotInitialized,
53 #[cfg(Py_3_13)]
54 Finalizing,
56}
57
58impl AttachGuard {
59 pub(crate) fn attach() -> Self {
65 match Self::try_attach() {
66 Ok(guard) => guard,
67 Err(AttachError::ForbiddenDuringTraverse) => {
68 panic!("{}", ForbidAttaching::FORBIDDEN_DURING_TRAVERSE)
69 }
70 Err(AttachError::NotInitialized) => {
71 crate::interpreter_lifecycle::ensure_initialized();
73 unsafe { Self::do_attach_unchecked() }
74 }
75 #[cfg(Py_3_13)]
76 Err(AttachError::Finalizing) => {
77 panic!("Cannot attach to the Python interpreter while it is finalizing.");
78 }
79 }
80 }
81
82 pub(crate) fn try_attach() -> Result<Self, AttachError> {
84 match ATTACH_COUNT.try_with(|c| c.get()) {
85 Ok(i) if i > 0 => {
86 return Ok(unsafe { Self::assume() });
88 }
89 Ok(ATTACH_FORBIDDEN_DURING_TRAVERSE) => {
91 return Err(AttachError::ForbiddenDuringTraverse)
92 }
93 _ => {}
95 }
96
97 if unsafe { ffi::Py_IsInitialized() } == 0 {
99 return Err(AttachError::NotInitialized);
100 }
101
102 crate::interpreter_lifecycle::wait_for_initialization();
106
107 #[cfg(Py_3_13)]
113 if unsafe { ffi::Py_IsFinalizing() } != 0 {
114 return Err(AttachError::Finalizing);
116 }
117
118 Ok(unsafe { Self::do_attach_unchecked() })
121 }
122
123 pub(crate) unsafe fn attach_unchecked() -> Self {
134 if thread_is_attached() {
135 return unsafe { Self::assume() };
136 }
137
138 unsafe { Self::do_attach_unchecked() }
139 }
140
141 #[cold]
143 unsafe fn do_attach_unchecked() -> Self {
144 let gstate = unsafe { ffi::PyGILState_Ensure() };
146 increment_attach_count();
147 drop_deferred_references(unsafe { Python::assume_attached() });
149 AttachGuard::Ensured { gstate }
150 }
151
152 pub(crate) unsafe fn assume() -> Self {
155 increment_attach_count();
156 drop_deferred_references(unsafe { Python::assume_attached() });
158 AttachGuard::Assumed
159 }
160
161 #[inline]
163 pub(crate) fn python(&self) -> Python<'_> {
164 unsafe { Python::assume_attached() }
166 }
167}
168
169impl Drop for AttachGuard {
171 fn drop(&mut self) {
172 match self {
173 AttachGuard::Assumed => {}
174 AttachGuard::Ensured { gstate } => unsafe {
175 ffi::PyGILState_Release(*gstate);
177 },
178 }
179 decrement_attach_count();
180 }
181}
182
183#[cfg(not(pyo3_disable_reference_pool))]
184type PyObjVec = Vec<NonNull<ffi::PyObject>>;
185
186#[cfg(not(pyo3_disable_reference_pool))]
187struct ReferencePool {
189 pending_decrefs: sync::Mutex<PyObjVec>,
190}
191
192#[cfg(not(pyo3_disable_reference_pool))]
193impl ReferencePool {
194 const fn new() -> Self {
195 Self {
196 pending_decrefs: sync::Mutex::new(Vec::new()),
197 }
198 }
199
200 fn register_decref(&self, obj: NonNull<ffi::PyObject>) {
201 self.pending_decrefs.lock().unwrap().push(obj);
202 }
203
204 fn drop_deferred_references(&self, _py: Python<'_>) {
205 let mut pending_decrefs = self.pending_decrefs.lock().unwrap();
206 if pending_decrefs.is_empty() {
207 return;
208 }
209
210 let decrefs = mem::take(&mut *pending_decrefs);
211 drop(pending_decrefs);
212
213 for ptr in decrefs {
214 unsafe { ffi::Py_DECREF(ptr.as_ptr()) };
215 }
216 }
217}
218
219#[cfg(not(pyo3_disable_reference_pool))]
220unsafe impl Send for ReferencePool {}
221
222#[cfg(not(pyo3_disable_reference_pool))]
223unsafe impl Sync for ReferencePool {}
224
225#[cfg(not(pyo3_disable_reference_pool))]
226static POOL: OnceLock<ReferencePool> = OnceLock::new();
227
228#[cfg(not(pyo3_disable_reference_pool))]
229fn get_pool() -> &'static ReferencePool {
230 POOL.get_or_init(ReferencePool::new)
231}
232
233#[cfg_attr(pyo3_disable_reference_pool, inline(always))]
234#[cfg_attr(pyo3_disable_reference_pool, allow(unused_variables))]
235fn drop_deferred_references(py: Python<'_>) {
236 #[cfg(not(pyo3_disable_reference_pool))]
237 if let Some(pool) = POOL.get() {
238 pool.drop_deferred_references(py);
239 }
240}
241
242pub(crate) struct SuspendAttach {
244 count: isize,
245 tstate: *mut ffi::PyThreadState,
246}
247
248impl SuspendAttach {
249 pub(crate) unsafe fn new() -> Self {
250 let count = ATTACH_COUNT.with(|c| c.replace(0));
251 let tstate = unsafe { ffi::PyEval_SaveThread() };
252
253 Self { count, tstate }
254 }
255}
256
257impl Drop for SuspendAttach {
258 fn drop(&mut self) {
259 ATTACH_COUNT.with(|c| c.set(self.count));
260 unsafe {
261 ffi::PyEval_RestoreThread(self.tstate);
262
263 #[cfg(not(pyo3_disable_reference_pool))]
265 if let Some(pool) = POOL.get() {
266 pool.drop_deferred_references(Python::assume_attached());
267 }
268 }
269 }
270}
271
272pub(crate) struct ForbidAttaching {
274 count: isize,
275}
276
277impl ForbidAttaching {
278 const FORBIDDEN_DURING_TRAVERSE: &'static str = "Attaching a thread to the interpreter is prohibited while a __traverse__ implementation is running.";
279
280 pub fn during_traverse() -> Self {
282 Self::new(ATTACH_FORBIDDEN_DURING_TRAVERSE)
283 }
284
285 fn new(reason: isize) -> Self {
286 let count = ATTACH_COUNT.with(|c| c.replace(reason));
287
288 Self { count }
289 }
290
291 #[cold]
292 fn bail(current: isize) {
293 match current {
294 ATTACH_FORBIDDEN_DURING_TRAVERSE => panic!("{}", Self::FORBIDDEN_DURING_TRAVERSE),
295 _ => panic!("Attaching a thread to the interpreter is currently prohibited."),
296 }
297 }
298}
299
300impl Drop for ForbidAttaching {
301 fn drop(&mut self) {
302 ATTACH_COUNT.with(|c| c.set(self.count));
303 }
304}
305
306#[inline]
316pub unsafe fn register_decref(obj: NonNull<ffi::PyObject>) {
317 #[cfg(not(pyo3_disable_reference_pool))]
318 {
319 get_pool().register_decref(obj);
320 }
321 #[cfg(all(
322 pyo3_disable_reference_pool,
323 not(pyo3_leak_on_drop_without_reference_pool)
324 ))]
325 {
326 let _trap = PanicTrap::new("Aborting the process to avoid panic-from-drop.");
327 panic!("Cannot drop pointer into Python heap without the thread being attached.");
328 }
329}
330
331#[cfg(any(not(Py_LIMITED_API), Py_3_11))]
333pub(crate) fn is_in_gc_traversal() -> bool {
334 ATTACH_COUNT
335 .try_with(|c| c.get() == ATTACH_FORBIDDEN_DURING_TRAVERSE)
336 .unwrap_or(false)
337}
338
339#[inline(always)]
341fn increment_attach_count() {
342 let _ = ATTACH_COUNT.try_with(|c| {
344 let current = c.get();
345 if current < 0 {
346 ForbidAttaching::bail(current);
347 }
348 c.set(current + 1);
349 });
350}
351
352#[inline(always)]
354fn decrement_attach_count() {
355 let _ = ATTACH_COUNT.try_with(|c| {
357 let current = c.get();
358 debug_assert!(
359 current > 0,
360 "Negative attach count detected. Please report this error to the PyO3 repo as a bug."
361 );
362 c.set(current - 1);
363 });
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 use crate::{Py, PyAny, Python};
371
372 fn get_object(py: Python<'_>) -> Py<PyAny> {
373 py.eval(c"object()", None, None).unwrap().unbind()
374 }
375
376 #[cfg(not(pyo3_disable_reference_pool))]
377 fn pool_dec_refs_does_not_contain(obj: &Py<PyAny>) -> bool {
378 !get_pool()
379 .pending_decrefs
380 .lock()
381 .unwrap()
382 .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) })
383 }
384
385 #[cfg(not(any(pyo3_disable_reference_pool, Py_GIL_DISABLED)))]
388 fn pool_dec_refs_contains(obj: &Py<PyAny>) -> bool {
389 get_pool()
390 .pending_decrefs
391 .lock()
392 .unwrap()
393 .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) })
394 }
395
396 #[test]
397 fn test_pyobject_drop_attached_decreases_refcnt() {
398 Python::attach(|py| {
399 let obj = get_object(py);
400
401 let reference = obj.clone_ref(py);
403
404 assert_eq!(obj._get_refcnt(py), 2);
405 #[cfg(not(pyo3_disable_reference_pool))]
406 assert!(pool_dec_refs_does_not_contain(&obj));
407
408 drop(reference);
410
411 assert_eq!(obj._get_refcnt(py), 1);
412 #[cfg(not(any(pyo3_disable_reference_pool)))]
413 assert!(pool_dec_refs_does_not_contain(&obj));
414 });
415 }
416
417 #[test]
418 #[cfg(all(not(pyo3_disable_reference_pool), not(target_arch = "wasm32")))] fn test_pyobject_drop_detached_doesnt_decrease_refcnt() {
420 let obj = Python::attach(|py| {
421 let obj = get_object(py);
422 let reference = obj.clone_ref(py);
424
425 assert_eq!(obj._get_refcnt(py), 2);
426 assert!(pool_dec_refs_does_not_contain(&obj));
427
428 std::thread::spawn(move || drop(reference)).join().unwrap();
430
431 assert_eq!(obj._get_refcnt(py), 2);
434 #[cfg(not(Py_GIL_DISABLED))]
435 assert!(pool_dec_refs_contains(&obj));
436 obj
437 });
438
439 #[allow(unused)]
441 Python::attach(|py| {
442 #[cfg(not(Py_GIL_DISABLED))]
446 assert_eq!(obj._get_refcnt(py), 1);
447 assert!(pool_dec_refs_does_not_contain(&obj));
448 });
449 }
450
451 #[test]
452 fn test_attach_counts() {
453 let get_attach_count = || ATTACH_COUNT.with(|c| c.get());
455
456 assert_eq!(get_attach_count(), 0);
457 Python::attach(|_| {
458 assert_eq!(get_attach_count(), 1);
459
460 let pool = unsafe { AttachGuard::assume() };
461 assert_eq!(get_attach_count(), 2);
462
463 let pool2 = unsafe { AttachGuard::assume() };
464 assert_eq!(get_attach_count(), 3);
465
466 drop(pool);
467 assert_eq!(get_attach_count(), 2);
468
469 Python::attach(|_| {
470 assert_eq!(get_attach_count(), 3);
472 });
473 assert_eq!(get_attach_count(), 2);
474
475 drop(pool2);
476 assert_eq!(get_attach_count(), 1);
477 });
478 assert_eq!(get_attach_count(), 0);
479 }
480
481 #[test]
482 fn test_detach() {
483 assert!(!thread_is_attached());
484
485 Python::attach(|py| {
486 assert!(thread_is_attached());
487
488 py.detach(move || {
489 assert!(!thread_is_attached());
490
491 Python::attach(|_| assert!(thread_is_attached()));
492
493 assert!(!thread_is_attached());
494 });
495
496 assert!(thread_is_attached());
497 });
498
499 assert!(!thread_is_attached());
500 }
501
502 #[cfg(feature = "py-clone")]
503 #[test]
504 #[should_panic]
505 fn test_detach_updates_refcounts() {
506 Python::attach(|py| {
507 let obj = get_object(py);
509 assert_eq!(obj._get_refcnt(py), 1);
510 py.detach(|| obj.clone());
512 });
513 }
514
515 #[test]
516 fn recursive_attach_ok() {
517 Python::attach(|py| {
518 let obj = Python::attach(|_| py.eval(c"object()", None, None).unwrap());
519 assert_eq!(obj._get_refcnt(), 1);
520 })
521 }
522
523 #[cfg(feature = "py-clone")]
524 #[test]
525 fn test_clone_attached() {
526 Python::attach(|py| {
527 let obj = get_object(py);
528 let count = obj._get_refcnt(py);
529
530 #[expect(clippy::redundant_clone)]
532 let c = obj.clone();
533 assert_eq!(count + 1, c._get_refcnt(py));
534 })
535 }
536
537 #[test]
538 #[cfg(not(pyo3_disable_reference_pool))]
539 fn test_drop_deferred_references_does_not_deadlock() {
540 use crate::ffi;
544
545 Python::attach(|py| {
546 let obj = get_object(py);
547
548 unsafe extern "C" fn capsule_drop(capsule: *mut ffi::PyObject) {
549 let pool = unsafe { AttachGuard::assume() };
552
553 unsafe {
555 use crate::Bound;
556
557 Bound::from_owned_ptr(
558 pool.python(),
559 ffi::PyCapsule_GetPointer(capsule, std::ptr::null()) as _,
560 )
561 };
562 }
563
564 let ptr = obj.into_ptr();
565
566 let capsule =
567 unsafe { ffi::PyCapsule_New(ptr as _, std::ptr::null(), Some(capsule_drop)) };
568
569 get_pool().register_decref(NonNull::new(capsule).unwrap());
570
571 get_pool().drop_deferred_references(py);
573 })
574 }
575
576 #[test]
577 #[cfg(not(pyo3_disable_reference_pool))]
578 fn test_attach_guard_drop_deferred_references() {
579 Python::attach(|py| {
580 let obj = get_object(py);
581
582 get_pool().register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap());
585 #[cfg(not(Py_GIL_DISABLED))]
586 assert!(pool_dec_refs_contains(&obj));
587 let _guard = AttachGuard::attach();
588 assert!(pool_dec_refs_does_not_contain(&obj));
589
590 get_pool().register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap());
593 #[cfg(not(Py_GIL_DISABLED))]
594 assert!(pool_dec_refs_contains(&obj));
595 let _guard2 = unsafe { AttachGuard::assume() };
596 assert!(pool_dec_refs_does_not_contain(&obj));
597 })
598 }
599}