Skip to content

Commit 39bf3b8

Browse files
authored
add Python::try_attach (#5342)
* add `Python::try_attach` * newsfragment * build `attach` in terms of `try_attach` * gate `Finalizing` error on 3.13 * fix msrv
1 parent 488eeda commit 39bf3b8

File tree

4 files changed

+106
-77
lines changed

4 files changed

+106
-77
lines changed

newsfragments/5342.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `Python::try_attach`.

pytests/src/misc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ fn hammer_gil_in_thread() -> LockHolder {
2424
// now the interpreter has shut down, so hammer the GIL. In buggy
2525
// versions of PyO3 this will cause a crash.
2626
loop {
27-
Python::attach(|_py| ());
27+
Python::try_attach(|_py| ());
2828
}
2929
});
3030
LockHolder { sender }

src/internal/state.rs

Lines changed: 87 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{ffi, Python};
77
use std::cell::Cell;
88
#[cfg(not(pyo3_disable_reference_pool))]
99
use std::sync::OnceLock;
10+
#[cfg_attr(pyo3_disable_reference_pool, allow(unused_imports))]
1011
use std::{mem, ptr::NonNull, sync};
1112

1213
std::thread_local! {
@@ -43,113 +44,123 @@ pub(crate) enum AttachGuard {
4344
Ensured { gstate: ffi::PyGILState_STATE },
4445
}
4546

47+
/// Possible error when calling `try_attach()`
48+
pub(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+
4658
impl AttachGuard {
4759
/// PyO3 internal API for attaching to the Python interpreter. The public API is Python::attach.
4860
///
4961
/// If the thread was already attached via PyO3, this returns
5062
/// `AttachGuard::Assumed`. Otherwise, the thread will attach now and
5163
/// `AttachGuard::Ensured` will be returned.
52-
pub(crate) fn acquire() -> Self {
53-
if thread_is_attached() {
54-
// SAFETY: We just checked that the thread is already attached.
55-
return unsafe { Self::assume() };
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+
}
5679
}
57-
58-
crate::interpreter_lifecycle::ensure_initialized();
59-
60-
// Calling `PyGILState_Ensure` while finalizing may crash CPython in unpredictable
61-
// ways, we'll make a best effort attempt here to avoid that. (There's a time of
62-
// check to time-of-use issue, but it's better than nothing.)
63-
assert!(
64-
!is_finalizing(),
65-
"Cannot attach to the Python interpreter while it is finalizing."
66-
);
67-
68-
// SAFETY: We have ensured the Python interpreter is initialized.
69-
unsafe { Self::acquire_unchecked() }
7080
}
7181

72-
/// Variant of the above which will will return `None` if the interpreter cannot be attached to.
73-
#[cfg(any(not(Py_LIMITED_API), Py_3_11, test))] // see Python::try_attach
74-
pub(crate) fn try_acquire() -> Option<Self> {
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> {
7584
match ATTACH_COUNT.try_with(|c| c.get()) {
7685
Ok(i) if i > 0 => {
7786
// SAFETY: We just checked that the thread is already attached.
78-
return Some(unsafe { Self::assume() });
87+
return Ok(unsafe { Self::assume() });
7988
}
8089
// Cannot attach during GC traversal.
81-
Ok(ATTACH_FORBIDDEN_DURING_TRAVERSE) => return None,
90+
Ok(ATTACH_FORBIDDEN_DURING_TRAVERSE) => {
91+
return Err(AttachError::ForbiddenDuringTraverse)
92+
}
8293
// other cases handled below
8394
_ => {}
8495
}
8596

86-
// SAFETY: These APIs are always sound to call
87-
if unsafe { ffi::Py_IsInitialized() } == 0 || is_finalizing() {
97+
// SAFETY: always safe to call this
98+
if unsafe { ffi::Py_IsInitialized() } == 0 {
99+
return Err(AttachError::NotInitialized);
100+
}
101+
102+
// Calling `PyGILState_Ensure` while finalizing may crash CPython in unpredictable
103+
// ways, we'll make a best effort attempt here to avoid that. (There's a time of
104+
// check to time-of-use issue, but it's better than nothing.)
105+
//
106+
// SAFETY: always safe to call this
107+
#[cfg(Py_3_13)]
108+
if unsafe { ffi::Py_IsFinalizing() } != 0 {
88109
// If the interpreter is not initialized, we cannot attach.
89-
return None;
110+
return Err(AttachError::Finalizing);
90111
}
91112

92-
// SAFETY: We have ensured the Python interpreter is initialized.
93-
Some(unsafe { Self::acquire_unchecked() })
113+
// SAFETY: We have done everything reasonable to ensure we're in a safe state to
114+
// attach to the Python interpreter.
115+
Ok(unsafe { Self::do_attach_unchecked() })
94116
}
95117

96118
/// Acquires the `AttachGuard` without performing any state checking.
97119
///
98120
/// This can be called in "unsafe" contexts where the normal interpreter state
99-
/// checking performed by `AttachGuard::acquire` may fail. This includes calling
121+
/// checking performed by `AttachGuard::try_attach` may fail. This includes calling
100122
/// as part of multi-phase interpreter initialization.
101123
///
102124
/// # Safety
103125
///
104126
/// The caller must ensure that the Python interpreter is sufficiently initialized
105127
/// for a thread to be able to attach to it.
106-
pub(crate) unsafe fn acquire_unchecked() -> Self {
128+
pub(crate) unsafe fn attach_unchecked() -> Self {
107129
if thread_is_attached() {
108130
return unsafe { Self::assume() };
109131
}
110132

133+
unsafe { Self::do_attach_unchecked() }
134+
}
135+
136+
/// Attach to the interpreter, without a fast-path to check if the thread is already attached.
137+
#[cold]
138+
unsafe fn do_attach_unchecked() -> Self {
111139
// SAFETY: interpreter is sufficiently initialized to attach a thread.
112140
let gstate = unsafe { ffi::PyGILState_Ensure() };
113141
increment_attach_count();
114-
115-
#[cfg(not(pyo3_disable_reference_pool))]
116-
if let Some(pool) = POOL.get() {
117-
pool.update_counts(unsafe { Python::assume_gil_acquired() });
118-
}
142+
// SAFETY: just attached to the interpreter
143+
drop_deferred_references(unsafe { Python::assume_gil_acquired() });
119144
AttachGuard::Ensured { gstate }
120145
}
121146

122147
/// Acquires the `AttachGuard` while assuming that the thread is already attached
123148
/// to the interpreter.
124149
pub(crate) unsafe fn assume() -> Self {
125150
increment_attach_count();
126-
let guard = AttachGuard::Assumed;
127-
#[cfg(not(pyo3_disable_reference_pool))]
128-
if let Some(pool) = POOL.get() {
129-
pool.update_counts(guard.python());
130-
}
131-
guard
151+
// SAFETY: invariant of calling this function
152+
drop_deferred_references(unsafe { Python::assume_gil_acquired() });
153+
AttachGuard::Assumed
132154
}
133155

134156
/// Gets the Python token associated with this [`AttachGuard`].
135157
#[inline]
136-
pub fn python(&self) -> Python<'_> {
158+
pub(crate) fn python(&self) -> Python<'_> {
159+
// SAFETY: this guard guarantees the thread is attached
137160
unsafe { Python::assume_gil_acquired() }
138161
}
139162
}
140163

141-
fn is_finalizing() -> bool {
142-
// SAFTETY: always safe to call this
143-
#[cfg(Py_3_13)]
144-
unsafe {
145-
ffi::Py_IsFinalizing() != 0
146-
}
147-
148-
// can't reliably check this before 3.13
149-
#[cfg(not(Py_3_13))]
150-
false
151-
}
152-
153164
/// The Drop implementation for `AttachGuard` will decrement the attach count (and potentially detach).
154165
impl Drop for AttachGuard {
155166
fn drop(&mut self) {
@@ -164,7 +175,7 @@ impl Drop for AttachGuard {
164175
}
165176
}
166177

167-
// Vector of PyObject
178+
#[cfg(not(pyo3_disable_reference_pool))]
168179
type PyObjVec = Vec<NonNull<ffi::PyObject>>;
169180

170181
#[cfg(not(pyo3_disable_reference_pool))]
@@ -185,7 +196,7 @@ impl ReferencePool {
185196
self.pending_decrefs.lock().unwrap().push(obj);
186197
}
187198

188-
fn update_counts(&self, _py: Python<'_>) {
199+
fn drop_deferred_references(&self, _py: Python<'_>) {
189200
let mut pending_decrefs = self.pending_decrefs.lock().unwrap();
190201
if pending_decrefs.is_empty() {
191202
return;
@@ -214,6 +225,15 @@ fn get_pool() -> &'static ReferencePool {
214225
POOL.get_or_init(ReferencePool::new)
215226
}
216227

228+
#[cfg_attr(pyo3_disable_reference_pool, inline(always))]
229+
#[cfg_attr(pyo3_disable_reference_pool, allow(unused_variables))]
230+
fn drop_deferred_references(py: Python<'_>) {
231+
#[cfg(not(pyo3_disable_reference_pool))]
232+
if let Some(pool) = POOL.get() {
233+
pool.drop_deferred_references(py);
234+
}
235+
}
236+
217237
/// A guard which can be used to temporarily detach from the interpreter and restore on `Drop`.
218238
pub(crate) struct SuspendAttach {
219239
count: isize,
@@ -238,7 +258,7 @@ impl Drop for SuspendAttach {
238258
// Update counts of `Py<T>` that were dropped while not attached.
239259
#[cfg(not(pyo3_disable_reference_pool))]
240260
if let Some(pool) = POOL.get() {
241-
pool.update_counts(Python::assume_gil_acquired());
261+
pool.drop_deferred_references(Python::assume_gil_acquired());
242262
}
243263
}
244264
}
@@ -250,6 +270,8 @@ pub(crate) struct ForbidAttaching {
250270
}
251271

252272
impl ForbidAttaching {
273+
const FORBIDDEN_DURING_TRAVERSE: &'static str = "Attaching a thread to the interpreter is prohibited while a __traverse__ implementation is running.";
274+
253275
/// Lock access to the interpreter while an implementation of `__traverse__` is running
254276
pub fn during_traverse() -> Self {
255277
Self::new(ATTACH_FORBIDDEN_DURING_TRAVERSE)
@@ -264,9 +286,7 @@ impl ForbidAttaching {
264286
#[cold]
265287
fn bail(current: isize) {
266288
match current {
267-
ATTACH_FORBIDDEN_DURING_TRAVERSE => panic!(
268-
"Attaching a thread to the interpreter is prohibited while a __traverse__ implementation is running."
269-
),
289+
ATTACH_FORBIDDEN_DURING_TRAVERSE => panic!("{}", Self::FORBIDDEN_DURING_TRAVERSE),
270290
_ => panic!("Attaching a thread to the interpreter is currently prohibited."),
271291
}
272292
}
@@ -359,7 +379,6 @@ mod tests {
359379
use super::*;
360380

361381
use crate::{ffi, types::PyAnyMethods, Py, PyAny, Python};
362-
use std::ptr::NonNull;
363382

364383
fn get_object(py: Python<'_>) -> Py<PyAny> {
365384
py.eval(ffi::c_str!("object()"), None, None)
@@ -531,8 +550,8 @@ mod tests {
531550

532551
#[test]
533552
#[cfg(not(pyo3_disable_reference_pool))]
534-
fn test_update_counts_does_not_deadlock() {
535-
// update_counts can run arbitrary Python code during Py_DECREF.
553+
fn test_drop_deferred_references_does_not_deadlock() {
554+
// drop_deferred_references can run arbitrary Python code during Py_DECREF.
536555
// if the locking is implemented incorrectly, it will deadlock.
537556

538557
use crate::ffi;
@@ -541,8 +560,8 @@ mod tests {
541560
let obj = get_object(py);
542561

543562
unsafe extern "C" fn capsule_drop(capsule: *mut ffi::PyObject) {
544-
// This line will implicitly call update_counts
545-
// -> and so cause deadlock if update_counts is not handling recursion correctly.
563+
// This line will implicitly call drop_deferred_references
564+
// -> and so cause deadlock if drop_deferred_references is not handling recursion correctly.
546565
let pool = unsafe { AttachGuard::assume() };
547566

548567
// Rebuild obj so that it can be dropped
@@ -562,22 +581,22 @@ mod tests {
562581
get_pool().register_decref(NonNull::new(capsule).unwrap());
563582

564583
// Updating the counts will call decref on the capsule, which calls capsule_drop
565-
get_pool().update_counts(py);
584+
get_pool().drop_deferred_references(py);
566585
})
567586
}
568587

569588
#[test]
570589
#[cfg(not(pyo3_disable_reference_pool))]
571-
fn test_attach_guard_update_counts() {
590+
fn test_attach_guard_drop_deferred_references() {
572591
Python::attach(|py| {
573592
let obj = get_object(py);
574593

575-
// For AttachGuard::acquire
594+
// For AttachGuard::attach
576595

577596
get_pool().register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap());
578597
#[cfg(not(Py_GIL_DISABLED))]
579598
assert!(pool_dec_refs_contains(&obj));
580-
let _guard = AttachGuard::acquire();
599+
let _guard = AttachGuard::attach();
581600
assert!(pool_dec_refs_does_not_contain(&obj));
582601

583602
// For AttachGuard::assume

src/marker.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,10 @@ impl Python<'_> {
393393
/// - If the [`auto-initialize`] feature is not enabled and the Python interpreter is not
394394
/// initialized.
395395
/// - If the Python interpreter is in the process of [shutting down].
396+
/// - If the middle of GC traversal.
397+
///
398+
/// To avoid possible initialization or panics if calling in a context where the Python
399+
/// interpreter might be unavailable, consider using [`Python::try_attach`].
396400
///
397401
/// # Examples
398402
///
@@ -417,24 +421,29 @@ impl Python<'_> {
417421
where
418422
F: for<'py> FnOnce(Python<'py>) -> R,
419423
{
420-
let guard = AttachGuard::acquire();
424+
let guard = AttachGuard::attach();
421425
f(guard.python())
422426
}
423427

424-
/// Variant of [`Python::attach`] which will do no work if the interpreter is in a
425-
/// state where it cannot be attached to:
428+
/// Variant of [`Python::attach`] which will return without attaching to the Python
429+
/// interpreter if the interpreter is in a state where it cannot be attached to:
426430
/// - in the middle of GC traversal
427431
/// - in the process of shutting down
428432
/// - not initialized
433+
///
434+
/// Note that due to the nature of the underlying Python APIs used to implement this,
435+
/// the behavior is currently provided on a best-effort basis; it is expected that a
436+
/// future CPython version will introduce APIs which guarantee this behaviour. This
437+
/// function is still recommended for use in the meanwhile as it provides the best
438+
/// possible behaviour and should transparently change to an optimal implementation
439+
/// once such APIs are available.
429440
#[inline]
430441
#[track_caller]
431-
#[cfg(any(not(Py_LIMITED_API), Py_3_11, test))] // only used in buffer.rs for now, allow in test cfg for simplicity
432-
// TODO: make this API public?
433-
pub(crate) fn try_attach<F, R>(f: F) -> Option<R>
442+
pub fn try_attach<F, R>(f: F) -> Option<R>
434443
where
435444
F: for<'py> FnOnce(Python<'py>) -> R,
436445
{
437-
let guard = AttachGuard::try_acquire()?;
446+
let guard = AttachGuard::try_attach().ok()?;
438447
Some(f(guard.python()))
439448
}
440449

@@ -494,7 +503,7 @@ impl Python<'_> {
494503
where
495504
F: for<'py> FnOnce(Python<'py>) -> R,
496505
{
497-
let guard = unsafe { AttachGuard::acquire_unchecked() };
506+
let guard = unsafe { AttachGuard::attach_unchecked() };
498507

499508
f(guard.python())
500509
}

0 commit comments

Comments
 (0)