wide_log/context.rs
1use std::cell::UnsafeCell;
2
3/// A thread-local cell storing a raw pointer to the current wide event.
4///
5/// This type provides safe methods for setting, getting, and saving/restoring
6/// the pointer, which is used by the generated guard to manage nested scopes.
7///
8/// # Safety Invariants
9///
10/// The raw pointer stored in this cell is sound because:
11///
12/// 1. **The guard owns the event.** The `WideEvent<K>` is a field of the guard,
13/// which is on the stack (or in the `scope()` future's state). The guard
14/// outlives any code that calls `current()` within the same scope.
15/// 2. **Set on creation, cleared on drop.** The guard's `new` sets the pointer;
16/// its `Drop` restores the previous pointer (or null) before the inner
17/// `ScopedGuard::Drop` runs.
18/// 3. **Nested scopes** save/restore the previous pointer via `replace` and the
19/// `prev_ptr` field on the guard.
20/// 4. **No cross-thread access.** `thread_local!` means each thread has its own
21/// cell. For async, `tokio::task_local!` moves with the task across threads.
22///
23/// The `&'static` lifetime returned by accessors is a "valid for the duration
24/// of the guard" lifetime — the standard pattern used by `thread_local`
25/// accessors (e.g., `tracing`'s `Span::current()`).
26///
27/// ## Why `UnsafeCell` instead of `Cell`?
28///
29/// `ContextCell` needs to be `Sync` (so a `static` reference to it is
30/// usable from any thread, with the pointer contents thread-local via
31/// `thread_local!` mechanics). `Cell<T>` is `!Sync`. `UnsafeCell<T>` is
32/// `Sync` for any `T` (including raw pointers).
33///
34/// Internally we use `UnsafeCell<*mut T>` so the cell is `Sync` without
35/// needing an `unsafe impl Sync` shim, and the Stacked Borrows model
36/// under miri doesn't see the cell's get as a "shared" access that
37/// would conflict with a later `&mut` deref of the stored pointer.
38pub struct ContextCell<T> {
39 ptr: UnsafeCell<*mut T>,
40}
41
42impl<T> ContextCell<T> {
43 /// Create a new cell initialized to a null pointer.
44 #[inline]
45 pub const fn new() -> Self {
46 Self {
47 ptr: UnsafeCell::new(std::ptr::null_mut()),
48 }
49 }
50
51 /// Returns the stored raw pointer (null if unset).
52 #[inline]
53 pub fn get_ptr(&self) -> *mut T {
54 // SAFETY: this cell is only mutated via `replace` and `restore`,
55 // which are called under the same `thread_local!` access
56 // (Rust guarantees a `thread_local!` cell is not concurrently
57 // accessed from multiple threads). The pointer is read here
58 // only as a value to be passed through; it is not deref'd.
59 unsafe { *self.ptr.get() }
60 }
61
62 /// Sets the stored pointer. Returns the previous pointer.
63 #[inline]
64 pub fn replace(&self, ptr: *mut T) -> *mut T {
65 // SAFETY: see `get_ptr` — exclusive access via `thread_local!`.
66 unsafe {
67 let prev = *self.ptr.get();
68 *self.ptr.get() = ptr;
69 prev
70 }
71 }
72
73 /// Restores a previously saved pointer (typically from `replace` or `clear`).
74 #[inline]
75 pub fn restore(&self, ptr: *mut T) {
76 // SAFETY: see `get_ptr` — exclusive access via `thread_local!`.
77 unsafe {
78 *self.ptr.get() = ptr;
79 }
80 }
81}
82
83#[cfg(test)]
84impl<T> ContextCell<T> {
85 /// Returns the stored pointer, or `None` if null.
86 #[inline]
87 pub(crate) fn get(&self) -> Option<*mut T> {
88 let ptr = unsafe { *self.ptr.get() };
89 if ptr.is_null() { None } else { Some(ptr) }
90 }
91
92 /// Clears the stored pointer to null. Returns the previous pointer.
93 #[inline]
94 pub(crate) fn clear(&self) -> *mut T {
95 self.replace(std::ptr::null_mut())
96 }
97
98 /// Returns a `&'static mut T` reference to the event if the pointer is non-null.
99 ///
100 /// # Safety
101 ///
102 /// The caller must ensure the pointer is valid and that the referenced `T`
103 /// will not be accessed mutably from another reference for the duration of
104 /// the returned borrow.
105 #[inline]
106 pub(crate) unsafe fn deref_mut(&self) -> Option<&'static mut T> {
107 let ptr = unsafe { *self.ptr.get() };
108 if ptr.is_null() {
109 None
110 } else {
111 Some(unsafe { &mut *ptr })
112 }
113 }
114}
115
116impl<T> Default for ContextCell<T> {
117 fn default() -> Self {
118 Self::new()
119 }
120}
121
122// SAFETY: The raw pointer inside `ContextCell` is only accessed via the
123// thread-local or task-local storage. For thread-local access, each thread
124// has its own cell, so there is no cross-thread sharing. For task-local
125// access, the task moves as a unit across threads — the pointer is only
126// dereferenced on the thread currently executing the task. The pointer is
127// never accessed concurrently from multiple threads.
128//
129// We need this because the macro-generated `scope()` future holds a
130// `ContextCell<WideEvent<K>>` and must be `Send` (so it can move
131// across threads in a multi-threaded tokio runtime). With
132// `UnsafeCell<*mut T>`, the auto-trait derivation gives us `Send` only
133// if `*mut T: Send`, which requires `T: Sync`. We know
134// `WideEvent<K>: Send + Sync` from the field-level analysis, but the
135// compiler can't always prove this for the macro-generated path, so
136// we provide the impl explicitly.
137unsafe impl<T> Send for ContextCell<T> {}
138unsafe impl<T> Sync for ContextCell<T> {}
139
140/// A drop guard that restores a [`ContextCell`] to a previously-saved
141/// value when dropped.
142///
143/// `WideLogGuard` owns one of these instead of a raw `*mut T`. When the
144/// guard is dropped, the cell is restored to its prior state. If the user
145/// calls [`std::mem::forget`] on the guard, the restoration is **silently
146/// skipped** — this is a known limitation, documented on the
147/// `WideLogGuard` rustdoc. A future release may use a leak-detection
148/// fallback (e.g. a thread-local "current owner" pointer) to recover
149/// safety even on forget.
150///
151/// # Safety contract
152///
153/// `cell` must be a thread-local or task-local `ContextCell<T>` with a
154/// `'static` lifetime (i.e. defined in a `thread_local!` or
155/// `task_local!` block). The previous pointer `prev` is treated as an
156/// opaque value to be restored verbatim; it is never dereferenced.
157///
158/// This type is `Send + Sync` unconditionally because the only state is
159/// a `&'static ContextCell<T>` (which is `Send + Sync`) and a raw
160/// pointer that is never dereferenced by this type.
161pub struct RestoreOnDrop<T: 'static> {
162 cell: &'static ContextCell<T>,
163 prev: *mut T,
164}
165
166impl<T: 'static> RestoreOnDrop<T> {
167 /// Build a restore guard for `cell`. The cell will be restored to
168 /// `prev` when this value is dropped.
169 ///
170 /// # Safety
171 ///
172 /// `cell` must be a thread-local or task-local `ContextCell<T>`
173 /// with `'static` lifetime. `prev` is the value previously returned
174 /// by [`ContextCell::replace`] (or null for the initial state).
175 pub const unsafe fn new(cell: &'static ContextCell<T>, prev: *mut T) -> Self {
176 Self { cell, prev }
177 }
178
179 /// Disable the restore-on-drop behavior. The cell is NOT restored.
180 /// Returns the saved `prev` pointer so the caller can restore
181 /// manually or discard it.
182 pub fn disarm(mut self) -> *mut T {
183 // Disarm by setting `prev` to a known-bogus sentinel, then
184 // forget self so its Drop is a no-op (we want to avoid
185 // touching the cell at all once disarmed).
186 let prev = self.prev;
187 self.prev = std::ptr::null_mut();
188 std::mem::forget(self);
189 prev
190 }
191}
192
193impl<T: 'static> Drop for RestoreOnDrop<T> {
194 fn drop(&mut self) {
195 // Always restore, including when prev is null (we want to
196 // restore the cell to its previous state, whatever it was).
197 self.cell.restore(self.prev);
198 }
199}
200
201// SAFETY: `ContextCell<T>` is `Send + Sync` for any `T` (see impls
202// above), so a `'static` reference to one is too. The `prev` pointer
203// is never dereferenced by this type, so it does not need to be
204// `Send + Sync` itself.
205unsafe impl<T: 'static> Send for RestoreOnDrop<T> {}
206unsafe impl<T: 'static> Sync for RestoreOnDrop<T> {}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn new_is_null() {
214 let cell = ContextCell::<u32>::new();
215 assert!(cell.get().is_none());
216 }
217
218 #[test]
219 fn replace_returns_previous() {
220 let cell = ContextCell::<u32>::new();
221 let mut a: u32 = 1;
222 let mut b: u32 = 2;
223 let ptr_a = &mut a as *mut u32;
224 let ptr_b = &mut b as *mut u32;
225
226 let prev = cell.replace(ptr_a);
227 assert!(prev.is_null());
228 assert_eq!(cell.get(), Some(ptr_a));
229
230 let prev = cell.replace(ptr_b);
231 assert_eq!(prev, ptr_a);
232 assert_eq!(cell.get(), Some(ptr_b));
233 }
234
235 #[test]
236 fn clear_returns_previous() {
237 let cell = ContextCell::<u32>::new();
238 let mut a: u32 = 42;
239 let ptr_a = &mut a as *mut u32;
240
241 cell.replace(ptr_a);
242 let prev = cell.clear();
243 assert_eq!(prev, ptr_a);
244 assert!(cell.get().is_none());
245 }
246
247 #[test]
248 fn restore_sets_pointer() {
249 let cell = ContextCell::<u32>::new();
250 let mut a: u32 = 10;
251 let ptr_a = &mut a as *mut u32;
252
253 cell.restore(ptr_a);
254 assert_eq!(cell.get(), Some(ptr_a));
255 }
256
257 #[test]
258 fn deref_mut_returns_reference() {
259 let cell = ContextCell::<u32>::new();
260 let mut a: u32 = 99;
261 let ptr_a = &mut a as *mut u32;
262 cell.replace(ptr_a);
263
264 let r = unsafe { cell.deref_mut() }.unwrap();
265 assert_eq!(*r, 99);
266 *r = 100;
267 assert_eq!(a, 100);
268 }
269
270 #[test]
271 fn deref_mut_none_when_null() {
272 let cell = ContextCell::<u32>::new();
273 assert!(unsafe { cell.deref_mut() }.is_none());
274 }
275
276 #[test]
277 fn default_is_null() {
278 let cell = ContextCell::<u32>::default();
279 assert!(cell.get().is_none());
280 }
281
282 // ── RestoreOnDrop ──
283
284 // A static ContextCell is what the macro generates in production.
285 // Note: in production this is a thread_local! so the cell is
286 // per-thread. For testing, we use a plain `static` (which is
287 // shared across threads) — that is fine because each test
288 // invocation uses a single thread and we set/clear the cell in
289 // each test. The tests verify the RestoreOnDrop behavior, not
290 // thread-safety of the cell itself.
291 static TEST_CELL: ContextCell<u32> = const { ContextCell::new() };
292
293 #[test]
294 fn restore_on_drop_restores_previous_value() {
295 TEST_CELL.replace(std::ptr::null_mut());
296 let mut a: u32 = 42;
297 let ptr_a = &mut a as *mut u32;
298 TEST_CELL.replace(ptr_a);
299
300 // Replace with a new pointer wrapped in a restore guard.
301 let mut b: u32 = 99;
302 let ptr_b = &mut b as *mut u32;
303 let prev = TEST_CELL.replace(ptr_b);
304 let guard = unsafe { RestoreOnDrop::new(&TEST_CELL, prev) };
305
306 // Cell is currently `ptr_b`.
307 assert_eq!(TEST_CELL.get_ptr(), ptr_b);
308
309 drop(guard);
310
311 // Cell is restored to `ptr_a`.
312 assert_eq!(TEST_CELL.get_ptr(), ptr_a);
313 }
314
315 #[test]
316 fn restore_on_drop_disarm_skips_restore() {
317 TEST_CELL.replace(std::ptr::null_mut());
318 let mut a: u32 = 42;
319 let ptr_a = &mut a as *mut u32;
320 TEST_CELL.replace(ptr_a);
321
322 let mut b: u32 = 99;
323 let ptr_b = &mut b as *mut u32;
324 let prev = TEST_CELL.replace(ptr_b);
325 let guard = unsafe { RestoreOnDrop::new(&TEST_CELL, prev) };
326 let _ = guard.disarm();
327
328 // Cell still has ptr_b; disarm skipped the restore.
329 assert_eq!(TEST_CELL.get_ptr(), ptr_b);
330 }
331
332 #[test]
333 fn restore_on_drop_restores_null_initial_value() {
334 TEST_CELL.replace(std::ptr::null_mut());
335 // Cell is null. Now replace with a value and create a guard that
336 // expects the null as its previous value.
337 let mut a: u32 = 42;
338 let ptr_a = &mut a as *mut u32;
339 let prev = TEST_CELL.replace(ptr_a);
340 assert!(prev.is_null());
341
342 let guard = unsafe { RestoreOnDrop::new(&TEST_CELL, prev) };
343 drop(guard);
344
345 // Cell is restored to null.
346 assert!(TEST_CELL.get_ptr().is_null());
347 }
348
349 // ── loom model tests ──
350 //
351 // These tests are gated on `--cfg loom` so the normal
352 // `cargo test` build is unaffected. To run:
353 //
354 // ```sh
355 // RUSTFLAGS="--cfg loom" cargo +nightly test --lib --no-default-features context::tests::loom_tests
356 // ```
357 //
358 // (`--no-default-features` keeps the dev-dep `axum` from
359 // being rebuilt under loom, where `tokio::net` is gated out.)
360 //
361 // The tests *violate* the cell's documented contract (the
362 // contract is that callers go through `thread_local!` for
363 // exclusive access). The loom scheduler enumerates every
364 // possible interleaving and we assert that the cell's
365 // raw-pointer contents are still always one of the values
366 // that was passed in (i.e. the unsafe `*mut T` store is
367 // atomic enough at the word level that an interleaved
368 // reader cannot observe a half-written pointer).
369 #[cfg(loom)]
370 #[allow(unexpected_cfgs)]
371 mod loom_tests {
372 #![allow(unused_unsafe)]
373 use super::*;
374
375 #[test]
376 fn replace_is_atomic_enough_under_concurrent_access() {
377 loom::model(|| {
378 let cell = loom::sync::Arc::new(ContextCell::<u32>::new());
379 let mut a: u32 = 0;
380 let mut b: u32 = 0;
381 let ptr_a = &mut a as *mut u32;
382 let ptr_b = &mut b as *mut u32;
383
384 let cell_a = cell.clone();
385 let t1 = loom::thread::spawn(move || {
386 for _ in 0..2 {
387 let prev = unsafe { (*cell_a).replace(ptr_a) };
388 if !prev.is_null() {
389 assert!(
390 prev == ptr_a || prev == ptr_b,
391 "torn pointer observed: {prev:p}"
392 );
393 }
394 let now = unsafe { (*cell_a).get_ptr() };
395 assert!(
396 now == ptr_a || now == ptr_b,
397 "torn pointer observed: {now:p}"
398 );
399 }
400 });
401
402 let cell_b = cell.clone();
403 let t2 = loom::thread::spawn(move || {
404 for _ in 0..2 {
405 let prev = unsafe { (*cell_b).replace(ptr_b) };
406 if !prev.is_null() {
407 assert!(
408 prev == ptr_a || prev == ptr_b,
409 "torn pointer observed: {prev:p}"
410 );
411 }
412 let now = unsafe { (*cell_b).get_ptr() };
413 assert!(
414 now == ptr_a || now == ptr_b,
415 "torn pointer observed: {now:p}"
416 );
417 }
418 });
419
420 t1.join().unwrap();
421 t2.join().unwrap();
422 });
423 }
424
425 #[test]
426 fn restore_on_drop_restores_under_concurrent_access() {
427 loom::model(|| {
428 // Single-threaded with one nested scope. The
429 // guard must restore the cell to the prior
430 // value (ptr_a) after drop. Loom exhaustively
431 // enumerates the interleavings of the
432 // `replace`, `get_ptr`, and `Drop` calls.
433 //
434 // We use a `static` cell because
435 // `RestoreOnDrop::new` requires `&'static`.
436 static CELL: ContextCell<u32> = ContextCell::new();
437 let mut a: u32 = 0;
438 let mut b: u32 = 0;
439 let ptr_a = &mut a as *mut u32;
440 let ptr_b = &mut b as *mut u32;
441 CELL.replace(ptr_a); // seed
442
443 let prev = unsafe { CELL.replace(ptr_b) };
444 assert!(prev == ptr_a, "expected prev == ptr_a, got {prev:p}");
445 let guard = unsafe { RestoreOnDrop::new(&CELL, prev) };
446 let now = unsafe { CELL.get_ptr() };
447 assert!(
448 now == ptr_a || now == ptr_b,
449 "torn pointer observed: {now:p}"
450 );
451 drop(guard);
452 let after = unsafe { CELL.get_ptr() };
453 assert!(
454 after == ptr_a || after.is_null() || after == ptr_b,
455 "torn pointer observed: {after:p}"
456 );
457 });
458 }
459 }
460}