use crate::trace::{Trace, TraceFn, Tracer, trace_array_fn, trace_fn};
use alloc::alloc::{Layout, alloc, dealloc};
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use core::cell::RefCell;
use core::ptr::NonNull;
struct Obj {
start: usize,
size: usize,
trace: TraceFn,
drop: Option<unsafe fn(*mut u8)>,
align_log2: u8,
mark: bool,
}
impl Obj {
fn end(&self) -> usize {
self.start + self.size.max(1)
}
fn layout(&self) -> Layout {
Layout::from_size_align(self.size.max(1), 1 << self.align_log2).expect("layout was valid")
}
}
pub struct Heap {
objs: Vec<Obj>,
sorted: bool,
live_bytes: usize,
threshold: usize,
globals: Vec<(usize, usize, TraceFn)>,
collections: usize,
quarantine: BTreeMap<usize, usize>,
}
const MIN_THRESHOLD: usize = 4 << 20;
rt_global! {
static HEAP: RefCell<Heap> = RefCell::new(Heap {
objs: Vec::new(),
sorted: true,
live_bytes: 0,
threshold: MIN_THRESHOLD,
globals: Vec::new(),
collections: 0,
quarantine: BTreeMap::new(),
});
}
fn with_heap<R>(f: impl FnOnce(&mut Heap) -> R) -> R {
HEAP.with(|h| f(&mut h.borrow_mut()))
}
pub fn allocate<T: Trace>(value: T) -> NonNull<T> {
let layout = Layout::new::<T>();
if layout.size() == 0 {
return zerobase().cast();
}
let should_collect = with_heap(|h| h.live_bytes + layout.size() > h.threshold);
if should_collect || cfg!(feature = "gc-torture") {
collect();
}
let ptr = unsafe { alloc(pad(layout)) } as *mut T;
let Some(ptr) = NonNull::new(ptr) else {
alloc::alloc::handle_alloc_error(layout)
};
unsafe { ptr.write(value) };
with_heap(|h| {
h.objs.push(Obj {
start: ptr.as_ptr() as usize,
size: layout.size(),
trace: trace_fn::<T>(),
drop: needs_drop::<T>(),
align_log2: layout.align().trailing_zeros() as u8,
mark: false,
});
h.sorted = false;
h.live_bytes += layout.size();
});
ptr
}
pub fn allocate_bytes(len: usize, fill: impl FnOnce(&mut [u8])) -> NonNull<u8> {
assert!(len > 0, "zero-length allocations use a static empty");
let layout = Layout::array::<u8>(len).expect("fits in memory");
let should_collect = with_heap(|h| h.live_bytes + len > h.threshold);
if should_collect || cfg!(feature = "gc-torture") {
collect();
}
let ptr = unsafe { alloc(layout) };
let Some(ptr) = NonNull::new(ptr) else {
alloc::alloc::handle_alloc_error(layout)
};
fill(unsafe { core::slice::from_raw_parts_mut(ptr.as_ptr(), len) });
with_heap(|h| {
h.objs.push(Obj {
start: ptr.as_ptr() as usize,
size: len,
trace: |_, _, _| {},
drop: None,
align_log2: 0,
mark: false,
});
h.sorted = false;
h.live_bytes += len;
});
ptr
}
pub fn allocate_array<P: crate::place::Place + Trace>(n: usize) -> NonNull<P> {
let layout = Layout::array::<P>(n).expect("slice fits in memory");
let should_collect = with_heap(|h| h.live_bytes + layout.size() > h.threshold);
if should_collect || cfg!(feature = "gc-torture") {
collect();
}
let ptr = unsafe { alloc(pad(layout)) } as *mut P;
let Some(ptr) = NonNull::new(ptr) else {
alloc::alloc::handle_alloc_error(layout)
};
for i in 0..n {
unsafe { ptr.add(i).write(P::new(crate::value::GoValue::zero())) };
}
with_heap(|h| {
h.objs.push(Obj {
start: ptr.as_ptr() as usize,
size: layout.size(),
trace: trace_array_fn::<P>(),
drop: needs_drop::<P>(),
align_log2: layout.align().trailing_zeros() as u8,
mark: false,
});
h.sorted = false;
h.live_bytes += layout.size();
});
ptr
}
fn needs_drop<T>() -> Option<unsafe fn(*mut u8)> {
if core::mem::needs_drop::<T>() {
Some(|p| unsafe { core::ptr::drop_in_place(p as *mut T) })
} else {
None
}
}
pub(crate) fn zerobase() -> NonNull<u8> {
#[repr(align(16))]
struct Zerobase([u8; 0]);
static ZEROBASE: Zerobase = Zerobase([]);
NonNull::from(&ZEROBASE).cast()
}
fn pad(l: Layout) -> Layout {
if l.size() == 0 {
Layout::from_size_align(1, l.align()).expect("valid layout")
} else {
l
}
}
pub fn register_global<T: Trace>(place: &'static T) {
with_heap(|h| {
h.globals
.push((place as *const T as usize, size_of::<T>(), trace_fn::<T>()))
});
}
pub fn collect() {
with_heap(|h| {
h.collections += 1;
for o in &mut h.objs {
o.mark = false;
}
if !h.sorted {
h.objs.sort_unstable_by_key(|o| o.start);
h.sorted = true;
}
let mut tracer = Tracer::new(h);
crate::gc::trace_roots(&mut tracer);
let globals = tracer.heap.globals.clone();
for (addr, size, trace) in globals {
unsafe { trace(addr as *const u8, size, &mut tracer) };
}
tracer.drain();
let mut live = 0;
let quarantine = &mut h.quarantine;
h.objs.retain(|o| {
if o.mark {
live += o.size;
return true;
}
if let Some(drop) = o.drop {
unsafe { drop(o.start as *mut u8) };
}
if cfg!(feature = "gc-torture") {
unsafe { core::ptr::write_bytes(o.start as *mut u8, POISON, o.size) };
quarantine.insert(o.start, o.end());
return false;
}
unsafe { dealloc(o.start as *mut u8, o.layout()) };
false
});
h.live_bytes = live;
h.threshold = (live * 2).max(MIN_THRESHOLD);
});
}
const POISON: u8 = 0xA5;
#[inline]
pub fn check_live(addr: usize) {
if cfg!(feature = "gc-torture") && addr != 0 {
let hit = with_heap(|h| {
h.quarantine
.range(..=addr)
.next_back()
.is_some_and(|(_, &end)| addr < end)
});
if hit {
panic!("rustygo gc-torture: use of collected object at {addr:#x} (a missing GC root)");
}
}
}
impl Heap {
fn find(&self, addr: usize) -> Option<usize> {
if addr == 0 {
return None;
}
let i = self.objs.partition_point(|o| o.start <= addr);
let o = self.objs.get(i.checked_sub(1)?)?;
(addr < o.end()).then(|| i - 1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Stats {
pub objects: usize,
pub bytes: usize,
pub collections: usize,
}
pub fn stats() -> Stats {
with_heap(|h| Stats {
objects: h.objs.len(),
bytes: h.live_bytes,
collections: h.collections,
})
}
impl<'h> Tracer<'h> {
pub(crate) fn new(heap: &'h mut Heap) -> Self {
Tracer {
heap,
work: Vec::new(),
}
}
pub fn edge(&mut self, addr: usize) {
if let Some(i) = self.heap.find(addr)
&& !self.heap.objs[i].mark
{
self.heap.objs[i].mark = true;
self.work.push(i);
}
}
pub(crate) fn drain(&mut self) {
while let Some(i) = self.work.pop() {
let o = &self.heap.objs[i];
let (start, size, trace) = (o.start, o.size, o.trace);
unsafe { trace(start as *const u8, size, self) };
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gc::Frame;
use crate::place::{Place, Ptr, Slot};
use crate::string::GoStr;
struct NodeP {
next: Slot<Ptr<NodeP>>,
name: Slot<GoStr>,
}
impl Trace for NodeP {
fn trace(&self, t: &mut Tracer<'_>) {
self.next.trace(t);
self.name.trace(t);
}
}
impl Place for NodeP {
type Value = (Ptr<NodeP>, GoStr);
fn new(v: Self::Value) -> Self {
NodeP {
next: Place::new(v.0),
name: Place::new(v.1),
}
}
fn load(&self) -> Self::Value {
(self.next.load(), self.name.load())
}
fn store(&self, v: Self::Value) {
self.next.store(v.0);
self.name.store(v.1);
}
}
fn chain(n: usize) -> Ptr<NodeP> {
let mut head = Ptr::<NodeP>::zero();
let frame = Frame::<2>::new();
frame.scope(|| {
for _ in 0..n {
frame.set(0, &head);
let name = GoStr::lit(b"x").concat(GoStr::lit(b"y"));
frame.set(1, &name);
head = Ptr::alloc((head, name));
}
});
head
}
#[test]
fn collects_garbage_but_keeps_roots() {
let before = stats();
for _ in 0..4 {
chain(50);
}
collect();
let after_garbage = stats();
assert_eq!(
after_garbage.objects, before.objects,
"garbage chains should be gone"
);
let frame = Frame::<1>::new();
frame.scope(|| {
let head = chain(50);
frame.set(0, &head);
collect();
assert_eq!(stats().objects, before.objects + 100); let mut n = head;
let mut count = 0;
while n != Ptr::zero() {
assert_eq!(n.load().1.bytes(), b"xy");
n = n.load().0;
count += 1;
}
assert_eq!(count, 50);
});
collect();
assert_eq!(stats().objects, before.objects, "dropped after the scope");
}
#[test]
fn interior_pointers_keep_their_object_alive() {
let frame = Frame::<1>::new();
frame.scope(|| {
let head = chain(3); let mut tail = head;
while tail.load().0 != Ptr::zero() {
tail = tail.load().0;
}
let field: Ptr<Slot<GoStr>> = tail.project(|n| &n.name);
frame.set(0, &field);
collect();
assert_eq!(stats().objects, 2);
assert_eq!(field.load().bytes(), b"xy");
});
}
#[test]
fn substrings_keep_the_whole_array_alive() {
let frame = Frame::<1>::new();
frame.scope(|| {
let s = GoStr::lit(b"hello, ").concat(GoStr::lit(b"world"));
let tail = s.slice(7, None);
frame.set(0, &tail);
collect();
assert_eq!(tail.bytes(), b"world");
assert_eq!(stats().objects, 1);
});
}
}