use crate::gc::{GcHeader, GcRef};
use crate::roots::RootSet;
use crate::{
FRAME_BYTES_BASE, FRAME_BYTES_PER_SLOT, MAX_RECURSION_DEPTH, REFERENCE_FRAME_SLOTS,
STACK_BUDGET_BYTES,
};
pub const MAX_SHADOW_SLOTS: usize = 192;
pub const MAX_DEBUG_VALUE_SLOTS: usize = 4096;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SlotCount(u32);
impl SlotCount {
#[must_use]
pub const fn new(n: u32) -> Option<SlotCount> {
if n as usize <= MAX_SHADOW_SLOTS {
Some(SlotCount(n))
} else {
None
}
}
#[must_use]
pub const fn get(self) -> u32 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DebugSlotCount(u32);
impl DebugSlotCount {
#[must_use]
pub const fn new(n: u32) -> Option<DebugSlotCount> {
if n as usize <= MAX_DEBUG_VALUE_SLOTS {
Some(DebugSlotCount(n))
} else {
None
}
}
#[must_use]
pub const fn get(self) -> u32 {
self.0
}
}
pub const SHADOW_STACK_SLOTS: usize = (STACK_BUDGET_BYTES / FRAME_BYTES_PER_SLOT) as usize
+ (MAX_RECURSION_DEPTH * REFERENCE_FRAME_SLOTS) as usize
+ MAX_SHADOW_SLOTS;
pub(crate) const MAX_LIVE_SLOTS: usize = (STACK_BUDGET_BYTES / FRAME_BYTES_PER_SLOT) as usize
+ (MAX_RECURSION_DEPTH * REFERENCE_FRAME_SLOTS) as usize;
const _: () = assert!(
SHADOW_STACK_SLOTS > MAX_LIVE_SLOTS,
"the shadow stack must cover every slot the budget can buy, plus one frame \
of headroom for Rust-side pushes"
);
const _: () = assert!(
FRAME_BYTES_BASE > 0 && FRAME_BYTES_PER_SLOT > 0,
"a frame and a slot must each spend budget, or the reservation argument in \
SHADOW_STACK_SLOTS does not close"
);
#[repr(C)]
pub struct SlotStackHeader<T: Copy> {
top: *mut T,
base: *mut T,
limit: *mut T,
}
impl<T: Copy> SlotStackHeader<T> {
pub const TOP_OFFSET: i32 = core::mem::offset_of!(Self, top) as i32;
pub(crate) unsafe fn claim(&mut self, n: usize, zero: T) -> *mut T {
let base = self.top;
let new_top = unsafe { base.add(n) };
assert!(
new_top <= self.limit,
"slot stack exhausted: {n} more slots do not fit"
);
unsafe { std::slice::from_raw_parts_mut(base, n) }.fill(zero);
self.top = new_top;
base
}
pub(crate) fn restore(&mut self, base: *mut T) {
self.top = base;
}
#[must_use]
pub fn claimed(&self) -> &[T] {
self.live_slots()
}
fn live_slots(&self) -> &[T] {
debug_assert!(self.base <= self.top && self.top <= self.limit);
unsafe {
let len = self.top.offset_from(self.base) as usize;
std::slice::from_raw_parts(self.base, len)
}
}
#[must_use]
pub fn len(&self) -> usize {
self.live_slots().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub struct SlotStack<T: Copy> {
header: Box<SlotStackHeader<T>>,
slots: Box<[T]>,
}
impl<T: Copy> SlotStack<T> {
#[must_use]
pub fn new(capacity: usize, zero: T) -> Self {
let mut slots: Box<[T]> = vec![zero; capacity].into_boxed_slice();
let base = slots.as_mut_ptr();
let limit = unsafe { base.add(capacity) };
SlotStack {
header: Box::new(SlotStackHeader {
top: base,
base,
limit,
}),
slots,
}
}
pub fn header_ptr(&mut self) -> *mut SlotStackHeader<T> {
&mut *self.header
}
#[must_use]
pub fn header(&self) -> &SlotStackHeader<T> {
&self.header
}
pub fn reset(&mut self) {
self.header.top = self.header.base;
}
#[must_use]
pub fn len(&self) -> usize {
self.header.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.header.is_empty()
}
#[must_use]
pub fn capacity(&self) -> usize {
self.slots.len()
}
}
pub type ShadowStack = SlotStack<*mut GcHeader>;
pub type ShadowStackHeader = SlotStackHeader<*mut GcHeader>;
const _: () = assert!(ShadowStackHeader::TOP_OFFSET == 0);
impl RootSet for ShadowStackHeader {
fn push_roots(&self, out: &mut Vec<GcRef>) {
out.extend(self.live_slots().iter().copied().filter_map(|p| {
std::ptr::NonNull::new(p).map(|nn| unsafe { GcRef::from_non_null(nn) })
}));
}
}
pub struct ShadowFrameGuard {
header: *mut ShadowStackHeader,
base: *mut *mut GcHeader,
count: u32,
}
impl ShadowFrameGuard {
pub fn set(&mut self, index: usize, r: GcRef) {
assert!(
index < self.count as usize,
"shadow slot {index} is outside a {}-slot frame",
self.count
);
unsafe { *self.base.add(index) = r.as_ptr() };
}
pub fn clear(&mut self, index: usize) {
assert!(
index < self.count as usize,
"shadow slot {index} is outside a {}-slot frame",
self.count
);
unsafe { *self.base.add(index) = std::ptr::null_mut() };
}
#[must_use]
pub fn base_ptr(&self) -> *mut *mut GcHeader {
self.base
}
}
impl Drop for ShadowFrameGuard {
fn drop(&mut self) {
unsafe { (*self.header).restore(self.base) };
}
}
#[must_use]
pub unsafe fn push_frame(ctx: *mut crate::RuntimeContext, count: SlotCount) -> ShadowFrameGuard {
assert!(!ctx.is_null(), "push_frame needs a wired context");
let header = unsafe { (*ctx).shadow };
assert!(
!header.is_null(),
"push_frame needs a context from `Runtime::context`, not a placeholder"
);
let n = count.get() as usize;
let base = unsafe { (*header).claim(n, std::ptr::null_mut()) };
ShadowFrameGuard {
header,
base,
count: count.get(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ptr::NonNull;
fn dummy_ref() -> GcRef {
let header = Box::leak(Box::new(GcHeader::detached()));
unsafe { GcRef::from_non_null(NonNull::from(header)) }
}
struct Fixture {
stack: ShadowStack,
ctx: Box<crate::RuntimeContext>,
}
impl Fixture {
fn new() -> Fixture {
let mut stack = ShadowStack::new(SHADOW_STACK_SLOTS, std::ptr::null_mut());
let mut ctx = Box::new(unsafe { crate::RuntimeContext::placeholder(dummy_ref()) });
ctx.shadow = stack.header_ptr();
Fixture { stack, ctx }
}
fn ctx_ptr(&mut self) -> *mut crate::RuntimeContext {
&mut *self.ctx
}
fn roots(&self) -> Vec<GcRef> {
let mut out = Vec::new();
self.stack.header().push_roots(&mut out);
out
}
}
#[test]
fn an_empty_stack_roots_nothing() {
let f = Fixture::new();
assert!(f.roots().is_empty());
assert!(f.stack.is_empty());
}
#[test]
fn a_frame_yields_written_slots_only() {
let mut f = Fixture::new();
let a = dummy_ref();
let b = dummy_ref();
let ctx = f.ctx_ptr();
let mut guard = unsafe { push_frame(ctx, SlotCount::new(3).unwrap()) };
guard.set(0, a);
guard.set(2, b); let out = f.roots();
assert_eq!(out.len(), 2);
assert_eq!(out[0].as_ptr(), a.as_ptr());
assert_eq!(out[1].as_ptr(), b.as_ptr());
drop(guard);
}
#[test]
fn nested_frames_are_one_contiguous_scan() {
let mut f = Fixture::new();
let a = dummy_ref();
let b = dummy_ref();
let ctx = f.ctx_ptr();
let (outer, inner) = unsafe {
let mut outer = push_frame(ctx, SlotCount::new(1).unwrap());
outer.set(0, a);
let mut inner = push_frame(ctx, SlotCount::new(1).unwrap());
inner.set(0, b);
(outer, inner)
};
let out = f.roots();
assert_eq!(out.len(), 2);
assert!(out.iter().any(|r| r.as_ptr() == a.as_ptr()));
assert!(out.iter().any(|r| r.as_ptr() == b.as_ptr()));
drop(inner);
drop(outer);
}
#[test]
fn a_popped_frames_slots_are_not_scanned() {
let mut f = Fixture::new();
let a = dummy_ref();
let ctx = f.ctx_ptr();
let base = unsafe {
let mut guard = push_frame(ctx, SlotCount::new(1).unwrap());
guard.set(0, a);
let base = guard.base_ptr();
drop(guard);
base
};
assert_eq!(unsafe { *base }, a.as_ptr(), "the slot memory is untouched");
assert!(f.roots().is_empty(), "but it is outside [base, top)");
}
#[test]
fn pushing_and_popping_restores_the_top() {
let mut f = Fixture::new();
let ctx = f.ctx_ptr();
unsafe {
let outer = push_frame(ctx, SlotCount::new(4).unwrap());
assert_eq!(f.stack.len(), 4);
{
let inner = push_frame(ctx, SlotCount::new(7).unwrap());
assert_eq!(f.stack.len(), 11);
drop(inner);
}
assert_eq!(f.stack.len(), 4, "an inner pop restores the outer extent");
drop(outer);
}
assert!(f.stack.is_empty(), "every push is balanced by a pop");
}
#[test]
fn a_zero_slot_frame_moves_nothing() {
let mut f = Fixture::new();
let ctx = f.ctx_ptr();
unsafe {
let guards: Vec<ShadowFrameGuard> = (0..1000)
.map(|_| push_frame(ctx, SlotCount::new(0).unwrap()))
.collect();
assert!(f.stack.is_empty(), "1000 frames claimed nothing");
drop(guards);
}
assert!(f.stack.is_empty());
}
#[test]
fn rejects_an_oversized_frame() {
assert!(SlotCount::new(MAX_SHADOW_SLOTS as u32).is_some());
assert!(SlotCount::new((MAX_SHADOW_SLOTS + 1) as u32).is_none());
}
#[test]
fn the_reservation_covers_every_slot_the_budget_can_buy() {
for width in [0u32, 1, 7, 64, MAX_SHADOW_SLOTS as u32] {
let per_frame = crate::frame_cost(width);
let frames = STACK_BUDGET_BYTES / per_frame;
let slots = frames as usize * width as usize;
assert!(
slots <= MAX_LIVE_SLOTS,
"a stack of {frames} frames {width} slots wide claims {slots} \
slots, past the {MAX_LIVE_SLOTS}-slot bound"
);
}
let stack = ShadowStack::new(SHADOW_STACK_SLOTS, std::ptr::null_mut());
assert_eq!(stack.capacity(), SHADOW_STACK_SLOTS);
}
#[test]
fn a_wide_frame_spends_more_budget_than_a_narrow_one() {
let reference = STACK_BUDGET_BYTES / crate::frame_cost(REFERENCE_FRAME_SLOTS);
let widest = STACK_BUDGET_BYTES / crate::frame_cost(MAX_SHADOW_SLOTS as u32);
assert_eq!(
STACK_BUDGET_BYTES / crate::frame_cost(0),
MAX_RECURSION_DEPTH,
"the cheapest frame there is must not buy more calls than the debug \
frame stack has entries — that stack is sized MAX_RECURSION_DEPTH + 1"
);
assert_eq!(
reference, MAX_RECURSION_DEPTH,
"a reference-width frame reaches exactly the depth the old call \
count allowed, so an ordinary recursive program is unaffected"
);
assert!(
widest * 3 < reference,
"the widest legal frame must be several times dearer than the \
reference one: {widest} vs {reference}"
);
}
#[test]
fn a_budget_larger_than_the_reservation_cannot_be_built() {
assert!(crate::StackBudget::new(STACK_BUDGET_BYTES).is_some());
assert!(crate::StackBudget::new(STACK_BUDGET_BYTES + 1).is_none());
assert_eq!(crate::StackBudget::DEFAULT.get(), STACK_BUDGET_BYTES);
}
}