Skip to main content

pyo3/internal/
state.rs

1//! Interaction with attachment of the current thread to the Python interpreter.
2
3#[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    /// This is an internal counter in pyo3 monitoring whether this thread is attached to the interpreter.
15    ///
16    /// It will be incremented whenever an AttachGuard is created, and decremented whenever
17    /// they are dropped.
18    ///
19    /// As a result, if this thread is attached to the interpreter, ATTACH_COUNT is greater than zero.
20    ///
21    /// Additionally, we sometimes need to prevent safe access to the Python interpreter,
22    /// e.g. when implementing `__traverse__`, which is represented by a negative value.
23    static ATTACH_COUNT: Cell<isize> = const { Cell::new(0) };
24}
25
26const ATTACH_FORBIDDEN_DURING_TRAVERSE: isize = -1;
27
28/// Checks whether the thread is attached to the Python interpreter.
29///
30/// Note: This uses pyo3's internal count rather than PyGILState_Check for two reasons:
31///  1) for performance
32///  2) PyGILState_Check always returns 1 if the sub-interpreter APIs have ever been called,
33///     which could lead to incorrect conclusions that the thread is attached.
34#[inline(always)]
35pub(crate) fn thread_is_attached() -> bool {
36    ATTACH_COUNT.try_with(|c| c.get() > 0).unwrap_or(false)
37}
38
39/// RAII type that represents thread attachment to the interpreter.
40pub(crate) enum AttachGuard {
41    /// Indicates the thread was already attached when this AttachGuard was acquired.
42    Assumed,
43    /// Indicates that we attached when this AttachGuard was acquired
44    Ensured { gstate: ffi::PyGILState_STATE },
45}
46
47/// Possible error when calling `try_attach()`
48pub(crate) enum AttachError {
49    /// Forbidden during GC traversal.
50    ForbiddenDuringTraverse,
51    /// The interpreter is not initialized.
52    NotInitialized,
53    #[cfg(Py_3_13)]
54    /// The interpreter is finalizing.
55    Finalizing,
56}
57
58impl AttachGuard {
59    /// PyO3 internal API for attaching to the Python interpreter. The public API is Python::attach.
60    ///
61    /// If the thread was already attached via PyO3, this returns
62    /// `AttachGuard::Assumed`. Otherwise, the thread will attach now and
63    /// `AttachGuard::Ensured` will be returned.
64    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                // try to initialize the interpreter and try again
72                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    /// Variant of the above which will will return gracefully if the interpreter cannot be attached to.
83    pub(crate) fn try_attach() -> Result<Self, AttachError> {
84        match ATTACH_COUNT.try_with(|c| c.get()) {
85            Ok(i) if i > 0 => {
86                // SAFETY: We just checked that the thread is already attached.
87                return Ok(unsafe { Self::assume() });
88            }
89            // Cannot attach during GC traversal.
90            Ok(ATTACH_FORBIDDEN_DURING_TRAVERSE) => {
91                return Err(AttachError::ForbiddenDuringTraverse)
92            }
93            // other cases handled below
94            _ => {}
95        }
96
97        // SAFETY: always safe to call this
98        if unsafe { ffi::Py_IsInitialized() } == 0 {
99            return Err(AttachError::NotInitialized);
100        }
101
102        // Py_IsInitialized() can return 1 while Py_InitializeEx is still
103        // running (e.g. importing site.py). Block until any in-progress PyO3
104        // initialization has fully completed.
105        crate::interpreter_lifecycle::wait_for_initialization();
106
107        // Calling `PyGILState_Ensure` while finalizing may crash CPython in unpredictable
108        // ways, we'll make a best effort attempt here to avoid that. (There's a time of
109        // check to time-of-use issue, but it's better than nothing.)
110        //
111        // SAFETY: always safe to call this
112        #[cfg(Py_3_13)]
113        if unsafe { ffi::Py_IsFinalizing() } != 0 {
114            // If the interpreter is not initialized, we cannot attach.
115            return Err(AttachError::Finalizing);
116        }
117
118        // SAFETY: We have done everything reasonable to ensure we're in a safe state to
119        // attach to the Python interpreter.
120        Ok(unsafe { Self::do_attach_unchecked() })
121    }
122
123    /// Acquires the `AttachGuard` without performing any state checking.
124    ///
125    /// This can be called in "unsafe" contexts where the normal interpreter state
126    /// checking performed by `AttachGuard::try_attach` may fail. This includes calling
127    /// as part of multi-phase interpreter initialization.
128    ///
129    /// # Safety
130    ///
131    /// The caller must ensure that the Python interpreter is sufficiently initialized
132    /// for a thread to be able to attach to it.
133    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    /// Attach to the interpreter, without a fast-path to check if the thread is already attached.
142    #[cold]
143    unsafe fn do_attach_unchecked() -> Self {
144        // SAFETY: interpreter is sufficiently initialized to attach a thread.
145        let gstate = unsafe { ffi::PyGILState_Ensure() };
146        increment_attach_count();
147        // SAFETY: just attached to the interpreter
148        drop_deferred_references(unsafe { Python::assume_attached() });
149        AttachGuard::Ensured { gstate }
150    }
151
152    /// Acquires the `AttachGuard` while assuming that the thread is already attached
153    /// to the interpreter.
154    pub(crate) unsafe fn assume() -> Self {
155        increment_attach_count();
156        // SAFETY: invariant of calling this function
157        drop_deferred_references(unsafe { Python::assume_attached() });
158        AttachGuard::Assumed
159    }
160
161    /// Gets the Python token associated with this [`AttachGuard`].
162    #[inline]
163    pub(crate) fn python(&self) -> Python<'_> {
164        // SAFETY: this guard guarantees the thread is attached
165        unsafe { Python::assume_attached() }
166    }
167}
168
169/// The Drop implementation for `AttachGuard` will decrement the attach count (and potentially detach).
170impl Drop for AttachGuard {
171    fn drop(&mut self) {
172        match self {
173            AttachGuard::Assumed => {}
174            AttachGuard::Ensured { gstate } => unsafe {
175                // Drop the objects in the pool before attempting to release the thread state
176                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))]
187/// Thread-safe storage for objects which were dec_ref while not attached.
188struct 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
242/// A guard which can be used to temporarily detach from the interpreter and restore on `Drop`.
243pub(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            // Update counts of `Py<T>` that were dropped while not attached.
264            #[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
272/// Used to lock safe access to the interpreter
273pub(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    /// Lock access to the interpreter while an implementation of `__traverse__` is running
281    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/// Registers a Python object pointer inside the release pool, to have its reference count decreased
307/// the next time the thread is attached in pyo3.
308///
309/// If the thread is attached, the reference count will be decreased immediately instead of being queued
310/// for later.
311///
312/// # Safety
313/// - The object must be an owned Python reference.
314/// - The reference must not be used after calling this function.
315#[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/// Private helper function to check if we are currently in a GC traversal (as detected by PyO3).
332#[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/// Increments pyo3's internal attach count - to be called whenever an AttachGuard is created.
340#[inline(always)]
341fn increment_attach_count() {
342    // Ignores the error in case this function called from `atexit`.
343    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/// Decrements pyo3's internal attach count - to be called whenever AttachGuard is dropped.
353#[inline(always)]
354fn decrement_attach_count() {
355    // Ignores the error in case this function called from `atexit`.
356    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    // With free-threading, threads can empty the POOL at any time, so this
386    // function does not test anything meaningful
387    #[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            // Create a reference to drop while attached.
402            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            // While attached, reference count will be decreased immediately.
409            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")))] // We are building wasm Python with pthreads disabled
419    fn test_pyobject_drop_detached_doesnt_decrease_refcnt() {
420        let obj = Python::attach(|py| {
421            let obj = get_object(py);
422            // Create a reference to drop while detached.
423            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            // Drop reference in a separate (detached) thread.
429            std::thread::spawn(move || drop(reference)).join().unwrap();
430
431            // The reference count should not have changed, it is remembered
432            // to release later.
433            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        // On next attach, the reference is released
440        #[allow(unused)]
441        Python::attach(|py| {
442            // With free-threading, another thread could still be processing
443            // DECREFs after releasing the lock on the POOL, so the
444            // refcnt could still be 2 when this assert happens
445            #[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        // Check `attach` and AttachGuard both increase counts correctly
454        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                // nested `attach` updates attach count
471                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            // Make a simple object with 1 reference
508            let obj = get_object(py);
509            assert_eq!(obj._get_refcnt(py), 1);
510            // Cloning the object when detached should panic
511            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            // Cloning when attached should increase reference count immediately
531            #[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        // drop_deferred_references can run arbitrary Python code during Py_DECREF.
541        // if the locking is implemented incorrectly, it will deadlock.
542
543        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                // This line will implicitly call drop_deferred_references
550                // -> and so cause deadlock if drop_deferred_references is not handling recursion correctly.
551                let pool = unsafe { AttachGuard::assume() };
552
553                // Rebuild obj so that it can be dropped
554                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            // Updating the counts will call decref on the capsule, which calls capsule_drop
572            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            // For AttachGuard::attach
583
584            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            // For AttachGuard::assume
591
592            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}
⚠️ Internal Docs ⚠️ Not Public API 👉 Official Docs Here