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