#[cfg(pyo3_disable_reference_pool)]
use crate::impl_::panic::PanicTrap;
use crate::{ffi, Python};
use std::cell::Cell;
#[cfg(not(pyo3_disable_reference_pool))]
use std::sync::OnceLock;
#[cfg_attr(pyo3_disable_reference_pool, allow(unused_imports))]
use std::{mem, ptr::NonNull, sync};
std::thread_local! {
static ATTACH_COUNT: Cell<isize> = const { Cell::new(0) };
}
const ATTACH_FORBIDDEN_DURING_TRAVERSE: isize = -1;
#[inline(always)]
pub(crate) fn thread_is_attached() -> bool {
ATTACH_COUNT.try_with(|c| c.get() > 0).unwrap_or(false)
}
pub(crate) enum AttachGuard {
Assumed,
Ensured { gstate: ffi::PyGILState_STATE },
}
pub(crate) enum AttachError {
ForbiddenDuringTraverse,
NotInitialized,
#[cfg(Py_3_13)]
Finalizing,
}
impl AttachGuard {
pub(crate) fn attach() -> Self {
match Self::try_attach() {
Ok(guard) => guard,
Err(AttachError::ForbiddenDuringTraverse) => {
panic!("{}", ForbidAttaching::FORBIDDEN_DURING_TRAVERSE)
}
Err(AttachError::NotInitialized) => {
crate::interpreter_lifecycle::ensure_initialized();
unsafe { Self::do_attach_unchecked() }
}
#[cfg(Py_3_13)]
Err(AttachError::Finalizing) => {
panic!("Cannot attach to the Python interpreter while it is finalizing.");
}
}
}
pub(crate) fn try_attach() -> Result<Self, AttachError> {
match ATTACH_COUNT.try_with(|c| c.get()) {
Ok(i) if i > 0 => {
return Ok(unsafe { Self::assume() });
}
Ok(ATTACH_FORBIDDEN_DURING_TRAVERSE) => {
return Err(AttachError::ForbiddenDuringTraverse)
}
_ => {}
}
if unsafe { ffi::Py_IsInitialized() } == 0 {
return Err(AttachError::NotInitialized);
}
crate::interpreter_lifecycle::wait_for_initialization();
#[cfg(Py_3_13)]
if unsafe { ffi::Py_IsFinalizing() } != 0 {
return Err(AttachError::Finalizing);
}
Ok(unsafe { Self::do_attach_unchecked() })
}
pub(crate) unsafe fn attach_unchecked() -> Self {
if thread_is_attached() {
return unsafe { Self::assume() };
}
unsafe { Self::do_attach_unchecked() }
}
#[cold]
unsafe fn do_attach_unchecked() -> Self {
let gstate = unsafe { ffi::PyGILState_Ensure() };
increment_attach_count();
drop_deferred_references(unsafe { Python::assume_attached() });
AttachGuard::Ensured { gstate }
}
pub(crate) unsafe fn assume() -> Self {
increment_attach_count();
drop_deferred_references(unsafe { Python::assume_attached() });
AttachGuard::Assumed
}
#[inline]
pub(crate) fn python(&self) -> Python<'_> {
unsafe { Python::assume_attached() }
}
}
impl Drop for AttachGuard {
fn drop(&mut self) {
match self {
AttachGuard::Assumed => {}
AttachGuard::Ensured { gstate } => unsafe {
ffi::PyGILState_Release(*gstate);
},
}
decrement_attach_count();
}
}
#[cfg(not(pyo3_disable_reference_pool))]
type PyObjVec = Vec<NonNull<ffi::PyObject>>;
#[cfg(not(pyo3_disable_reference_pool))]
struct ReferencePool {
pending_decrefs: sync::Mutex<PyObjVec>,
}
#[cfg(not(pyo3_disable_reference_pool))]
impl ReferencePool {
const fn new() -> Self {
Self {
pending_decrefs: sync::Mutex::new(Vec::new()),
}
}
fn register_decref(&self, obj: NonNull<ffi::PyObject>) {
self.pending_decrefs.lock().unwrap().push(obj);
}
fn drop_deferred_references(&self, _py: Python<'_>) {
let mut pending_decrefs = self.pending_decrefs.lock().unwrap();
if pending_decrefs.is_empty() {
return;
}
let decrefs = mem::take(&mut *pending_decrefs);
drop(pending_decrefs);
for ptr in decrefs {
unsafe { ffi::Py_DECREF(ptr.as_ptr()) };
}
}
}
#[cfg(not(pyo3_disable_reference_pool))]
unsafe impl Send for ReferencePool {}
#[cfg(not(pyo3_disable_reference_pool))]
unsafe impl Sync for ReferencePool {}
#[cfg(not(pyo3_disable_reference_pool))]
static POOL: OnceLock<ReferencePool> = OnceLock::new();
#[cfg(not(pyo3_disable_reference_pool))]
fn get_pool() -> &'static ReferencePool {
POOL.get_or_init(ReferencePool::new)
}
#[cfg_attr(pyo3_disable_reference_pool, inline(always))]
#[cfg_attr(pyo3_disable_reference_pool, allow(unused_variables))]
fn drop_deferred_references(py: Python<'_>) {
#[cfg(not(pyo3_disable_reference_pool))]
if let Some(pool) = POOL.get() {
pool.drop_deferred_references(py);
}
}
pub(crate) struct SuspendAttach {
count: isize,
tstate: *mut ffi::PyThreadState,
}
impl SuspendAttach {
pub(crate) unsafe fn new() -> Self {
let count = ATTACH_COUNT.with(|c| c.replace(0));
let tstate = unsafe { ffi::PyEval_SaveThread() };
Self { count, tstate }
}
}
impl Drop for SuspendAttach {
fn drop(&mut self) {
ATTACH_COUNT.with(|c| c.set(self.count));
unsafe {
ffi::PyEval_RestoreThread(self.tstate);
#[cfg(not(pyo3_disable_reference_pool))]
if let Some(pool) = POOL.get() {
pool.drop_deferred_references(Python::assume_attached());
}
}
}
}
pub(crate) struct ForbidAttaching {
count: isize,
}
impl ForbidAttaching {
const FORBIDDEN_DURING_TRAVERSE: &'static str = "Attaching a thread to the interpreter is prohibited while a __traverse__ implementation is running.";
pub fn during_traverse() -> Self {
Self::new(ATTACH_FORBIDDEN_DURING_TRAVERSE)
}
fn new(reason: isize) -> Self {
let count = ATTACH_COUNT.with(|c| c.replace(reason));
Self { count }
}
#[cold]
fn bail(current: isize) {
match current {
ATTACH_FORBIDDEN_DURING_TRAVERSE => panic!("{}", Self::FORBIDDEN_DURING_TRAVERSE),
_ => panic!("Attaching a thread to the interpreter is currently prohibited."),
}
}
}
impl Drop for ForbidAttaching {
fn drop(&mut self) {
ATTACH_COUNT.with(|c| c.set(self.count));
}
}
#[inline]
pub unsafe fn register_decref(obj: NonNull<ffi::PyObject>) {
#[cfg(not(pyo3_disable_reference_pool))]
{
get_pool().register_decref(obj);
}
#[cfg(all(
pyo3_disable_reference_pool,
not(pyo3_leak_on_drop_without_reference_pool)
))]
{
let _trap = PanicTrap::new("Aborting the process to avoid panic-from-drop.");
panic!("Cannot drop pointer into Python heap without the thread being attached.");
}
}
#[cfg(any(not(Py_LIMITED_API), Py_3_11))]
pub(crate) fn is_in_gc_traversal() -> bool {
ATTACH_COUNT
.try_with(|c| c.get() == ATTACH_FORBIDDEN_DURING_TRAVERSE)
.unwrap_or(false)
}
#[inline(always)]
fn increment_attach_count() {
let _ = ATTACH_COUNT.try_with(|c| {
let current = c.get();
if current < 0 {
ForbidAttaching::bail(current);
}
c.set(current + 1);
});
}
#[inline(always)]
fn decrement_attach_count() {
let _ = ATTACH_COUNT.try_with(|c| {
let current = c.get();
debug_assert!(
current > 0,
"Negative attach count detected. Please report this error to the PyO3 repo as a bug."
);
c.set(current - 1);
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{types::PyAnyMethods, Py, PyAny, Python};
fn get_object(py: Python<'_>) -> Py<PyAny> {
py.eval(c"object()", None, None).unwrap().unbind()
}
#[cfg(not(pyo3_disable_reference_pool))]
fn pool_dec_refs_does_not_contain(obj: &Py<PyAny>) -> bool {
!get_pool()
.pending_decrefs
.lock()
.unwrap()
.contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) })
}
#[cfg(not(any(pyo3_disable_reference_pool, Py_GIL_DISABLED)))]
fn pool_dec_refs_contains(obj: &Py<PyAny>) -> bool {
get_pool()
.pending_decrefs
.lock()
.unwrap()
.contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) })
}
#[test]
fn test_pyobject_drop_attached_decreases_refcnt() {
Python::attach(|py| {
let obj = get_object(py);
let reference = obj.clone_ref(py);
assert_eq!(obj.get_refcnt(py), 2);
#[cfg(not(pyo3_disable_reference_pool))]
assert!(pool_dec_refs_does_not_contain(&obj));
drop(reference);
assert_eq!(obj.get_refcnt(py), 1);
#[cfg(not(any(pyo3_disable_reference_pool)))]
assert!(pool_dec_refs_does_not_contain(&obj));
});
}
#[test]
#[cfg(all(not(pyo3_disable_reference_pool), not(target_arch = "wasm32")))] fn test_pyobject_drop_detached_doesnt_decrease_refcnt() {
let obj = Python::attach(|py| {
let obj = get_object(py);
let reference = obj.clone_ref(py);
assert_eq!(obj.get_refcnt(py), 2);
assert!(pool_dec_refs_does_not_contain(&obj));
std::thread::spawn(move || drop(reference)).join().unwrap();
assert_eq!(obj.get_refcnt(py), 2);
#[cfg(not(Py_GIL_DISABLED))]
assert!(pool_dec_refs_contains(&obj));
obj
});
#[allow(unused)]
Python::attach(|py| {
#[cfg(not(Py_GIL_DISABLED))]
assert_eq!(obj.get_refcnt(py), 1);
assert!(pool_dec_refs_does_not_contain(&obj));
});
}
#[test]
fn test_attach_counts() {
let get_attach_count = || ATTACH_COUNT.with(|c| c.get());
assert_eq!(get_attach_count(), 0);
Python::attach(|_| {
assert_eq!(get_attach_count(), 1);
let pool = unsafe { AttachGuard::assume() };
assert_eq!(get_attach_count(), 2);
let pool2 = unsafe { AttachGuard::assume() };
assert_eq!(get_attach_count(), 3);
drop(pool);
assert_eq!(get_attach_count(), 2);
Python::attach(|_| {
assert_eq!(get_attach_count(), 3);
});
assert_eq!(get_attach_count(), 2);
drop(pool2);
assert_eq!(get_attach_count(), 1);
});
assert_eq!(get_attach_count(), 0);
}
#[test]
fn test_detach() {
assert!(!thread_is_attached());
Python::attach(|py| {
assert!(thread_is_attached());
py.detach(move || {
assert!(!thread_is_attached());
Python::attach(|_| assert!(thread_is_attached()));
assert!(!thread_is_attached());
});
assert!(thread_is_attached());
});
assert!(!thread_is_attached());
}
#[cfg(feature = "py-clone")]
#[test]
#[should_panic]
fn test_detach_updates_refcounts() {
Python::attach(|py| {
let obj = get_object(py);
assert_eq!(obj.get_refcnt(py), 1);
py.detach(|| obj.clone());
});
}
#[test]
fn recursive_attach_ok() {
Python::attach(|py| {
let obj = Python::attach(|_| py.eval(c"object()", None, None).unwrap());
assert_eq!(obj.get_refcnt(), 1);
})
}
#[cfg(feature = "py-clone")]
#[test]
fn test_clone_attached() {
Python::attach(|py| {
let obj = get_object(py);
let count = obj.get_refcnt(py);
#[expect(clippy::redundant_clone)]
let c = obj.clone();
assert_eq!(count + 1, c.get_refcnt(py));
})
}
#[test]
#[cfg(not(pyo3_disable_reference_pool))]
fn test_drop_deferred_references_does_not_deadlock() {
use crate::ffi;
Python::attach(|py| {
let obj = get_object(py);
unsafe extern "C" fn capsule_drop(capsule: *mut ffi::PyObject) {
let pool = unsafe { AttachGuard::assume() };
unsafe {
use crate::Bound;
Bound::from_owned_ptr(
pool.python(),
ffi::PyCapsule_GetPointer(capsule, std::ptr::null()) as _,
)
};
}
let ptr = obj.into_ptr();
let capsule =
unsafe { ffi::PyCapsule_New(ptr as _, std::ptr::null(), Some(capsule_drop)) };
get_pool().register_decref(NonNull::new(capsule).unwrap());
get_pool().drop_deferred_references(py);
})
}
#[test]
#[cfg(not(pyo3_disable_reference_pool))]
fn test_attach_guard_drop_deferred_references() {
Python::attach(|py| {
let obj = get_object(py);
get_pool().register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap());
#[cfg(not(Py_GIL_DISABLED))]
assert!(pool_dec_refs_contains(&obj));
let _guard = AttachGuard::attach();
assert!(pool_dec_refs_does_not_contain(&obj));
get_pool().register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap());
#[cfg(not(Py_GIL_DISABLED))]
assert!(pool_dec_refs_contains(&obj));
let _guard2 = unsafe { AttachGuard::assume() };
assert!(pool_dec_refs_does_not_contain(&obj));
})
}
}