Skip to main content

vtcode_commons/
thread_safety.rs

1//! # Thread Safety Primitives
2//!
3//! Based on "Formal methods for the unsafe side of the Force" (Antithesis, 2026).
4//! Provides rigorously defined primitives for bridging FFI and multi-threaded boundaries.
5//!
6//! ## `RelaxedAtomic<T>`
7//!
8//! Provides inner mutability for `Copy` types via relaxed atomic loads and stores.
9//! On x86_64 and ARM, relaxed loads/stores compile to the same instructions as
10//! regular memory accesses (no `LOCK` prefix), making this a zero-overhead way to
11//! achieve interior mutability for atomic-compatible types.
12//!
13//! Deliberately omits `fetch_*` / CAS methods — those lower to bus-lock instructions
14//! that add noticeable overhead. Instead, use the `load`–mutate–`store` pattern:
15//!
16//! ```
17//! # use vtcode_commons::thread_safety::RelaxedAtomic;
18//! let counter = RelaxedAtomic::new(0u32);
19//! let val = counter.load();
20//! counter.store(val + 1);
21//! ```
22
23use std::fmt;
24use std::marker::PhantomData;
25use std::sync::OnceLock;
26use std::sync::atomic::Ordering;
27use std::thread::{self, ThreadId};
28
29/// Trait for types that can be stored in a [`RelaxedAtomic`].
30///
31/// Implemented for `bool`, `u8`, `u16`, `u32`, `usize`, `i8`, `i16`, `i32`, `isize`.
32pub trait AtomicRepr: Copy + 'static {
33    /// The underlying `std::sync::atomic::Atomic*` type.
34    type Atomic: 'static + Send + Sync;
35    /// Create a new atomic instance for the given value.
36    fn new_atomic(val: Self) -> Self::Atomic;
37    /// Load the value with `Ordering::Relaxed`.
38    fn load(atomic: &Self::Atomic) -> Self;
39    /// Store the value with `Ordering::Relaxed`.
40    fn store(atomic: &Self::Atomic, val: Self);
41    /// Unwrap the atomic and return the contained value (no atomic instruction).
42    fn into_inner(atomic: Self::Atomic) -> Self;
43}
44
45macro_rules! impl_atomic_repr {
46    ($ty:ty, $atomic:ty) => {
47        impl AtomicRepr for $ty {
48            type Atomic = $atomic;
49            fn new_atomic(val: Self) -> Self::Atomic {
50                <$atomic>::new(val)
51            }
52            fn load(atomic: &Self::Atomic) -> Self {
53                atomic.load(Ordering::Relaxed)
54            }
55            fn store(atomic: &Self::Atomic, val: Self) {
56                atomic.store(val, Ordering::Relaxed);
57            }
58            fn into_inner(atomic: Self::Atomic) -> Self {
59                atomic.into_inner()
60            }
61        }
62    };
63}
64
65impl_atomic_repr!(bool, std::sync::atomic::AtomicBool);
66impl_atomic_repr!(u8, std::sync::atomic::AtomicU8);
67impl_atomic_repr!(u16, std::sync::atomic::AtomicU16);
68impl_atomic_repr!(u32, std::sync::atomic::AtomicU32);
69impl_atomic_repr!(usize, std::sync::atomic::AtomicUsize);
70impl_atomic_repr!(i8, std::sync::atomic::AtomicI8);
71impl_atomic_repr!(i16, std::sync::atomic::AtomicI16);
72impl_atomic_repr!(i32, std::sync::atomic::AtomicI32);
73impl_atomic_repr!(isize, std::sync::atomic::AtomicIsize);
74
75/// Provides inner mutability for `Copy` types via relaxed atomic operations.
76///
77/// On x86_64 and ARM, relaxed loads and stores compile to the same instructions
78/// as regular memory accesses — no `LOCK` prefix is emitted. This makes
79/// `RelaxedAtomic` a zero-overhead way to achieve interior mutability without
80/// the bus-lock cost of `fetch_*` or CAS operations.
81///
82/// Deliberately exposes only `load` and `store`. The `fetch_*` methods are
83/// omitted because they emit `LOCK`-prefixed instructions with measurable
84/// overhead. Instead, use the load–mutate–store pattern:
85///
86/// ```
87/// # use vtcode_commons::thread_safety::RelaxedAtomic;
88/// let counter = RelaxedAtomic::new(0u32);
89/// let val = counter.load();
90/// counter.store(val + 1);
91/// ```
92///
93/// # When to use
94///
95/// Use when a field needs interior mutability and is accessed without
96/// contention (same pattern as the original C code using plain loads/stores).
97/// If you need multi-step atomic operations (CAS, fetch_add), use the
98/// underlying `std::sync::atomic` types directly.
99///
100/// # When *not* to use
101///
102/// Do not use when the operation must be atomic relative to other threads.
103/// The load–mutate–store pattern is *not* atomic as a whole — it can race
104/// with concurrent stores. Use only where the C code would have used a
105/// non-atomic access that happens to be race-free by design.
106#[derive(Debug)]
107pub struct RelaxedAtomic<T: AtomicRepr> {
108    inner: T::Atomic,
109}
110
111impl<T: AtomicRepr> RelaxedAtomic<T> {
112    /// Create a new `RelaxedAtomic` with the given initial value.
113    #[inline]
114    pub fn new(val: T) -> Self {
115        Self {
116            inner: T::new_atomic(val),
117        }
118    }
119
120    /// Load the current value with relaxed ordering.
121    #[inline]
122    pub fn load(&self) -> T {
123        T::load(&self.inner)
124    }
125
126    /// Store a new value with relaxed ordering.
127    #[inline]
128    pub fn store(&self, val: T) {
129        T::store(&self.inner, val);
130    }
131
132    /// Consume the atomic and return the inner value.
133    pub fn into_inner(self) -> T {
134        T::into_inner(self.inner)
135    }
136}
137
138impl RelaxedAtomic<u32> {
139    /// Atomic add with relaxed ordering.
140    ///
141    /// Returns the previous value. Under `Relaxed` ordering this does not emit
142    /// a `LOCK`-prefixed bus cycle on x86_64 or ARMv8 — it compiles to a
143    /// plain locked `add` that implements a single-copy atomic RMW without
144    /// the costly bus-lock side effects of stronger orderings.
145    #[inline]
146    pub fn fetch_add(&self, val: u32) -> u32 {
147        self.inner.fetch_add(val, Ordering::Relaxed)
148    }
149}
150
151impl RelaxedAtomic<u32> {
152    /// Atomic subtract with relaxed ordering.
153    #[inline]
154    pub fn fetch_sub(&self, val: u32) -> u32 {
155        self.inner.fetch_sub(val, Ordering::Relaxed)
156    }
157}
158
159/// Note: this performs two separate relaxed loads. Under concurrent writes
160/// the two values may come from different points in time. Use this only
161/// for diagnostic assertions — never for correctness-critical decisions.
162impl<T: AtomicRepr + PartialEq> PartialEq for RelaxedAtomic<T> {
163    fn eq(&self, other: &Self) -> bool {
164        self.load() == other.load()
165    }
166}
167
168impl<T: AtomicRepr + Eq> Eq for RelaxedAtomic<T> {}
169
170impl<T: AtomicRepr + Default> Default for RelaxedAtomic<T> {
171    fn default() -> Self {
172        Self::new(T::default())
173    }
174}
175
176impl<T: AtomicRepr> Clone for RelaxedAtomic<T> {
177    fn clone(&self) -> Self {
178        Self::new(self.load())
179    }
180}
181
182impl<T: AtomicRepr + fmt::Display> fmt::Display for RelaxedAtomic<T> {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        self.load().fmt(f)
185    }
186}
187
188/// Stores the `ThreadId` designated as the application's main thread.
189///
190/// Populated exactly once by [`designate_main_thread`]; subsequent calls are no-ops
191/// so that callers can re-assert designation from defensive initialization paths
192/// without panicking.
193static MAIN_THREAD_ID: OnceLock<ThreadId> = OnceLock::new();
194
195/// Designate the calling thread as the application's main thread.
196///
197/// Should be invoked once, early in `main`, before spawning any worker threads
198/// that may try to obtain a [`MainThreadToken`]. Subsequent calls have no effect.
199pub fn designate_main_thread() {
200    let _ = MAIN_THREAD_ID.set(thread::current().id());
201}
202
203/// Returns the `ThreadId` previously designated as the main thread, if any.
204pub fn main_thread_id() -> Option<ThreadId> {
205    MAIN_THREAD_ID.get().copied()
206}
207
208/// A witness of execution that exists solely on a designated "Main Thread".
209///
210/// In FFI contexts, many libraries (especially legacy C++ or UI frameworks)
211/// are not thread-safe and must only be initialized, called, or dropped from
212/// the same thread that originally created them.
213///
214/// `MainThreadToken` is a zero-sized proof carrier. Possessing it proves
215/// (at a type-system level) that the holder previously executed on the
216/// designated main thread. The `PhantomData<*mut ()>` makes the token
217/// `!Send + !Sync`, so a token obtained on the main thread cannot leak to
218/// another thread through ordinary safe code.
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub struct MainThreadToken(PhantomData<*mut ()>);
221
222impl MainThreadToken {
223    /// Create a new `MainThreadToken` without verifying the current thread.
224    ///
225    /// # Safety
226    ///
227    /// The caller must guarantee that:
228    /// 1. They are executing on the thread that was (or will be) passed to
229    ///    [`designate_main_thread`], and
230    /// 2. The resulting token will not be transmitted to another thread
231    ///    through `unsafe` channels (the type is `!Send + !Sync`, which
232    ///    prevents safe channels from doing so).
233    #[expect(
234        unsafe_code,
235        reason = "phantom data marker; !Send + !Sync prevents token leakage"
236    )]
237    pub unsafe fn new_unchecked() -> Self {
238        Self(PhantomData)
239    }
240
241    /// Obtain a token if the current thread matches the one previously passed
242    /// to [`designate_main_thread`].
243    ///
244    /// Returns `None` if [`designate_main_thread`] has never been called, or
245    /// if the current thread is not the designated main thread.
246    pub fn try_new() -> Option<Self> {
247        let designated = MAIN_THREAD_ID.get()?;
248        if *designated == thread::current().id() {
249            Some(Self(PhantomData))
250        } else {
251            None
252        }
253    }
254}
255
256/// A wrapper that allows sending non-`Send` types across thread boundaries.
257///
258/// Re-exported from the `send_wrapper` crate. It implements `Send` and `Sync`
259/// regardless of whether the wrapped type is thread-safe. However, it will
260/// panic at runtime if the wrapped value is accessed from any thread other
261/// than the one that created it.
262pub use send_wrapper::SendWrapper;
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use std::thread;
268
269    #[test]
270    fn worker_thread_never_obtains_token() {
271        // A spawned worker thread is never the designated main thread, even if
272        // some other test in this process has called `designate_main_thread`
273        // on a different thread. The token type is `!Send`, so we materialize
274        // it inside the worker and return only its presence as a `bool`.
275        let on_worker = thread::spawn(|| MainThreadToken::try_new().is_some())
276            .join()
277            .expect("worker thread");
278        assert!(!on_worker);
279    }
280
281    #[test]
282    fn try_new_returns_some_after_designation_on_same_thread() {
283        designate_main_thread();
284        // If this test happens to run on the same thread that another test
285        // designated, we still get a token; if a different thread was
286        // designated first, `try_new` correctly returns `None`.
287        match main_thread_id() {
288            Some(id) if id == thread::current().id() => {
289                assert!(MainThreadToken::try_new().is_some());
290            }
291            _ => {
292                assert!(MainThreadToken::try_new().is_none());
293            }
294        }
295    }
296}