From 23f4775facee351626896db3b85f2492d1f89ec3 Mon Sep 17 00:00:00 2001 From: Jake Hughes Date: Wed, 17 Jan 2024 12:40:59 +0000 Subject: [PATCH] Allow Boehm to track thread locals This introduces a thread-local rootset vector, which holds pointers to the current thread's TL values. This rootset is then specifically added to Boehm's mark list so that TL values can be traced. --- library/boehm/src/lib.rs | 4 + .../src/sys/common/thread_local/os_local.rs | 40 ++++++++++ tests/ui/runtime/gc/thread_local.rs | 74 +++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 tests/ui/runtime/gc/thread_local.rs diff --git a/library/boehm/src/lib.rs b/library/boehm/src/lib.rs index f1b8c08dd30c7..f745e635c5092 100644 --- a/library/boehm/src/lib.rs +++ b/library/boehm/src/lib.rs @@ -79,5 +79,9 @@ extern "C" { pub fn GC_set_warn_proc(level: *mut u8); + pub fn GC_tls_rootset() -> *mut u8; + + pub fn GC_init_tls_rootset(rootset: *mut u8); + pub fn GC_ignore_warn_proc(proc: *mut u8, word: usize); } diff --git a/library/std/src/sys/common/thread_local/os_local.rs b/library/std/src/sys/common/thread_local/os_local.rs index 7cf291921228b..2856fd1ce7725 100644 --- a/library/std/src/sys/common/thread_local/os_local.rs +++ b/library/std/src/sys/common/thread_local/os_local.rs @@ -3,6 +3,45 @@ use crate::cell::Cell; use crate::sys_common::thread_local_key::StaticKey as OsStaticKey; use crate::{fmt, marker, panic, ptr}; +use alloc::boehm; + +/// A buffer of pointers to each thread local variable. +/// +/// The Boehm GC can't locate GC pointers stored inside POSIX thread locals, so +/// this struct keeps track of pointers to thread local data, which the GC then +/// uses as part of its marking rootset. +/// +/// Despite its implementation as a ZST, this struct is stateful -- its methods +/// have side-effects and are performed on a buffer stored in a special +/// thread-local value. However, this state is declared from within the BDWGC +/// and deliberately hidden from rustc, which is why the API uses static methods +/// (i.e. does not take self references). +/// +/// The reason for this design is that `TLSRoots` is modified from inside Rust's +/// `thread_local!` API: if we were to implement this data structure using +/// Rust's thread local API, we would run into problems such as re-entrancy +/// issues or infinite recursion. +/// +/// Usage of this struct is safe because it provides no access to the underlying +/// roots except via methods which are guaranteed not to leak aliasing mutable +/// references. +struct TLSRoots; + +impl TLSRoots { + /// Push a root to the current thread's TLS rootset. This lazily + /// initialises the backing vector. + fn push(root: *mut u8) { + let mut rootset = unsafe { boehm::GC_tls_rootset() as *mut Vec<*mut u8> }; + if rootset.is_null() { + let v = Vec::new(); + let buf: *mut Vec<*mut u8> = Box::into_raw(Box::new(v)); + unsafe { boehm::GC_init_tls_rootset(buf as *mut u8) }; + rootset = buf + } + unsafe { (&mut *rootset).push(root) }; + } +} + #[doc(hidden)] #[allow_internal_unstable(thread_local_internals)] #[allow_internal_unsafe] @@ -143,6 +182,7 @@ impl Key { // If the lookup returned null, we haven't initialized our own // local copy, so do that now. let ptr = Box::into_raw(Box::new(Value { inner: LazyKeyInner::new(), key: self })); + TLSRoots::push(ptr as *mut u8); // SAFETY: At this point we are sure there is no value inside // ptr so setting it will not affect anyone else. unsafe { diff --git a/tests/ui/runtime/gc/thread_local.rs b/tests/ui/runtime/gc/thread_local.rs new file mode 100644 index 0000000000000..633eee575e064 --- /dev/null +++ b/tests/ui/runtime/gc/thread_local.rs @@ -0,0 +1,74 @@ +// ignore-test +// ignore-tidy-linelength +// no-prefer-dynamic +#![feature(allocator_api)] +#![feature(gc)] +#![feature(negative_impls)] +#![feature(thread_local)] + +use std::gc::{Gc, GcAllocator}; +use std::{thread, time}; +use std::sync::atomic::{self, AtomicUsize}; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[global_allocator] +static GC: GcAllocator = GcAllocator; + +struct Finalizable(u32); + +static FINALIZER_COUNT: AtomicUsize = AtomicUsize::new(0); + +impl Drop for Finalizable { + fn drop(&mut self) { + FINALIZER_COUNT.fetch_add(1, atomic::Ordering::Relaxed); + } +} + +thread_local!{ + static LOCAL1: Gc = Gc::new(Finalizable(1)); + static LOCAL2: Gc = Gc::new(Finalizable(2)); + static LOCAL3: Gc = Gc::new(Finalizable(3)); + + static LOCAL4: Box> = Box::new(Gc::new(Finalizable(4))); + static LOCAL5: Box> = Box::new(Gc::new(Finalizable(5))); + static LOCAL6: Box> = Box::new(Gc::new(Finalizable(6))); +} + +fn do_stuff_with_tls() { + let nanos = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_nanos(); + + // We need to use the thread-local at least once to ensure that it is initialised. By adding it + // to the current system time, we ensure that this use can't be optimised away (e.g. by constant + // folding). + let mut dynamic_value = nanos; + + dynamic_value += LOCAL1.with(|l| l.0); + dynamic_value += LOCAL2.with(|l| l.0); + dynamic_value += LOCAL3.with(|l| l.0); + dynamic_value += LOCAL4.with(|l| l.0); + dynamic_value += LOCAL5.with(|l| l.0); + dynamic_value += LOCAL6.with(|l| l.0); + + // Keep the thread alive long enough so that the GC has the chance to scan its thread-locals for + // roots. + thread::sleep(time::Duration::from_millis(20)); + + + assert!(dynamic_value > 0); + + // This ensures that a GC invoked from the main thread does not cause this thread's thread + // locals to be reclaimed too early. + assert_eq!(FINALIZER_COUNT.load(atomic::Ordering::Relaxed), 0); + +} + +fn main() { + let t2 = std::thread::spawn(do_stuff_with_tls); + + // Wait a little bit of time for the t2 to initialise thread-locals. + thread::sleep(time::Duration::from_millis(10)); + + GcAllocator::force_gc(); + + let _ = t2.join().unwrap(); +}