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}