1#![deny(clippy::undocumented_unsafe_blocks)]
2
3use crate::{ffi, PyAny};
11
12#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
13use crate::{
14 err::{error_on_minusone, error_on_minusone_with_result},
15 Borrowed, PyResult, Python,
16};
17#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
18use core::ffi::c_int;
19
20#[repr(transparent)]
27pub struct PyContext(PyAny);
28
29pyobject_native_type_core!(
30 PyContext,
31 pyobject_native_static_type_object!(ffi::PyContext_Type),
32 "contextvars",
33 "Context",
34 #module=Some("contextvars"),
35 #checkfunction=ffi::PyContext_CheckExact
36);
37
38#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
41impl PyContext {
42 #[doc(alias = "PyContext_AddWatcher")]
52 pub fn add_watcher(
53 py: Python<'_>,
54 callback: WatchCallback,
55 ) -> PyResult<BoundContextWatcherGuard<'_>> {
56 let watcher_id =
60 error_on_minusone_with_result(py, unsafe { ffi::PyContext_AddWatcher(callback.0) })?;
61
62 Ok(BoundContextWatcherGuard {
63 watcher_id,
64 py,
65 active: true,
66 })
67 }
68}
69
70#[doc(alias = "PyContextEvent")]
74#[derive(Debug)]
75#[non_exhaustive]
76#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
77pub enum ContextEvent<'a, 'py> {
78 Switched(Option<Borrowed<'a, 'py, PyContext>>),
82
83 Unknown {
85 raw_event: ffi::PyContextEvent,
87
88 object: Option<Borrowed<'a, 'py, PyAny>>,
90 },
91}
92
93#[must_use = "dropping the guard immediately unregisters the context watcher"]
105#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
106pub struct BoundContextWatcherGuard<'py> {
107 watcher_id: c_int,
108 py: Python<'py>,
109 active: bool,
110}
111
112#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
113impl BoundContextWatcherGuard<'_> {
114 #[doc(alias = "PyContext_ClearWatcher")]
118 pub fn clear(mut self) -> PyResult<()> {
119 self.active = false;
120 clear_watcher(self.py, self.watcher_id)
121 }
122
123 pub fn unbind(mut self) -> ContextWatcherGuard {
130 self.active = false;
131 ContextWatcherGuard {
132 watcher_id: self.watcher_id,
133 active: true,
134 }
135 }
136}
137
138#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
139impl Drop for BoundContextWatcherGuard<'_> {
140 fn drop(&mut self) {
141 if !self.active {
142 return;
143 }
144
145 self.active = false;
146 clear_watcher_on_drop(self.py, self.watcher_id);
147 }
148}
149
150#[must_use = "dropping the guard unregisters the context watcher"]
186#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
187pub struct ContextWatcherGuard {
188 watcher_id: c_int,
189 active: bool,
190}
191
192#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
193impl ContextWatcherGuard {
194 #[doc(alias = "PyContext_ClearWatcher")]
198 pub fn clear(mut self, py: Python<'_>) -> PyResult<()> {
199 self.active = false;
200 clear_watcher(py, self.watcher_id)
201 }
202
203 pub fn into_bound<'py>(mut self, py: Python<'py>) -> BoundContextWatcherGuard<'py> {
208 self.active = false;
209 BoundContextWatcherGuard {
210 watcher_id: self.watcher_id,
211 py,
212 active: true,
213 }
214 }
215}
216
217#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
218impl Drop for ContextWatcherGuard {
219 fn drop(&mut self) {
220 if !self.active {
221 return;
222 }
223
224 self.active = false;
225 let watcher_id = self.watcher_id;
226 let _ = Python::try_attach(|py| clear_watcher_on_drop(py, watcher_id));
227 }
228}
229
230#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
231fn clear_watcher(py: Python<'_>, watcher_id: c_int) -> PyResult<()> {
232 error_on_minusone(py, unsafe { ffi::PyContext_ClearWatcher(watcher_id) })
238}
239
240#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
241fn clear_watcher_on_drop(_py: Python<'_>, watcher_id: c_int) {
242 unsafe {
254 let pending_exception = ffi::PyErr_GetRaisedException();
255 if ffi::PyContext_ClearWatcher(watcher_id) == -1 {
256 ffi::PyErr_WriteUnraisable(core::ptr::null_mut());
257 }
258
259 if !pending_exception.is_null() {
260 ffi::PyErr_Clear();
262 ffi::PyErr_SetRaisedException(pending_exception);
263 }
264 }
265}
266
267#[repr(transparent)]
271#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
272pub struct WatchCallback(ffi::PyContext_WatchCallback);
273
274#[macro_export]
303#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
304macro_rules! watch_callback {
305 ($callback:path) => {{
306 struct Callback;
307
308 impl $crate::types::context::impl_::ContextWatcherCallbackDef for Callback {
309 const CALLBACK: $crate::types::context::impl_::ContextWatcherCallback = $callback;
310 }
311
312 $crate::types::context::impl_::new_watch_callback::<Callback>()
313 }};
314}
315
316#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
317pub use crate::watch_callback;
318
319#[doc(hidden)]
321#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
322pub mod impl_ {
323 use crate::{ffi_ptr_ext::FfiPtrExt, types::PyAnyMethods};
324
325 use super::*;
326
327 pub type ContextWatcherCallback =
329 for<'a, 'py> fn(Python<'py>, ContextEvent<'a, 'py>) -> PyResult<()>;
330
331 pub trait ContextWatcherCallbackDef {
333 const CALLBACK: ContextWatcherCallback;
335 }
336
337 pub fn new_watch_callback<Callback: ContextWatcherCallbackDef>() -> WatchCallback {
338 WatchCallback(context_watcher::<Callback>)
339 }
340
341 unsafe fn event_from_raw<'a, 'py>(
342 py: Python<'py>,
343 event: ffi::PyContextEvent,
344 object: *mut ffi::PyObject,
345 ) -> PyResult<ContextEvent<'a, 'py>> {
346 match event {
347 ffi::Py_CONTEXT_SWITCHED => {
348 let object = unsafe { object.assume_borrowed_unchecked(py) };
350
351 if object.is_none() {
352 Ok(ContextEvent::Switched(None))
353 } else {
354 Ok(ContextEvent::Switched(Some(object.cast()?)))
355 }
356 }
357
358 raw_event => {
359 let object = unsafe { object.assume_borrowed_or_opt(py) };
361 Ok(ContextEvent::Unknown { raw_event, object })
362 }
363 }
364 }
365
366 pub unsafe extern "C" fn context_watcher<Callback: ContextWatcherCallbackDef>(
373 event: ffi::PyContextEvent,
374 object: *mut ffi::PyObject,
375 ) -> c_int {
376 let pending_exception = unsafe { ffi::PyErr_GetRaisedException() };
383
384 let result = unsafe {
390 crate::impl_::trampoline::trampoline(|py| {
391 let event = event_from_raw(py, event, object)?;
392
393 (Callback::CALLBACK)(py, event)?;
394
395 if crate::PyErr::occurred(py) {
396 return Err(crate::PyErr::fetch(py));
397 }
398
399 Ok(0)
400 })
401 };
402
403 if pending_exception.is_null() {
404 return result;
405 }
406
407 unsafe {
417 if result == -1 {
418 ffi::PyErr_WriteUnraisable(object);
419 }
420
421 ffi::PyErr_Clear();
423 ffi::PyErr_SetRaisedException(pending_exception);
424 }
425
426 0
427 }
428}
429
430#[cfg(all(test, Py_3_14, not(Py_GIL_DISABLED)))]
431mod watcher_tests {
432 use super::impl_::{context_watcher, ContextWatcherCallback, ContextWatcherCallbackDef};
433 use super::{ContextEvent, PyContext};
434 use crate::exceptions::{PyRuntimeError, PyValueError};
435 use crate::platform::sync::non_poison::{Mutex, MutexGuard};
436 #[cfg(feature = "macros")]
437 use crate::test_utils::UnraisableCapture;
438 use crate::types::PyAnyMethods;
439 use crate::{ffi, PyErr, PyResult, Python};
440 use alloc::string::ToString;
441 use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
442 use static_assertions::{assert_impl_all, assert_not_impl_any};
443
444 static WATCHER_TEST_MUTEX: Mutex<()> = Mutex::new(());
447 static SWITCH_COUNT: AtomicUsize = AtomicUsize::new(0);
448 static SAW_CONTEXT: AtomicBool = AtomicBool::new(false);
449
450 fn acquire_watcher_test_lock() -> MutexGuard<'static, ()> {
451 WATCHER_TEST_MUTEX.lock()
452 }
453
454 fn run_context_switch(py: Python<'_>) {
455 py.run(
456 c"import contextvars; contextvars.Context().run(lambda: None)",
457 None,
458 None,
459 )
460 .unwrap();
461 }
462
463 fn assert_no_context_switches(py: Python<'_>, count_before: usize) {
464 run_context_switch(py);
465 assert_eq!(SWITCH_COUNT.load(Ordering::Relaxed), count_before);
466 }
467
468 #[allow(clippy::unnecessary_wraps, reason = "context watcher callback")]
469 fn record_switch(_py: Python<'_>, event: ContextEvent<'_, '_>) -> PyResult<()> {
470 if let ContextEvent::Switched(context) = event {
471 SWITCH_COUNT.fetch_add(1, Ordering::Relaxed);
472 if let Some(context) = context {
473 assert!(context.is_exact_instance_of::<PyContext>());
474 SAW_CONTEXT.store(true, Ordering::Relaxed);
475 }
476 }
477 Ok(())
478 }
479
480 #[test]
481 fn watcher_is_cleared_on_drop() {
482 let _guard = acquire_watcher_test_lock();
483 Python::attach(|py| {
484 SWITCH_COUNT.store(0, Ordering::Relaxed);
485 SAW_CONTEXT.store(false, Ordering::Relaxed);
486
487 let watcher = PyContext::add_watcher(py, watch_callback!(record_switch)).unwrap();
488 run_context_switch(py);
489
490 let count_after_first_run = SWITCH_COUNT.load(Ordering::Relaxed);
491 assert!(count_after_first_run >= 2);
492 assert!(SAW_CONTEXT.load(Ordering::Relaxed));
493
494 drop(watcher);
495
496 assert_no_context_switches(py, count_after_first_run);
497 });
498 }
499
500 #[test]
501 fn watcher_can_be_cleared_explicitly() {
502 let _guard = acquire_watcher_test_lock();
503 Python::attach(|py| {
504 SWITCH_COUNT.store(0, Ordering::Relaxed);
505
506 let watcher = PyContext::add_watcher(py, watch_callback!(record_switch)).unwrap();
507 watcher.clear().unwrap();
508
509 assert_no_context_switches(py, 0);
510 });
511 }
512
513 #[test]
514 fn multiple_watchers_can_register_the_same_callback() {
515 let _guard = acquire_watcher_test_lock();
516 Python::attach(|py| {
517 SWITCH_COUNT.store(0, Ordering::Relaxed);
518
519 let first = PyContext::add_watcher(py, watch_callback!(record_switch)).unwrap();
520 let second = PyContext::add_watcher(py, watch_callback!(record_switch)).unwrap();
521
522 run_context_switch(py);
523 assert!(SWITCH_COUNT.load(Ordering::Relaxed) >= 4);
524
525 drop(first);
526 let count_with_both = SWITCH_COUNT.load(Ordering::Relaxed);
527 run_context_switch(py);
528 assert!(SWITCH_COUNT.load(Ordering::Relaxed) >= count_with_both + 2);
529
530 drop(second);
531 let count_after_drop = SWITCH_COUNT.load(Ordering::Relaxed);
532 assert_no_context_switches(py, count_after_drop);
533 });
534 }
535
536 #[test]
537 fn dropping_watcher_preserves_a_pending_exception() {
538 let _guard = acquire_watcher_test_lock();
539 Python::attach(|py| {
540 let watcher = PyContext::add_watcher(py, watch_callback!(record_switch)).unwrap();
541 PyValueError::new_err("original error").restore(py);
542
543 drop(watcher);
544
545 let error = PyErr::fetch(py);
546 assert!(error.is_instance_of::<PyValueError>(py));
547 });
548 }
549
550 fn fail_callback(_py: Python<'_>, _event: ContextEvent<'_, '_>) -> PyResult<()> {
551 Err(PyRuntimeError::new_err("watcher failed"))
552 }
553
554 struct FailingCallback;
555
556 impl ContextWatcherCallbackDef for FailingCallback {
557 const CALLBACK: ContextWatcherCallback = fail_callback;
558 }
559
560 #[test]
561 fn callback_error_is_returned_without_a_pending_exception() {
562 Python::attach(|py| {
563 let result = unsafe {
565 context_watcher::<FailingCallback>(ffi::Py_CONTEXT_SWITCHED, ffi::Py_None())
566 };
567
568 assert_eq!(result, -1);
569 let error = PyErr::fetch(py);
570 assert!(error.is_instance_of::<PyRuntimeError>(py));
571 });
572 }
573
574 #[allow(clippy::unnecessary_wraps, reason = "context watcher callback")]
575 fn restore_error_callback(py: Python<'_>, _event: ContextEvent<'_, '_>) -> PyResult<()> {
576 PyRuntimeError::new_err("watcher restored error").restore(py);
577 Ok(())
578 }
579
580 struct RestoringErrorCallback;
581
582 impl ContextWatcherCallbackDef for RestoringErrorCallback {
583 const CALLBACK: ContextWatcherCallback = restore_error_callback;
584 }
585
586 #[test]
587 fn callback_cannot_return_success_with_an_exception_set() {
588 Python::attach(|py| {
589 let result = unsafe {
591 context_watcher::<RestoringErrorCallback>(ffi::Py_CONTEXT_SWITCHED, ffi::Py_None())
592 };
593
594 assert_eq!(result, -1);
595 let error = PyErr::fetch(py);
596 assert!(error.is_instance_of::<PyRuntimeError>(py));
597 assert_eq!(error.to_string(), "RuntimeError: watcher restored error");
598 });
599 }
600
601 #[test]
602 #[cfg(feature = "macros")]
603 fn callback_error_preserves_a_pending_exception() {
604 Python::attach(|py| {
605 UnraisableCapture::enter(py, |capture| {
606 PyValueError::new_err("original error").restore(py);
607
608 let result = unsafe {
610 context_watcher::<FailingCallback>(ffi::Py_CONTEXT_SWITCHED, ffi::Py_None())
611 };
612
613 assert_eq!(result, 0);
614
615 let original_error = PyErr::fetch(py);
616 assert!(original_error.is_instance_of::<PyValueError>(py));
617 assert_eq!(original_error.to_string(), "ValueError: original error");
618
619 let (watcher_error, object) =
620 capture.take_capture().expect("missing unraisable error");
621 assert!(watcher_error.is_instance_of::<PyRuntimeError>(py));
622 assert!(object.is_none());
623 });
624 });
625 }
626
627 #[test]
628 #[cfg(feature = "macros")]
629 fn registered_callback_errors_are_unraisable() {
630 let _guard = acquire_watcher_test_lock();
631 Python::attach(|py| {
632 UnraisableCapture::enter(py, |capture| {
633 let watcher = PyContext::add_watcher(py, watch_callback!(fail_callback)).unwrap();
634
635 run_context_switch(py);
636
637 let (watcher_error, _) = capture.take_capture().expect("missing unraisable error");
638 assert!(watcher_error.is_instance_of::<PyRuntimeError>(py));
639
640 drop(watcher);
641 });
642 });
643 }
644
645 #[cfg(all(wip_feature_std, panic = "unwind"))]
646 fn panic_callback(_py: Python<'_>, _event: ContextEvent<'_, '_>) -> PyResult<()> {
647 panic!("context watcher panic")
648 }
649
650 #[cfg(all(wip_feature_std, panic = "unwind"))]
651 struct PanickingCallback;
652
653 #[cfg(all(wip_feature_std, panic = "unwind"))]
654 impl ContextWatcherCallbackDef for PanickingCallback {
655 const CALLBACK: ContextWatcherCallback = panic_callback;
656 }
657
658 #[cfg(all(wip_feature_std, panic = "unwind"))]
659 #[test]
660 fn callback_panic_does_not_cross_ffi_boundary() {
661 Python::attach(|py| {
662 let result = unsafe {
664 context_watcher::<PanickingCallback>(ffi::Py_CONTEXT_SWITCHED, ffi::Py_None())
665 };
666
667 assert_eq!(result, -1);
668 assert!(PyErr::occurred(py));
669
670 unsafe { ffi::PyErr_Clear() };
672 });
673 }
674
675 static UNKNOWN_EVENT: AtomicU32 = AtomicU32::new(0);
676
677 #[allow(clippy::unnecessary_wraps, reason = "context watcher callback")]
678 fn record_unknown(_py: Python<'_>, event: ContextEvent<'_, '_>) -> PyResult<()> {
679 if let ContextEvent::Unknown { raw_event, object } = event {
680 UNKNOWN_EVENT.store(raw_event, Ordering::Relaxed);
681 assert!(object.is_none());
682 }
683 Ok(())
684 }
685
686 struct UnknownCallback;
687
688 impl ContextWatcherCallbackDef for UnknownCallback {
689 const CALLBACK: ContextWatcherCallback = record_unknown;
690 }
691
692 #[test]
693 fn unknown_events_are_forwarded() {
694 const FUTURE_EVENT: ffi::PyContextEvent = 123;
695
696 Python::attach(|_py| {
697 UNKNOWN_EVENT.store(0, Ordering::Relaxed);
698
699 let result =
701 unsafe { context_watcher::<UnknownCallback>(FUTURE_EVENT, core::ptr::null_mut()) };
702
703 assert_eq!(result, 0);
704 assert_eq!(UNKNOWN_EVENT.load(Ordering::Relaxed), FUTURE_EVENT);
705 });
706 }
707
708 #[test]
709 fn context_watcher_guard_traits() {
710 assert_not_impl_any!(super::BoundContextWatcherGuard<'_>: Send, Sync);
711 assert_impl_all!(super::ContextWatcherGuard: Send, Sync);
712 }
713
714 #[cfg(not(target_arch = "wasm32"))] #[test]
716 fn unbound_watcher_attaches_on_drop_from_another_thread() {
717 let _guard = acquire_watcher_test_lock();
718 SWITCH_COUNT.store(0, Ordering::Relaxed);
719 let watcher = Python::attach(|py| {
720 PyContext::add_watcher(py, watch_callback!(record_switch))
721 .unwrap()
722 .unbind()
723 });
724
725 Python::attach(run_context_switch);
726 assert!(SWITCH_COUNT.load(Ordering::Relaxed) >= 2);
727
728 std::thread::spawn(move || drop(watcher)).join().unwrap();
729 let count_after_drop = SWITCH_COUNT.load(Ordering::Relaxed);
730
731 Python::attach(|py| assert_no_context_switches(py, count_after_drop));
732 }
733
734 #[test]
735 fn unbound_watcher_can_be_cleared_with_an_attachment() {
736 let _guard = acquire_watcher_test_lock();
737 SWITCH_COUNT.store(0, Ordering::Relaxed);
738 let watcher = Python::attach(|py| {
739 PyContext::add_watcher(py, watch_callback!(record_switch))
740 .unwrap()
741 .unbind()
742 });
743
744 let count_after_clear = Python::attach(|py| {
745 watcher.clear(py).unwrap();
746 SWITCH_COUNT.load(Ordering::Relaxed)
747 });
748
749 Python::attach(|py| assert_no_context_switches(py, count_after_clear));
750 }
751
752 #[test]
753 fn unbound_watcher_can_be_rebound() {
754 let _guard = acquire_watcher_test_lock();
755 SWITCH_COUNT.store(0, Ordering::Relaxed);
756 let watcher = Python::attach(|py| {
757 PyContext::add_watcher(py, watch_callback!(record_switch))
758 .unwrap()
759 .unbind()
760 });
761
762 Python::attach(|py| {
763 let watcher = watcher.into_bound(py);
764 run_context_switch(py);
765
766 let count_before_drop = SWITCH_COUNT.load(Ordering::Relaxed);
767 assert!(count_before_drop >= 2);
768 drop(watcher);
769
770 assert_no_context_switches(py, count_before_drop);
771 });
772 }
773}
774
775#[cfg(test)]
776mod tests {
777 use super::PyContext;
778 use crate::types::PyAnyMethods;
779 use crate::Python;
780
781 #[test]
782 fn context_type() {
783 Python::attach(|py| {
784 let context = py
785 .import(c"contextvars")
786 .unwrap()
787 .getattr(c"Context")
788 .unwrap()
789 .call0()
790 .unwrap();
791
792 assert!(context.is_exact_instance_of::<PyContext>());
793 context.cast::<PyContext>().unwrap();
794 });
795 }
796}