@@ -7,6 +7,7 @@ use crate::{ffi, Python};
77use std:: cell:: Cell ;
88#[ cfg( not( pyo3_disable_reference_pool) ) ]
99use std:: sync:: OnceLock ;
10+ #[ cfg_attr( pyo3_disable_reference_pool, allow( unused_imports) ) ]
1011use std:: { mem, ptr:: NonNull , sync} ;
1112
1213std:: 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+
4658impl 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).
154165impl 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 ) ) ]
168179type 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`.
218238pub ( 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
252272impl 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
0 commit comments