Skip to main content

ferroui_system/threading/
synchronization_context.rs

1//! `System.Threading.SynchronizationContext`: where work posted from any thread runs, and the
2//! current context of a thread (docs/porting/reactive.md, sections 5.1 and 5.8).
3//!
4//! A context is shared between threads (`Arc<dyn SynchronizationContext>`), as in .NET, and its
5//! work comes in two forms: `Send` work that any thread posts ([`post`](SynchronizationContext::post),
6//! [`send`](SynchronizationContext::send)), and work that is not `Send`, posted from the thread of
7//! the context itself ([`post_local`](SynchronizationContext::post_local),
8//! [`send_local`](SynchronizationContext::send_local)). The second form is what the single-threaded
9//! code of the workspace (the observables, the tasks of [`tasks`](super::tasks)) uses: it defers
10//! its work to the context of its own thread.
11//!
12//! The current context of a thread ([`current_synchronization_context`],
13//! `SynchronizationContext.Current`) is set with [`set_synchronization_context`]
14//! (`SynchronizationContext.SetSynchronizationContext`). Setting it also makes it the thread poster
15//! of the thread ([`tasks::set_thread_poster`](super::tasks::set_thread_poster)): a wake of a task
16//! of the thread, or a cancellation registered there with
17//! [`CancellationToken::register_local`](super::CancellationToken::register_local), that comes from
18//! another thread is posted to the context. The context of a thread is therefore expected to run
19//! the work it is posted on that thread.
20
21use super::tasks::{set_thread_poster, ThreadPoster};
22use std::cell::RefCell;
23use std::sync::Arc;
24
25/// Where posted work runs (`System.Threading.SynchronizationContext`).
26///
27/// The base class of .NET posts to the thread pool and sends on the calling thread; here every
28/// implementation says where its work runs. An implementation is shared between threads, as the
29/// contexts of .NET are.
30pub trait SynchronizationContext: Send + Sync {
31    /// Queues `work` to run on the context, from any thread, without waiting for it (`Post`).
32    fn post(&self, work: Box<dyn FnOnce() + Send>);
33
34    /// Runs `work` on the context, from any thread, and waits for it (`Send`).
35    fn send(&self, work: Box<dyn FnOnce() + Send>);
36
37    /// Queues `work`, which is not `Send`, to run on the context (`Post` from the thread of the
38    /// context). Implementations may panic when it is called from another thread.
39    fn post_local(&self, work: Box<dyn FnOnce()>);
40
41    /// Runs `work`, which is not `Send`, on the context and waits for it (`Send` from the thread of
42    /// the context). Implementations may panic when it is called from another thread.
43    fn send_local(&self, work: Box<dyn FnOnce()>);
44
45    /// Tells the context that an operation has started (`OperationStarted`); nothing by default.
46    fn operation_started(&self) {}
47
48    /// Tells the context that an operation has completed (`OperationCompleted`); nothing by
49    /// default.
50    fn operation_completed(&self) {}
51}
52
53crate::ferro_thread_local! {
54    /// The current context of the thread (`SynchronizationContext.Current`).
55    static CURRENT: RefCell<Option<Arc<dyn SynchronizationContext>>> = const { RefCell::new(None) };
56}
57
58/// The current context of the calling thread (`SynchronizationContext.Current`).
59pub fn current_synchronization_context() -> Option<Arc<dyn SynchronizationContext>> {
60    CURRENT.try_with(|current| current.borrow().clone()).ok().flatten()
61}
62
63/// Sets the current context of the calling thread (`SynchronizationContext.SetSynchronizationContext`),
64/// and makes it the thread poster of the thread, through which other threads wake its tasks and
65/// run its cancellations; `None` clears both.
66pub fn set_synchronization_context(context: Option<Arc<dyn SynchronizationContext>>) {
67    let poster: Option<ThreadPoster> = context.as_ref().map(|context| {
68        let context = context.clone();
69        let poster: ThreadPoster = Arc::new(move |work: Box<dyn FnOnce() + Send>| context.post(work));
70        poster
71    });
72    // The previous context is dropped outside the borrow, in case its drop reads the current one.
73    let previous = CURRENT.try_with(|current| current.replace(context)).ok().flatten();
74    drop(previous);
75    set_thread_poster(poster);
76}
77
78impl dyn SynchronizationContext {
79    /// The current context of the calling thread (`SynchronizationContext.Current`); the same as
80    /// [`current_synchronization_context`].
81    pub fn current() -> Option<Arc<dyn SynchronizationContext>> {
82        current_synchronization_context()
83    }
84
85    /// Sets the current context of the calling thread
86    /// (`SynchronizationContext.SetSynchronizationContext`); the same as
87    /// [`set_synchronization_context`].
88    pub fn set_synchronization_context(context: Option<Arc<dyn SynchronizationContext>>) {
89        set_synchronization_context(context);
90    }
91
92    /// Whether `context` is the current context of the calling thread
93    /// (`context == SynchronizationContext.Current`, a comparison of references).
94    pub fn is_current(context: &Arc<dyn SynchronizationContext>) -> bool {
95        current_synchronization_context().is_some_and(|current| std::ptr::addr_eq(Arc::as_ptr(&current), Arc::as_ptr(context)))
96    }
97}