Skip to main content

pyo3/internal/
state.rs

1// TODO https://github.com/PyO3/pyo3/issues/5487
2#![allow(clippy::undocumented_unsafe_blocks)]
3
4//! Interaction with attachment of the current thread to the Python interpreter.
5
6#[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    /// This is an internal counter in pyo3 monitoring whether this thread is attached to the interpreter.
19    ///
20    /// It will be incremented whenever an AttachGuard is created, and decremented whenever
21    /// they are dropped.
22    ///
23    /// As a result, if this thread is attached to the interpreter, ATTACH_COUNT is greater than zero.
24    ///
25    /// Additionally, we sometimes need to prevent safe access to the Python interpreter,
26    /// e.g. when implementing `__traverse__`, which is represented by a negative value.
27    static ATTACH_COUNT: Cell<isize> = const { Cell::new(0) };
28}
29
30const ATTACH_FORBIDDEN_DURING_TRAVERSE: isize = -1;
31
32/// Checks whether the thread is attached to the Python interpreter.
33///
34/// Note: This uses pyo3's internal count rather than PyGILState_Check for two reasons:
35///  1) for performance
36///  2) PyGILState_Check always returns 1 if the sub-interpreter APIs have ever been called,
37///     which could lead to incorrect conclusions that the thread is attached.
38#[inline(always)]
39pub(crate) fn thread_is_attached() -> bool {
40    ATTACH_COUNT.try_with(|c| c.get() > 0).unwrap_or(false)
41}
42
43/// RAII type that represents thread attachment to the interpreter.
44pub(crate) enum AttachGuard {
45    /// Indicates the thread was already attached when this AttachGuard was acquired.
46    Assumed,
47    /// Indicates that we attached when this AttachGuard was acquired
48    Ensured { gstate: ffi::PyGILState_STATE },
49}
50
51/// Possible error when calling `try_attach()`
52pub(crate) enum AttachError {
53    /// Forbidden during GC traversal.
54    ForbiddenDuringTraverse,
55    /// The interpreter is not initialized.
56    NotInitialized,
57    #[cfg(Py_3_13)]
58    /// The interpreter is finalizing.
59    Finalizing,
60}
61
62impl AttachGuard {
63    /// PyO3 internal API for attaching to the Python interpreter. The public API is Python::attach.
64    ///
65    /// If the thread was already attached via PyO3, this returns
66    /// `AttachGuard::Assumed`. Otherwise, the thread will attach now and
67    /// `AttachGuard::Ensured` will be returned.
68    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                // try to initialize the interpreter and try again
76                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    /// Variant of the above which will will return gracefully if the interpreter cannot be attached to.
87    pub(crate) fn try_attach() -> Result<Self, AttachError> {
88        match ATTACH_COUNT.try_with(|c| c.get()) {
89            Ok(i) if i > 0 => {
90                // SAFETY: We just checked that the thread is already attached.
91                return Ok(unsafe { Self::assume() });
92            }
93            // Cannot attach during GC traversal.
94            Ok(ATTACH_FORBIDDEN_DURING_TRAVERSE) => {
95                return Err(AttachError::ForbiddenDuringTraverse)
96            }
97            // other cases handled below
98            _ => {}
99        }
100
101        // SAFETY: always safe to call this
102        if unsafe { ffi::Py_IsInitialized() } == 0 {
103            return Err(AttachError::NotInitialized);
104        }
105
106        // Py_IsInitialized() can return 1 while Py_InitializeEx is still
107        // running (e.g. importing site.py). Block until any in-progress PyO3
108        // initialization has fully completed.
109        crate::interpreter_lifecycle::wait_for_initialization();
110
111        // Calling `PyGILState_Ensure` while finalizing may crash CPython in unpredictable
112        // ways, we'll make a best effort attempt here to avoid that. (There's a time of
113        // check to time-of-use issue, but it's better than nothing.)
114        //
115        // SAFETY: always safe to call this
116        #[cfg(Py_3_13)]
117        if unsafe { ffi::Py_IsFinalizing() } != 0 {
118            // If the interpreter is not initialized, we cannot attach.
119            return Err(AttachError::Finalizing);
120        }
121
122        // SAFETY: We have done everything reasonable to ensure we're in a safe state to
123        // attach to the Python interpreter.
124        Ok(unsafe { Self::do_attach_unchecked() })
125    }
126
127    /// Acquires the `AttachGuard` without performing any state checking.
128    ///
129    /// This can be called in "unsafe" contexts where the normal interpreter state
130    /// checking performed by `AttachGuard::try_attach` may fail. This includes calling
131    /// as part of multi-phase interpreter initialization.
132    ///
133    /// # Safety
134    ///
135    /// The caller must ensure that the Python interpreter is sufficiently initialized
136    /// for a thread to be able to attach to it.
137    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    /// Attach to the interpreter, without a fast-path to check if the thread is already attached.
146    #[cold]
147    unsafe fn do_attach_unchecked() -> Self {
148        // SAFETY: interpreter is sufficiently initialized to attach a thread.
149        let gstate = unsafe { ffi::PyGILState_Ensure() };
150        increment_attach_count();
151        // SAFETY: just attached to the interpreter
152        drop_deferred_references(unsafe { Python::assume_attached() });
153        AttachGuard::Ensured { gstate }
154    }
155
156    /// Acquires the `AttachGuard` while assuming that the thread is already attached
157    /// to the interpreter.
158    pub(crate) unsafe fn assume() -> Self {
159        increment_attach_count();
160        // SAFETY: invariant of calling this function
161        drop_deferred_references(unsafe { Python::assume_attached() });
162        AttachGuard::Assumed
163    }
164
165    /// Gets the Python token associated with this [`AttachGuard`].
166    #[inline]
167    pub(crate) fn python(&self) -> Python<'_> {
168        // SAFETY: this guard guarantees the thread is attached
169        unsafe { Python::assume_attached() }
170    }
171}
172
173/// The Drop implementation for `AttachGuard` will decrement the attach count (and potentially detach).
174impl Drop for AttachGuard {
175    fn drop(&mut self) {
176        match self {
177            AttachGuard::Assumed => {}
178            AttachGuard::Ensured { gstate } => unsafe {
179                // Drop the objects in the pool before attempting to release the thread state
180                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))]
191/// Thread-safe storage for objects which were dec_ref while not attached.
192struct 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
246/// A guard which can be used to temporarily detach from the interpreter and restore on `Drop`.
247pub(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            // Update counts of `Py<T>` that were dropped while not attached.
268            #[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
276/// Used to lock safe access to the interpreter
277pub(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    /// Lock access to the interpreter while an implementation of `__traverse__` is running
285    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/// Registers a Python object pointer inside the release pool, to have its reference count decreased
311/// the next time the thread is attached in pyo3.
312///
313/// If the thread is attached, the reference count will be decreased immediately instead of being queued
314/// for later.
315///
316/// # Safety
317/// - The object must be an owned Python reference.
318/// - The reference must not be used after calling this function.
319#[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/// Private helper function to check if we are currently in a GC traversal (as detected by PyO3).
336#[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/// Increments pyo3's internal attach count - to be called whenever an AttachGuard is created.
344#[inline(always)]
345fn increment_attach_count() {
346    // Ignores the error in case this function called from `atexit`.
347    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/// Decrements pyo3's internal attach count - to be called whenever AttachGuard is dropped.
357#[inline(always)]
358fn decrement_attach_count() {
359    // Ignores the error in case this function called from `atexit`.
360    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    // With free-threading, threads can empty the POOL at any time, so this
390    // function does not test anything meaningful
391    #[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            // Create a reference to drop while attached.
406            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            // While attached, reference count will be decreased immediately.
413            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")))] // We are building wasm Python with pthreads disabled
423    fn test_pyobject_drop_detached_doesnt_decrease_refcnt() {
424        let obj = Python::attach(|py| {
425            let obj = get_object(py);
426            // Create a reference to drop while detached.
427            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            // Drop reference in a separate (detached) thread.
433            std::thread::spawn(move || drop(reference)).join().unwrap();
434
435            // The reference count should not have changed, it is remembered
436            // to release later.
437            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        // On next attach, the reference is released
444        #[allow(unused)]
445        Python::attach(|py| {
446            // With free-threading, another thread could still be processing
447            // DECREFs after releasing the lock on the POOL, so the
448            // refcnt could still be 2 when this assert happens
449            #[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        // Check `attach` and AttachGuard both increase counts correctly
458        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                // nested `attach` updates attach count
475                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            // Make a simple object with 1 reference
512            let obj = get_object(py);
513            assert_eq!(obj._get_refcnt(py), 1);
514            // Cloning the object when detached should panic
515            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            // Cloning when attached should increase reference count immediately
535            #[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        // drop_deferred_references can run arbitrary Python code during Py_DECREF.
545        // if the locking is implemented incorrectly, it will deadlock.
546
547        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                // This line will implicitly call drop_deferred_references
554                // -> and so cause deadlock if drop_deferred_references is not handling recursion correctly.
555                let pool = unsafe { AttachGuard::assume() };
556
557                // Rebuild obj so that it can be dropped
558                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            // Updating the counts will call decref on the capsule, which calls capsule_drop
576            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            // For AttachGuard::attach
587
588            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            // For AttachGuard::assume
595
596            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}
⚠️ Internal Docs ⚠️ Not Public API 👉 Official Docs Here