Skip to content

Commit

Permalink
Allow Boehm to track thread locals
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jacob-hughes committed Jan 17, 2024
1 parent fd6f1af commit 23f4775
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 0 deletions.
4 changes: 4 additions & 0 deletions library/boehm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
40 changes: 40 additions & 0 deletions library/std/src/sys/common/thread_local/os_local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -143,6 +182,7 @@ impl<T: 'static> Key<T> {
// 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 {
Expand Down
74 changes: 74 additions & 0 deletions tests/ui/runtime/gc/thread_local.rs
Original file line number Diff line number Diff line change
@@ -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<Finalizable> = Gc::new(Finalizable(1));
static LOCAL2: Gc<Finalizable> = Gc::new(Finalizable(2));
static LOCAL3: Gc<Finalizable> = Gc::new(Finalizable(3));

static LOCAL4: Box<Gc<Finalizable>> = Box::new(Gc::new(Finalizable(4)));
static LOCAL5: Box<Gc<Finalizable>> = Box::new(Gc::new(Finalizable(5)));
static LOCAL6: Box<Gc<Finalizable>> = 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();
}

0 comments on commit 23f4775

Please sign in to comment.