use std::cell::RefCell;
use std::marker::PhantomData;
use crate::GcRef;
use crate::context::RuntimeContext;
pub trait RootSet {
fn push_roots(&self, out: &mut Vec<GcRef>);
}
impl RootSet for () {
fn push_roots(&self, _out: &mut Vec<GcRef>) {}
}
pub trait WeakSet {
fn clear_reclaimed(&self) -> usize;
}
impl WeakSet for () {
fn clear_reclaimed(&self) -> usize {
0
}
}
pub struct RootScope<'a> {
parent: Option<&'a dyn RootSet>,
roots: Vec<GcRef>,
}
impl<'a> RootScope<'a> {
pub fn new() -> Self {
RootScope {
parent: None,
roots: Vec::new(),
}
}
pub fn child(parent: &'a dyn RootSet) -> Self {
RootScope {
parent: Some(parent),
roots: Vec::new(),
}
}
pub fn root(&mut self, gcref: GcRef) -> GcRef {
self.roots.push(gcref);
gcref
}
pub fn root_count(&self) -> usize {
self.roots.len()
}
}
impl Default for RootScope<'_> {
fn default() -> Self {
Self::new()
}
}
impl RootSet for RootScope<'_> {
fn push_roots(&self, out: &mut Vec<GcRef>) {
if let Some(parent) = self.parent {
parent.push_roots(out);
}
out.extend_from_slice(&self.roots);
}
}
pub const NATIVE_ROOT_RESERVATION: usize = 1024;
#[derive(Debug)]
pub struct NativeRootStore {
roots: RefCell<Vec<GcRef>>,
}
impl NativeRootStore {
#[must_use]
pub fn new() -> NativeRootStore {
NativeRootStore {
roots: RefCell::new(Vec::with_capacity(NATIVE_ROOT_RESERVATION)),
}
}
#[must_use]
pub fn len(&self) -> usize {
self.roots.borrow().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn capacity(&self) -> usize {
self.roots.borrow().capacity()
}
#[inline]
fn push(&self, r: GcRef) {
self.roots.borrow_mut().push(r);
}
#[inline]
fn truncate(&self, watermark: usize) {
self.roots.borrow_mut().truncate(watermark);
}
pub(crate) fn reset(&mut self) {
self.roots.get_mut().clear();
}
}
impl Default for NativeRootStore {
fn default() -> Self {
Self::new()
}
}
impl RootSet for NativeRootStore {
fn push_roots(&self, out: &mut Vec<GcRef>) {
out.extend_from_slice(&self.roots.borrow());
}
}
#[derive(Clone, Copy, Debug)]
pub struct Rooted<'s> {
r: GcRef,
_scope: PhantomData<&'s ()>,
}
impl Rooted<'_> {
#[inline]
#[must_use]
pub fn get(self) -> GcRef {
self.r
}
}
pub struct NativeScope<'c> {
store: *const NativeRootStore,
watermark: usize,
_ctx: PhantomData<&'c mut RuntimeContext>,
}
impl<'c> NativeScope<'c> {
#[must_use]
pub unsafe fn new(ctx: *mut RuntimeContext) -> NativeScope<'c> {
let store: *const NativeRootStore = if ctx.is_null() {
std::ptr::null()
} else {
unsafe { (*ctx).native_roots }
};
let watermark = match unsafe { store.as_ref() } {
Some(store) => store.len(),
None => 0,
};
NativeScope {
store,
watermark,
_ctx: PhantomData,
}
}
#[inline]
pub fn root(&self, r: GcRef) -> Rooted<'_> {
if let Some(store) = unsafe { self.store.as_ref() } {
store.push(r);
}
Rooted {
r,
_scope: PhantomData,
}
}
#[must_use]
pub fn root_count(&self) -> usize {
match unsafe { self.store.as_ref() } {
Some(store) => store.len() - self.watermark,
None => 0,
}
}
}
impl Drop for NativeScope<'_> {
fn drop(&mut self) {
if let Some(store) = unsafe { self.store.as_ref() } {
store.truncate(self.watermark);
}
}
}
pub struct RuntimeRoots<'a> {
shadow: Option<&'a crate::ShadowStackHeader>,
input: Option<GcRef>,
parse_partial: Option<GcRef>,
snapshot: Option<&'a crate::CrashSnapshot>,
native: Option<&'a NativeRootStore>,
debug: Option<DebugArm<'a>>,
}
#[derive(Clone, Copy)]
struct DebugArm<'a> {
frames: &'a crate::DebugFrameStackHeader,
values: &'a crate::DebugValueStackHeader,
}
impl<'a> RuntimeRoots<'a> {
#[must_use]
pub unsafe fn from_context(ctx: *mut RuntimeContext) -> RuntimeRoots<'a> {
if ctx.is_null() {
return RuntimeRoots {
shadow: None,
input: None,
parse_partial: None,
snapshot: None,
native: None,
debug: None,
};
}
let c = unsafe { &*ctx };
RuntimeRoots {
shadow: unsafe { c.shadow.as_ref() },
input: Some(c.input_source),
parse_partial: unsafe { c.parse_detail.as_ref() }
.and_then(|d| d.fail.as_ref())
.and_then(|f| f.partial),
snapshot: unsafe { c.crash_snapshot.as_ref() }.and_then(|s| s.get()),
native: unsafe { c.native_roots.as_ref() },
debug: unsafe { c.debug_frames.as_ref() }
.zip(unsafe { c.debug_values.as_ref() })
.map(|(frames, values)| DebugArm { frames, values }),
}
}
}
impl RootSet for RuntimeRoots<'_> {
fn push_roots(&self, out: &mut Vec<GcRef>) {
let RuntimeRoots {
shadow,
input,
parse_partial,
snapshot,
native,
debug,
} = self;
if let Some(shadow) = shadow {
shadow.push_roots(out);
}
out.extend(input.iter().copied());
out.extend(parse_partial.iter().copied());
if let Some(snapshot) = snapshot {
snapshot.push_roots(out);
}
if let Some(native) = native {
native.push_roots(out);
}
let _ = debug;
}
}
impl WeakSet for RuntimeRoots<'_> {
fn clear_reclaimed(&self) -> usize {
let Some(arm) = self.debug else {
return 0;
};
unsafe { arm.frames.clear_reclaimed(arm.values) }
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ptr::NonNull;
fn dummy_ref(n: usize) -> GcRef {
let header = Box::leak(Box::new(crate::GcHeader::detached()));
let nn = NonNull::from(header);
let r = unsafe { GcRef::from_non_null(nn) };
let _ = n;
r
}
#[test]
fn empty_scope_has_no_roots() {
let scope = RootScope::new();
let mut out = Vec::new();
scope.push_roots(&mut out);
assert!(out.is_empty());
}
#[test]
fn scope_yields_its_roots() {
let mut scope = RootScope::new();
let a = dummy_ref(1);
let b = dummy_ref(2);
scope.root(a);
scope.root(b);
let mut out = Vec::new();
scope.push_roots(&mut out);
assert_eq!(out.len(), 2);
assert!(out.contains(&a));
assert!(out.contains(&b));
}
#[test]
fn child_scope_chains_to_parent() {
let mut parent = RootScope::new();
let a = dummy_ref(1);
parent.root(a);
let mut child = RootScope::child(&parent);
let b = dummy_ref(2);
child.root(b);
let mut out = Vec::new();
child.push_roots(&mut out);
assert_eq!(out.len(), 2);
assert!(out.contains(&a));
assert!(out.contains(&b));
}
struct Native {
rt: Box<crate::Runtime>,
ctx: Box<RuntimeContext>,
}
impl Native {
fn new() -> Native {
let mut rt = Box::new(crate::Runtime::new());
let ctx = Box::new(rt.context());
Native { rt, ctx }
}
fn ctx_ptr(&mut self) -> *mut RuntimeContext {
&mut *self.ctx
}
fn store(&self) -> &NativeRootStore {
self.rt.native_root_store()
}
fn native_roots(&mut self) -> Vec<GcRef> {
let ctx = self.ctx_ptr();
let roots = unsafe { RuntimeRoots::from_context(ctx) };
let mut out = Vec::new();
roots.push_roots(&mut out);
out
}
}
fn heap_ref(rt: &crate::Runtime, value: i64) -> GcRef {
rt.heap().alloc_unpaced(crate::scalars::INT_PAYLOAD, value)
}
#[test]
fn a_scope_claims_the_tail_and_drops_exactly_what_it_claimed() {
let mut f = Native::new();
let a = heap_ref(&f.rt, 1);
let b = heap_ref(&f.rt, 2);
assert!(
f.store().is_empty(),
"a fresh runtime holds no native roots"
);
{
let ctx = f.ctx_ptr();
let scope = unsafe { NativeScope::new(ctx) };
scope.root(a);
scope.root(b);
assert_eq!(scope.root_count(), 2);
assert_eq!(f.store().len(), 2);
let found = f.native_roots();
assert!(found.contains(&a) && found.contains(&b));
}
assert!(f.store().is_empty(), "the scope released its whole run");
assert!(f.native_roots().iter().all(|r| *r != a && *r != b));
}
#[test]
fn nested_scopes_partition_one_contiguous_run() {
let mut f = Native::new();
let a = heap_ref(&f.rt, 1);
let b = heap_ref(&f.rt, 2);
let ctx = f.ctx_ptr();
unsafe {
let outer = NativeScope::new(ctx);
outer.root(a);
{
let inner = NativeScope::new(ctx);
inner.root(b);
assert_eq!(inner.root_count(), 1);
assert_eq!(f.store().len(), 2, "one run holds both scopes");
let found = f.native_roots();
assert!(found.contains(&a) && found.contains(&b));
}
assert_eq!(
f.store().len(),
1,
"the inner pop restores the outer scope's extent"
);
assert!(!f.native_roots().contains(&b));
drop(outer);
}
assert!(f.store().is_empty());
}
#[test]
fn a_scope_survives_the_growth_its_own_roots_force() {
let mut f = Native::new();
let refs: Vec<GcRef> = (0..(NATIVE_ROOT_RESERVATION as i64 + 64))
.map(|n| heap_ref(&f.rt, n))
.collect();
let ctx = f.ctx_ptr();
let scope = unsafe { NativeScope::new(ctx) };
assert_eq!(f.store().capacity(), NATIVE_ROOT_RESERVATION);
for r in &refs {
scope.root(*r);
}
assert!(
f.store().capacity() > NATIVE_ROOT_RESERVATION,
"the reservation was not actually exceeded, so this test proves \
nothing: capacity is still {}",
f.store().capacity()
);
assert_eq!(scope.root_count(), refs.len());
let found = f.native_roots();
let native: Vec<GcRef> = found[found.len() - refs.len()..].to_vec();
assert_eq!(native, refs);
drop(scope);
assert!(f.store().is_empty());
}
#[test]
fn an_inner_scopes_growth_leaves_the_outer_scopes_watermark_valid() {
let mut f = Native::new();
let outer_refs: Vec<GcRef> = (0..3).map(|n| heap_ref(&f.rt, n)).collect();
let inner_refs: Vec<GcRef> = (0..(NATIVE_ROOT_RESERVATION as i64 + 8))
.map(|n| heap_ref(&f.rt, 1_000 + n))
.collect();
let ctx = f.ctx_ptr();
unsafe {
let outer = NativeScope::new(ctx);
for r in &outer_refs {
outer.root(*r);
}
{
let inner = NativeScope::new(ctx);
for r in &inner_refs {
inner.root(*r);
}
assert!(f.store().capacity() > NATIVE_ROOT_RESERVATION);
}
assert_eq!(
f.store().len(),
outer_refs.len(),
"the inner scope released exactly its own run across a growth"
);
let found = f.native_roots();
for r in &outer_refs {
assert!(found.contains(r), "the outer scope lost a root");
}
for r in &inner_refs {
assert!(!found.contains(r), "a released root is still scanned");
}
drop(outer);
}
assert!(f.store().is_empty());
}
#[test]
fn a_rooted_handed_out_before_a_growth_still_names_its_object() {
let mut f = Native::new();
let first = heap_ref(&f.rt, 7);
let filler: Vec<GcRef> = (0..(NATIVE_ROOT_RESERVATION as i64))
.map(|n| heap_ref(&f.rt, n))
.collect();
let ctx = f.ctx_ptr();
let scope = unsafe { NativeScope::new(ctx) };
let rooted = scope.root(first);
for r in &filler {
scope.root(*r);
}
assert!(f.store().capacity() > NATIVE_ROOT_RESERVATION);
assert_eq!(rooted.get(), first, "the proof still names its object");
assert!(f.native_roots().contains(&first));
}
#[test]
fn a_scope_dropped_out_of_order_cannot_raise_the_watermark() {
let mut f = Native::new();
let a = heap_ref(&f.rt, 1);
let b = heap_ref(&f.rt, 2);
let ctx = f.ctx_ptr();
let (outer, inner) = unsafe {
let outer = NativeScope::new(ctx);
outer.root(a);
let inner = NativeScope::new(ctx);
inner.root(b);
(outer, inner)
};
drop(outer);
assert_eq!(f.store().len(), 0, "the outer release took both runs");
drop(inner);
assert_eq!(
f.store().len(),
0,
"the late inner release restored a watermark above the length and \
the store stayed where it was"
);
assert!(f.native_roots().iter().all(|r| *r != a && *r != b));
}
#[test]
fn a_scope_on_a_null_context_roots_nothing_and_drops_cleanly() {
let mut rt = crate::Runtime::new();
let a = heap_ref(&rt, 1);
let scope = unsafe { NativeScope::new(std::ptr::null_mut()) };
assert_eq!(scope.root(a).get(), a);
assert_eq!(scope.root_count(), 0);
drop(scope);
assert!(rt.native_root_store().is_empty());
assert!(
!rt.context().native_roots.is_null(),
"a wired context is the case that does have a store"
);
}
#[test]
fn every_context_this_runtime_mints_sees_the_same_store() {
let mut f = Native::new();
let a = heap_ref(&f.rt, 42);
let ctx = f.ctx_ptr();
let scope = unsafe { NativeScope::new(ctx) };
scope.root(a);
let mut fresh = f.rt.context();
let roots = unsafe { RuntimeRoots::from_context(&mut fresh) };
let mut out = Vec::new();
roots.push_roots(&mut out);
assert!(
out.contains(&a),
"a freshly minted context could not see the open scope"
);
}
#[test]
fn a_native_root_survives_the_collection_that_reclaims_its_neighbour() {
let mut f = Native::new();
let kept = heap_ref(&f.rt, 111);
let dropped = heap_ref(&f.rt, 222);
let before = f.rt.heap().stats().live_count;
assert!(before >= 2);
let ctx = f.ctx_ptr();
let scope = unsafe { NativeScope::new(ctx) };
let rooted = scope.root(kept);
f.rt.collect_now();
assert!(
f.rt.heap().stats().live_count < before,
"nothing was reclaimed, so this test cannot distinguish the arms"
);
assert!(
f.native_roots().contains(&kept),
"the scope's root did not survive its own collection"
);
assert_eq!(rooted.get(), kept);
let _ = dropped;
drop(scope);
}
#[test]
fn the_reservation_is_a_reservation_and_not_a_bound() {
let store = NativeRootStore::new();
assert_eq!(store.capacity(), NATIVE_ROOT_RESERVATION);
assert!(store.is_empty());
}
#[test]
fn the_debug_arm_contributes_no_strong_roots() {
let mut rt = crate::Runtime::new();
let value = rt.heap().alloc_unpaced(crate::scalars::INT_PAYLOAD, 9_999);
let mut ctx = Box::new(rt.context());
let name = b"x";
let locals = [crate::DebugLocalMeta {
callee_name: std::ptr::null(),
callee_name_len: 0,
source_name: name.as_ptr(),
name_len: 1,
symbol_id: 1,
descriptor: &crate::scalars::INT,
type_id: 1,
kind: crate::LOCAL_KIND_USER,
span_start: 0,
span_end: 0,
slot_kind: crate::debug::DebugSlotKind::Reference,
}];
let meta = crate::FunctionDebugMeta {
func_name: b"f".as_ptr(),
func_name_len: 1,
local_count: 1,
locals: locals.as_ptr(),
span_start: 0,
span_end: 0,
};
let mut guard = unsafe { crate::debug::push_frame(&mut *ctx, &meta) };
guard.set(0, value);
assert_eq!(guard.values()[0], Some(value), "the debugger names it");
let roots = unsafe { RuntimeRoots::from_context(&mut *ctx) };
let mut out = Vec::new();
roots.push_roots(&mut out);
assert!(
!out.contains(&value),
"the debug slot put a value in the collector's strong set — that is \
the ADR-044 set-merge, arriving as one line in `push_roots`"
);
drop(guard);
}
}