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}