Skip to main content

waterui_core/foundation/
main_thread.rs

1//! [`MainThreadBound`]: a main-thread-confinement wrapper.
2//!
3//! The wrapper makes a `!Send`/`!Sync` value satisfy `Send + Sync` at the type
4//! level while asserting at runtime that the value is only ever accessed on the
5//! thread that created it (the main / UI thread).
6//!
7//! Use it for state that is `!Send` because it belongs to the main / UI thread —
8//! a platform widget handle, a GPU view, a renderer's device context — but has to
9//! sit inside a type that some `Send + Sync` bound forces it into. The runtime
10//! check never fires in correct operation; it is a fail-fast net for state that
11//! escaped to a worker, not a hot-path guard.
12//!
13//! Layout measurement does **not** need it: [`SubView`](crate::layout::SubView) is
14//! single-threaded by contract, so a measurement cache is a plain `RefCell`.
15
16use core::fmt;
17use core::mem::ManuallyDrop;
18use core::ops::Deref;
19
20/// Wraps a `!Send`/`!Sync` value so it satisfies `Send + Sync`, while enforcing at
21/// runtime that it is only accessed on the thread that constructed it.
22///
23/// # Safety model
24///
25/// The [`Send`] and [`Sync`] impls are `unsafe`: they are sound only because every
26/// access goes through [`Deref`] / [`DerefMut`](core::ops::DerefMut) /
27/// [`into_inner`](Self::into_inner), each of which asserts the caller is on the
28/// owning thread and panics otherwise, so the inner `!Send` value never actually
29/// crosses a thread boundary. If the wrapper is dropped on a non-owning thread the
30/// inner value is **leaked** rather than dropped, since running a `!Send`
31/// destructor off-thread would be undefined behavior.
32///
33/// The owner-thread check requires the `std` feature, which is on by default and
34/// must stay on for any build that can spawn a second thread. Disabling default
35/// features is reserved for genuinely single-threaded `no_std` builds, where there
36/// is no other thread to violate the confinement and the checks compile to no-ops;
37/// opting out on a threaded target silently removes the soundness net.
38pub struct MainThreadBound<T> {
39    #[cfg(feature = "std")]
40    owner: std::thread::ThreadId,
41    value: ManuallyDrop<T>,
42}
43
44#[allow(
45    clippy::non_send_fields_in_send_ty,
46    reason = "`MainThreadBound` deliberately carries non-`Send` data; the `Send` impl is sound because access is confined to the owning thread by `assert_owner`"
47)]
48// SAFETY: the inner value is only ever accessed on the owning thread (enforced at
49// runtime by `assert_owner`), and is leaked rather than dropped off-thread, so it
50// never crosses a thread boundary in practice.
51unsafe impl<T> Send for MainThreadBound<T> {}
52// SAFETY: see the `Send` impl — shared access is likewise confined to the owning
53// thread by `assert_owner`.
54unsafe impl<T> Sync for MainThreadBound<T> {}
55
56impl<T> MainThreadBound<T> {
57    /// Bind `value` to the current (main) thread.
58    ///
59    /// Recording the owning thread needs `std`; without it there is no thread to
60    /// record and the binding is a plain wrapper, so that build gets a `const`
61    /// constructor.
62    #[cfg(feature = "std")]
63    #[must_use]
64    pub fn new(value: T) -> Self {
65        Self {
66            owner: std::thread::current().id(),
67            value: ManuallyDrop::new(value),
68        }
69    }
70
71    /// Bind `value` to the current (main) thread.
72    #[cfg(not(feature = "std"))]
73    #[must_use]
74    pub const fn new(value: T) -> Self {
75        Self {
76            value: ManuallyDrop::new(value),
77        }
78    }
79
80    #[cfg(feature = "std")]
81    #[inline]
82    fn is_owner_thread(&self) -> bool {
83        std::thread::current().id() == self.owner
84    }
85
86    #[cfg(not(feature = "std"))]
87    #[inline]
88    #[allow(clippy::unused_self)]
89    const fn is_owner_thread(&self) -> bool {
90        // Single-threaded (no_std/embedded) targets have no other thread to violate
91        // the confinement, so the owner check is unconditionally satisfied.
92        true
93    }
94
95    #[inline]
96    fn assert_owner(&self) {
97        assert!(
98            self.is_owner_thread(),
99            "MainThreadBound accessed off the main thread: a value confined to \
100             the main / UI thread escaped to a worker."
101        );
102    }
103
104    /// Consume the wrapper and return the inner value, asserting the caller is on
105    /// the owning thread.
106    ///
107    /// # Panics
108    ///
109    /// Panics if called from a thread other than the one that constructed the value.
110    #[inline]
111    #[must_use]
112    pub fn into_inner(self) -> T {
113        self.assert_owner();
114        let mut me = ManuallyDrop::new(self);
115        // SAFETY: `me`'s Drop is suppressed by ManuallyDrop and `me` is never used
116        // again, so taking the inner value exactly once is sound.
117        unsafe { ManuallyDrop::take(&mut me.value) }
118    }
119}
120
121// Access is exposed only through `Deref`/`DerefMut` (not inherent `get`/`get_mut`)
122// so the wrapped type's own methods — `Signal::get`, `RefCell::get`, etc. — remain
123// reachable transparently via auto-deref. Both directions assert main-thread access.
124impl<T> Deref for MainThreadBound<T> {
125    type Target = T;
126
127    #[inline]
128    fn deref(&self) -> &T {
129        self.assert_owner();
130        &self.value
131    }
132}
133
134impl<T> core::ops::DerefMut for MainThreadBound<T> {
135    #[inline]
136    fn deref_mut(&mut self) -> &mut T {
137        self.assert_owner();
138        &mut self.value
139    }
140}
141
142impl<T> Drop for MainThreadBound<T> {
143    fn drop(&mut self) {
144        if self.is_owner_thread() {
145            // SAFETY: on the owning thread the inner value has not been taken and is
146            // safe to drop here; `value` is not used again after this.
147            unsafe { ManuallyDrop::drop(&mut self.value) }
148        } else {
149            // Dropping a `!Send` value off-thread would run its destructor on the
150            // wrong thread (undefined behavior). Leak it instead to stay sound, and
151            // flag the misuse loudly in debug builds.
152            debug_assert!(
153                false,
154                "MainThreadBound dropped off the main thread; leaking the inner value \
155                 to stay sound"
156            );
157        }
158    }
159}
160
161impl<T> fmt::Debug for MainThreadBound<T> {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        // Do not access the value here: keep `Debug` usable from any thread for
164        // diagnostics, and avoid requiring `T: Debug` on wrapping structs.
165        f.debug_struct("MainThreadBound").finish_non_exhaustive()
166    }
167}
168
169#[cfg(all(test, feature = "std"))]
170mod tests {
171    use super::MainThreadBound;
172    use alloc::rc::Rc;
173
174    #[test]
175    fn access_on_owner_thread_succeeds() {
176        let bound = MainThreadBound::new(Rc::new(7u32));
177        assert_eq!(**bound, 7);
178        assert_eq!(*bound.into_inner(), 7);
179    }
180
181    #[test]
182    fn is_send_and_sync() {
183        fn assert_send_sync<T: Send + Sync>() {}
184        // `Rc` is neither Send nor Sync; the wrapper makes it both.
185        assert_send_sync::<MainThreadBound<Rc<u32>>>();
186    }
187
188    #[test]
189    fn access_off_owner_thread_panics() {
190        // Move the wrapper to another thread and access it there: the owner-thread
191        // assertion must fire (the fail-fast safety net), not silently succeed.
192        let bound = MainThreadBound::new(Rc::new(1u32));
193        let handle = std::thread::spawn(move || {
194            let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
195                let _ = &*bound;
196            }));
197            // Leak the wrapper on this non-owning thread so its `Rc` is not dropped
198            // here; `MainThreadBound`'s Drop would leak it anyway, but be explicit.
199            core::mem::forget(bound);
200            caught.is_err()
201        });
202        assert!(
203            handle.join().expect("spawned thread panicked unexpectedly"),
204            "accessing MainThreadBound off the owner thread must panic"
205        );
206    }
207}