use super::VMArrayRef;
use super::trace_info::{TraceInfo, TraceInfos};
use crate::runtime::vm::{
ExternRefHostDataId, ExternRefHostDataTable, GarbageCollection, GcHeap, GcHeapObject,
GcProgress, GcRootsIter, GcRuntime, SendSyncUnsafeCell, TypedGcRef, VMExternRef, VMGcHeader,
VMGcRef, VMMemoryDefinition,
};
use crate::{Engine, bail_bug, prelude::*};
use core::{
alloc::Layout, any::Any, mem, num::NonZeroU32, ptr::NonNull, sync::atomic::AtomicUsize,
};
use wasmtime_environ::copying::{
ALIGN, ARRAY_LENGTH_OFFSET, CopyingTypeLayouts, HEADER_COPIED_BIT, HEADER_SIZE, InlineTraceInfo,
};
use wasmtime_environ::{
GcArrayLayout, GcStructLayout, GcTypeLayouts, POISON, VMGcKind, VMSharedTypeIndex, gc_assert,
};
#[expect(clippy::cast_possible_truncation, reason = "known to not overflow")]
const GC_REF_ARRAY_ELEMS_OFFSET: u32 = ARRAY_LENGTH_OFFSET + (mem::size_of::<u32>() as u32);
#[derive(Default)]
pub struct CopyingCollector {
layouts: CopyingTypeLayouts,
}
unsafe impl GcRuntime for CopyingCollector {
fn layouts(&self) -> &dyn GcTypeLayouts {
&self.layouts
}
fn new_gc_heap(&self, engine: &Engine) -> Result<Box<dyn GcHeap>> {
let heap = CopyingHeap::new(engine)?;
Ok(Box::new(heap) as _)
}
}
#[repr(C)]
struct VMCopyingHeader {
header: VMGcHeader,
object_size: u32,
}
unsafe impl GcHeapObject for VMCopyingHeader {
#[inline]
fn is(_header: &VMGcHeader) -> bool {
true
}
}
impl VMCopyingHeader {
#[inline]
fn object_size(&self) -> u32 {
self.object_size
}
#[inline]
fn copied(&self) -> bool {
self.header.reserved_u26() & HEADER_COPIED_BIT != 0
}
#[inline]
fn set_copied(&mut self) {
let reserved = self.header.reserved_u26();
self.header.set_reserved_u26(reserved | HEADER_COPIED_BIT);
}
#[inline]
fn inline_trace_info(&self) -> InlineTraceInfo {
InlineTraceInfo::decode(self.header.reserved_u26())
}
}
#[repr(C)]
struct VMCopyingHeaderAndForwardingRef {
header: VMCopyingHeader,
forwarding_ref: Option<VMGcRef>,
}
unsafe impl GcHeapObject for VMCopyingHeaderAndForwardingRef {
#[inline]
fn is(_header: &VMGcHeader) -> bool {
true
}
}
impl VMCopyingHeaderAndForwardingRef {
fn forwarding_ref(&self) -> Result<Option<VMGcRef>> {
debug_assert!(
self.header.object_size()
>= u32::try_from(mem::size_of::<VMCopyingHeaderAndForwardingRef>()).unwrap()
);
Ok(if self.header.copied() {
match self.forwarding_ref.as_ref() {
Some(r) => Some(r.unchecked_copy()),
None => bail_bug!("should always have a forwarding ref if the copied bit is set"),
}
} else {
None
})
}
fn set_forwarding_ref(&mut self, forwarding_ref: VMGcRef) {
debug_assert!(!self.header.copied());
debug_assert!(
self.header.object_size()
>= u32::try_from(mem::size_of::<VMCopyingHeaderAndForwardingRef>()).unwrap()
);
self.header.set_copied();
self.forwarding_ref = Some(forwarding_ref);
}
}
#[repr(C)]
struct VMCopyingArrayHeader {
header: VMCopyingHeader,
length: u32,
}
unsafe impl GcHeapObject for VMCopyingArrayHeader {
#[inline]
fn is(header: &VMGcHeader) -> bool {
header.kind() == VMGcKind::ArrayRef
}
}
#[repr(C)]
struct VMCopyingExternRef {
header: VMCopyingHeaderAndForwardingRef,
host_data: ExternRefHostDataId,
next_extern_ref: Option<VMExternRef>,
}
unsafe impl GcHeapObject for VMCopyingExternRef {
#[inline]
fn is(header: &VMGcHeader) -> bool {
header.kind() == VMGcKind::ExternRef
}
}
#[derive(Default)]
#[repr(C)]
struct VMCopyingHeapDataInner {
bump_ptr: u32,
active_space_end: u32,
}
#[derive(Default)]
#[repr(transparent)]
struct VMCopyingHeapData {
inner: SendSyncUnsafeCell<VMCopyingHeapDataInner>,
}
impl VMCopyingHeapData {
fn bump_ptr(&self) -> u32 {
unsafe { (*self.inner.get()).bump_ptr }
}
fn set_bump_ptr(&mut self, val: u32) {
self.inner.get_mut().bump_ptr = val;
}
fn active_space_end(&self) -> u32 {
unsafe { (*self.inner.get()).active_space_end }
}
fn set_active_space_end(&mut self, val: u32) {
self.inner.get_mut().active_space_end = val;
}
}
fn copying_ref(gc_ref: &VMGcRef) -> &TypedGcRef<VMCopyingHeader> {
debug_assert!(!gc_ref.is_i31());
gc_ref.as_typed_unchecked()
}
fn header_and_forwarding_ref(gc_ref: &VMGcRef) -> &TypedGcRef<VMCopyingHeaderAndForwardingRef> {
debug_assert!(!gc_ref.is_i31());
gc_ref.as_typed_unchecked()
}
fn externref_to_copying(externref: &VMExternRef) -> &TypedGcRef<VMCopyingExternRef> {
let gc_ref = externref.as_gc_ref();
debug_assert!(!gc_ref.is_i31());
gc_ref.as_typed_unchecked()
}
struct CopyingHeap {
trace_infos: TraceInfos,
no_gc_count: u64,
memory: Option<crate::vm::Memory>,
vmmemory: Option<VMMemoryDefinition>,
vmctx_data: VMCopyingHeapData,
active_space_start: u32,
idle_space_start: u32,
idle_space_end: u32,
worklist_ptr: u32,
active_extern_ref_set_head: Option<VMExternRef>,
idle_extern_ref_set_head: Option<VMExternRef>,
}
impl CopyingHeap {
fn new(engine: &Engine) -> Result<Self> {
log::trace!("allocating new copying heap");
Ok(Self {
trace_infos: TraceInfos::new(engine, GC_REF_ARRAY_ELEMS_OFFSET),
no_gc_count: 0,
memory: None,
vmmemory: None,
vmctx_data: VMCopyingHeapData::default(),
active_space_start: 0,
idle_space_start: 0,
idle_space_end: 0,
worklist_ptr: 0,
active_extern_ref_set_head: None,
idle_extern_ref_set_head: None,
})
}
fn bump_ptr(&self) -> u32 {
self.vmctx_data.bump_ptr()
}
fn set_bump_ptr(&mut self, val: u32) {
self.vmctx_data.set_bump_ptr(val);
}
fn active_space_end(&self) -> u32 {
self.vmctx_data.active_space_end()
}
fn set_active_space_end(&mut self, val: u32) {
self.vmctx_data.set_active_space_end(val);
}
fn capacity(&self) -> u32 {
let len = self.vmmemory.as_ref().unwrap().current_length();
let len = u32::try_from(len).unwrap_or(u32::MAX);
let len = len & !(ALIGN - 1);
len
}
fn initialize_semi_spaces(&mut self) {
debug_assert_eq!(self.active_space_start, 0);
debug_assert_eq!(self.active_space_end(), 0);
debug_assert_eq!(self.idle_space_start, 0);
debug_assert_eq!(self.idle_space_end, 0);
debug_assert_eq!(self.bump_ptr(), 0);
self.resize_semi_spaces();
self.reset_bump_ptr();
}
fn resize_semi_spaces(&mut self) {
debug_assert_eq!(
self.active_space_end() - self.active_space_start,
self.idle_space_end - self.idle_space_start,
"the active and idle spaces should be the same size"
);
if self.idle_space_start < self.active_space_start {
return;
}
let capacity = self.capacity();
let halfway = capacity / 2;
debug_assert!(self.bump_ptr() <= halfway);
debug_assert!(self.idle_space_start <= halfway);
debug_assert!(self.active_space_end() <= halfway);
self.set_active_space_end(halfway);
self.idle_space_start = halfway;
self.idle_space_end = capacity;
debug_assert_eq!(
self.active_space_end() - self.active_space_start,
self.idle_space_end - self.idle_space_start,
"the active and idle spaces should be the same size"
);
}
fn reset_bump_ptr(&mut self) {
let mut bp = self.active_space_start;
if self.active_space_end() - self.active_space_start >= ALIGN {
bp += ALIGN;
}
self.set_bump_ptr(bp);
debug_assert!(self.bump_ptr().is_multiple_of(ALIGN));
}
fn allocate(&mut self, size: u32) -> Option<u32> {
debug_assert!(size.is_multiple_of(ALIGN));
debug_assert!(self.bump_ptr().is_multiple_of(ALIGN));
debug_assert!(self.bump_ptr() >= self.active_space_start);
debug_assert!(self.bump_ptr() <= self.active_space_end());
let result = self.bump_ptr();
let new_bump_ptr = result.checked_add(size)?;
if new_bump_ptr > self.active_space_end() {
return None;
}
self.set_bump_ptr(new_bump_ptr);
debug_assert!(self.bump_ptr().is_multiple_of(ALIGN));
debug_assert!(self.bump_ptr() >= self.active_space_start);
debug_assert!(self.bump_ptr() <= self.active_space_end());
Some(result)
}
fn is_in_active_space(&self, index: u32) -> bool {
index >= self.active_space_start && index < self.active_space_end()
}
fn is_in_idle_space(&self, index: u32) -> bool {
index >= self.idle_space_start && index < self.idle_space_end
}
fn flip(&mut self) {
debug_assert_eq!(
self.active_space_end() - self.active_space_start,
self.idle_space_end - self.idle_space_start,
"the active and idle spaces should be the same size"
);
mem::swap(&mut self.active_space_start, &mut self.idle_space_start);
let new_active_space_end = self.idle_space_end;
self.idle_space_end = self.active_space_end();
self.set_active_space_end(new_active_space_end);
self.reset_bump_ptr();
self.idle_extern_ref_set_head = self.active_extern_ref_set_head.take();
}
fn initialize_worklist(&mut self) {
self.worklist_ptr = self.bump_ptr();
}
fn worklist_pop(&mut self) -> Result<Option<VMGcRef>> {
debug_assert!(
self.is_in_active_space(self.worklist_ptr)
|| self.worklist_ptr == self.active_space_end()
);
debug_assert!(
self.is_in_active_space(self.bump_ptr()) || self.bump_ptr() == self.active_space_end()
);
debug_assert!(self.worklist_ptr <= self.bump_ptr());
if self.worklist_ptr == self.bump_ptr() {
return Ok(None);
}
let result = self.worklist_ptr;
let result = match NonZeroU32::new(result) {
Some(result) => result,
None => bail_bug!("worklist ptr is zero"),
};
let result = match VMGcRef::from_heap_index(result) {
Some(result) => result,
None => bail_bug!("corrupt worklist_ptr"),
};
let obj_size = self.index(copying_ref(&result))?.object_size();
self.worklist_ptr += obj_size;
debug_assert!(self.worklist_ptr <= self.bump_ptr());
Ok(Some(result))
}
fn worklist_insert(&self, gc_ref: &VMGcRef) -> Result<()> {
if !cfg!(debug_assertions) {
return Ok(());
}
let index = gc_ref.heap_index()?.get();
debug_assert!(self.is_in_active_space(index));
let obj_size = self.index(copying_ref(gc_ref))?.object_size();
debug_assert_eq!(index + obj_size, self.bump_ptr());
debug_assert!(self.worklist_ptr <= index);
Ok(())
}
fn forward(&mut self, from_ref: &VMGcRef) -> Result<VMGcRef> {
debug_assert!(!from_ref.is_i31());
debug_assert!(self.is_in_idle_space(from_ref.heap_index()?.get()));
if let Some(to_ref) = self
.index(header_and_forwarding_ref(from_ref))?
.forwarding_ref()?
{
return Ok(to_ref);
}
self.copy(from_ref)
}
fn copy(&mut self, from_ref: &VMGcRef) -> Result<VMGcRef> {
debug_assert!(!from_ref.is_i31());
let from_index = from_ref.heap_index()?.get();
debug_assert!(self.is_in_idle_space(from_index));
debug_assert!(!self.index(copying_ref(from_ref))?.copied());
let size = self.index(copying_ref(from_ref))?.object_size();
let to_index = match self.allocate(size) {
Some(i) => i,
None => {
bail_bug!(
"\
there should always be enough room in the active semi-space for objects that \
survived collection, since the active space is the same size as the idle space",
)
}
};
debug_assert!(self.is_in_active_space(to_index));
let to_ref = match NonZeroU32::new(to_index).and_then(VMGcRef::from_heap_index) {
Some(r) => r,
None => bail_bug!("invalid to_index"),
};
let from_start = usize::try_from(from_index)?;
let to_start = usize::try_from(to_index)?;
let size_usize = usize::try_from(size)?;
let heap = self.heap_slice_mut();
if heap
.get(from_start..)
.and_then(|s| s.get(..size_usize))
.is_none()
|| heap
.get(to_start..)
.and_then(|s| s.get(..size_usize))
.is_none()
{
bail_bug!("corrupt gc heap");
}
heap.copy_within(from_start..from_start + size_usize, to_start);
self.index_mut(header_and_forwarding_ref(from_ref))?
.set_forwarding_ref(to_ref.unchecked_copy());
if self
.index(copying_ref(&to_ref))?
.header
.kind()
.matches(VMGcKind::ExternRef)
{
let old_head = self.active_extern_ref_set_head.take();
self.index_mut::<VMCopyingExternRef>(to_ref.as_typed_unchecked())?
.next_extern_ref = old_head;
self.active_extern_ref_set_head =
Some(to_ref.unchecked_copy().into_externref_unchecked());
}
self.worklist_insert(&to_ref)?;
Ok(to_ref)
}
fn scan(&mut self, gc_ref: &VMGcRef, trace_infos: &TraceInfos) -> Result<()> {
debug_assert!(!gc_ref.is_i31());
let index = gc_ref.heap_index()?.get();
debug_assert!(self.is_in_active_space(index));
let inline_info = self.index(copying_ref(gc_ref))?.inline_trace_info();
let object_start = usize::try_from(index)?;
match inline_info {
InlineTraceInfo::Struct { gc_ref_bitmap } => {
let mut remaining = gc_ref_bitmap;
while remaining != 0 {
let slot = remaining.trailing_zeros();
self.scan_field(object_start, HEADER_SIZE + slot * 4)?;
remaining &= remaining - 1;
}
}
InlineTraceInfo::Array { elems_are_gc_refs } => {
if elems_are_gc_refs {
let array_ref = gc_ref.as_arrayref_unchecked();
let len = self.array_len(array_ref)?;
for i in 0..len {
let elem_offset = match i
.checked_mul(u32::try_from(mem::size_of::<u32>())?)
.and_then(|i| i.checked_add(GC_REF_ARRAY_ELEMS_OFFSET))
{
Some(i) => i,
None => bail_bug!("array element offset overflow"),
};
self.scan_field(object_start, elem_offset)?;
}
}
}
InlineTraceInfo::OutOfLine => {
let ty = self.index(copying_ref(gc_ref))?.header.ty();
let Some(ty) = ty else {
bail_bug!("out-of-line trace info but no type index");
};
match trace_infos.trace_info(&ty) {
TraceInfo::Struct { gc_ref_offsets } => {
for &offset in gc_ref_offsets {
self.scan_field(object_start, offset)?;
}
}
TraceInfo::Array { .. } => {
bail_bug!("arrays should always have inline trace info");
}
}
}
}
Ok(())
}
#[inline]
fn scan_field(&mut self, object_start: usize, offset: u32) -> Result<()> {
let offset = usize::try_from(offset)?;
let field_start = match object_start.checked_add(offset) {
Some(start) => start,
None => bail_bug!("field offset overflow"),
};
let field_end = field_start + mem::size_of::<u32>();
let raw = match self
.heap_slice()
.get(field_start..)
.and_then(|s| s.first_chunk())
{
Some(raw) => u32::from_le_bytes(*raw),
None => bail_bug!("corrupt gc heap"),
};
if let Some(child) = VMGcRef::from_raw_u32(raw)
&& !child.is_i31()
{
debug_assert!(self.is_in_idle_space(child.heap_index()?.get()));
let new_ref = self.forward(&child)?;
debug_assert!(self.is_in_active_space(new_ref.heap_index()?.get()));
let new_raw = new_ref.as_raw_u32().to_le_bytes();
self.heap_slice_mut()[field_start..field_end].copy_from_slice(&new_raw);
}
Ok(())
}
}
unsafe impl GcHeap for CopyingHeap {
fn is_attached(&self) -> bool {
debug_assert_eq!(self.memory.is_some(), self.vmmemory.is_some());
self.memory.is_some()
}
fn attach(&mut self, memory: crate::vm::Memory) {
assert!(!self.is_attached());
assert!(!memory.is_shared_memory());
self.vmmemory = Some(memory.vmmemory());
self.memory = Some(memory);
self.initialize_semi_spaces();
if cfg!(gc_zeal) {
self.heap_slice_mut().fill(POISON);
}
}
fn detach(&mut self) -> crate::vm::Memory {
assert!(self.is_attached());
self.set_bump_ptr(0);
self.set_active_space_end(0);
let CopyingHeap {
no_gc_count,
memory,
vmmemory,
vmctx_data: _,
active_space_start,
idle_space_start,
idle_space_end,
worklist_ptr,
active_extern_ref_set_head,
idle_extern_ref_set_head,
trace_infos,
} = self;
*no_gc_count = 0;
*vmmemory = None;
*active_space_start = 0;
*idle_space_start = 0;
*idle_space_end = 0;
*worklist_ptr = 0;
*active_extern_ref_set_head = None;
*idle_extern_ref_set_head = None;
trace_infos.clear();
memory.take().unwrap()
}
fn ensure_trace_info(&mut self, ty: VMSharedTypeIndex) {
self.trace_infos.ensure(ty);
}
fn as_any(&self) -> &dyn Any {
self as _
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self as _
}
fn enter_no_gc_scope(&mut self) {
self.no_gc_count += 1;
}
fn exit_no_gc_scope(&mut self) {
self.no_gc_count -= 1;
}
fn clone_gc_ref(&mut self, gc_ref: &VMGcRef) -> VMGcRef {
gc_ref.unchecked_copy()
}
fn write_gc_ref(
&mut self,
_host_data_table: &mut ExternRefHostDataTable,
destination: &mut Option<VMGcRef>,
source: Option<&VMGcRef>,
) -> Result<()> {
*destination = source.map(|s| s.unchecked_copy());
Ok(())
}
fn expose_gc_ref_to_wasm(&mut self, _gc_ref: VMGcRef) -> Result<()> {
Ok(())
}
fn alloc_externref(
&mut self,
host_data: ExternRefHostDataId,
) -> Result<Result<VMExternRef, u64>> {
let align = usize::try_from(ALIGN).unwrap();
let size = core::mem::size_of::<VMCopyingExternRef>();
let size = (size + align - 1) & !(align - 1);
let mut header = VMGcHeader::externref();
header.set_reserved_u26(InlineTraceInfo::NO_GC_EDGES.encode());
let gc_ref = match self.alloc_raw(header, Layout::from_size_align(size, align)?)? {
Err(n) => return Ok(Err(n)),
Ok(gc_ref) => gc_ref,
};
let old_head = self.active_extern_ref_set_head.take();
let externref_obj = self.index_mut::<VMCopyingExternRef>(gc_ref.as_typed_unchecked())?;
externref_obj.host_data = host_data;
externref_obj.next_extern_ref = old_head;
let externref = gc_ref.into_externref_unchecked();
self.active_extern_ref_set_head = Some(externref.unchecked_copy());
Ok(Ok(externref))
}
fn externref_host_data(&self, externref: &VMExternRef) -> Result<ExternRefHostDataId> {
let typed_ref = externref_to_copying(externref);
Ok(self.index(typed_ref)?.host_data)
}
fn header(&self, gc_ref: &VMGcRef) -> Result<&VMGcHeader> {
let header: &VMGcHeader = self.index(gc_ref.as_typed_unchecked())?;
debug_assert!(
VMGcKind::try_from_u32(header.kind().as_u32()).is_some(),
"header: invalid VMGcKind {:#010x} at gc_ref {gc_ref:#p}",
header.kind().as_u32(),
);
Ok(header)
}
fn header_mut(&mut self, gc_ref: &VMGcRef) -> Result<&mut VMGcHeader> {
let header: &mut VMGcHeader = self.index_mut(gc_ref.as_typed_unchecked())?;
debug_assert!(
VMGcKind::try_from_u32(header.kind().as_u32()).is_some(),
"header_mut: invalid VMGcKind {:#010x} at gc_ref {gc_ref:#p}",
header.kind().as_u32(),
);
Ok(header)
}
fn object_size(&self, gc_ref: &VMGcRef) -> Result<usize> {
Ok(usize::try_from(
self.index(copying_ref(gc_ref))?.object_size(),
)?)
}
fn alloc_raw(&mut self, header: VMGcHeader, layout: Layout) -> Result<Result<VMGcRef, u64>> {
let align = u32::try_from(layout.align()).unwrap();
ensure!(
align == ALIGN,
"copying collector requires all allocations to have alignment {ALIGN}, \
but got alignment {align}",
);
debug_assert!(layout.size() >= core::mem::size_of::<VMCopyingHeader>());
debug_assert_eq!(
self.bump_ptr() % ALIGN,
0,
"bump_ptr is not aligned to ALIGN"
);
debug_assert_eq!(header.reserved_u26() & HEADER_COPIED_BIT, 0);
if let Some(ty) = header.ty() {
debug_assert!(
self.trace_infos.contains(&ty),
"trace info for {ty:?} should have been eagerly registered",
);
} else {
debug_assert_eq!(header.kind(), VMGcKind::ExternRef);
}
let size = u32::try_from(layout.size()).unwrap();
let size = size
.checked_next_multiple_of(ALIGN)
.ok_or_else(|| crate::Trap::AllocationTooLarge)?;
let gc_ref = match self.allocate(size) {
None => {
let bytes_needed = u64::try_from(layout.size()).unwrap().saturating_mul(2);
return Ok(Err(bytes_needed));
}
Some(index) => {
debug_assert_ne!(index, 0, "index 0 is reserved; bump_ptr should skip it");
VMGcRef::from_heap_index(NonZeroU32::new(index).unwrap()).unwrap()
}
};
if cfg!(gc_zeal) {
let start = usize::try_from(gc_ref.heap_index()?.get()).unwrap();
let slice = &self.heap_slice()[start..][..layout.size()];
gc_assert!(
slice.iter().all(|&b| b == POISON),
"newly allocated GC object at index {start} is not fully poisoned; \
freed memory was corrupted",
);
}
let object_size = size;
*self.index_mut(copying_ref(&gc_ref))? = VMCopyingHeader {
header,
object_size,
};
Ok(Ok(gc_ref))
}
fn alloc_uninit_struct_or_exn(
&mut self,
ty: VMSharedTypeIndex,
layout: &GcStructLayout,
) -> Result<Result<VMGcRef, u64>> {
let kind = if layout.is_exception {
VMGcKind::ExnRef
} else {
VMGcKind::StructRef
};
let mut header = VMGcHeader::from_kind_and_index(kind, ty);
header.set_reserved_u26(InlineTraceInfo::r#struct(layout).encode());
let gc_ref = match self.alloc_raw(header, layout.layout())? {
Err(n) => return Ok(Err(n)),
Ok(gc_ref) => gc_ref,
};
Ok(Ok(gc_ref))
}
fn dealloc_uninit_struct_or_exn(&mut self, _gcref: VMGcRef) -> Result<()> {
Ok(())
}
fn alloc_uninit_array(
&mut self,
ty: VMSharedTypeIndex,
length: u32,
layout: &GcArrayLayout,
) -> Result<Result<VMArrayRef, u64>> {
let mut header = VMGcHeader::from_kind_and_index(VMGcKind::ArrayRef, ty);
header.set_reserved_u26(InlineTraceInfo::array(layout).encode());
let layout = layout
.layout(length)
.ok_or_else(|| crate::Trap::AllocationTooLarge)?;
let gc_ref = match self.alloc_raw(header, layout)? {
Err(n) => return Ok(Err(n)),
Ok(gc_ref) => gc_ref,
};
self.index_mut(gc_ref.as_typed_unchecked::<VMCopyingArrayHeader>())?
.length = length;
Ok(Ok(gc_ref.into_arrayref_unchecked()))
}
fn dealloc_uninit_array(&mut self, _arrayref: VMArrayRef) -> Result<()> {
Ok(())
}
fn array_len(&self, arrayref: &VMArrayRef) -> Result<u32> {
debug_assert!(arrayref.as_gc_ref().is_typed::<VMCopyingArrayHeader>(self));
Ok(self
.index::<VMCopyingArrayHeader>(arrayref.as_gc_ref().as_typed_unchecked())?
.length)
}
fn allocated_bytes(&self) -> usize {
usize::try_from(self.bump_ptr() - self.active_space_start).unwrap()
}
fn gc<'a>(
&'a mut self,
roots: GcRootsIter<'a>,
host_data_table: &'a mut ExternRefHostDataTable,
) -> Box<dyn GarbageCollection<'a> + 'a> {
assert_eq!(self.no_gc_count, 0, "Cannot GC inside a no-GC scope!");
Box::new(CopyingCollection {
roots: Some(roots),
host_data_table,
heap: self,
phase: CopyingCollectionPhase::ProcessRoots,
})
}
unsafe fn vmctx_gc_heap_data(&self) -> NonNull<u8> {
NonNull::from(&self.vmctx_data).cast::<u8>()
}
fn take_memory(&mut self) -> crate::vm::Memory {
debug_assert!(self.is_attached());
self.vmmemory.take();
self.memory.take().unwrap()
}
fn needs_gc_before_next_growth(&self) -> bool {
self.idle_space_start < self.active_space_start
}
unsafe fn replace_memory(&mut self, memory: crate::vm::Memory, delta_bytes_grown: u64) {
debug_assert!(self.memory.is_none());
debug_assert!(!memory.is_shared_memory());
self.vmmemory = Some(memory.vmmemory());
self.memory = Some(memory);
if self.active_space_end() == 0 && self.idle_space_end == 0 {
self.initialize_semi_spaces();
} else {
self.resize_semi_spaces();
}
if cfg!(gc_zeal) && delta_bytes_grown > 0 {
let slice = self.heap_slice_mut();
let len = slice.len();
let delta_bytes_grown = usize::try_from(delta_bytes_grown).unwrap();
slice[len - delta_bytes_grown..].fill(POISON);
}
}
#[inline]
fn vmmemory(&self) -> VMMemoryDefinition {
debug_assert!(self.is_attached());
debug_assert!(!self.memory.as_ref().unwrap().is_shared_memory());
let vmmemory = self.vmmemory.as_ref().unwrap();
VMMemoryDefinition {
base: vmmemory.base,
current_length: AtomicUsize::new(vmmemory.current_length()),
}
}
}
const OBJECTS_PER_INCREMENT: usize = 16_384;
struct CopyingCollection<'a> {
roots: Option<GcRootsIter<'a>>,
host_data_table: &'a mut ExternRefHostDataTable,
heap: &'a mut CopyingHeap,
phase: CopyingCollectionPhase,
}
enum CopyingCollectionPhase {
ProcessRoots,
ProcessWorklist,
SweepExternRefs,
Done,
}
impl CopyingCollection<'_> {
fn process_roots(&mut self) -> Result<()> {
log::trace!("Begin processing GC roots");
let roots = self.roots.take().unwrap();
for mut root in roots {
let gc_ref = root.get()?;
if gc_ref.is_i31() {
continue;
}
let old_index = gc_ref.heap_index()?.get();
debug_assert!(self.heap.is_in_idle_space(old_index));
let new_ref = self.heap.forward(&gc_ref)?;
root.set(new_ref);
}
log::trace!("End processing GC roots");
Ok(())
}
fn begin_collection(&mut self) -> Result<GcProgress> {
log::trace!("Begin copying collection: processing roots");
assert!(self.heap.active_space_start <= self.heap.bump_ptr());
assert!(self.heap.bump_ptr() <= self.heap.active_space_end());
assert!(self.heap.idle_space_start <= self.heap.idle_space_end);
assert!(
self.heap.active_space_end() <= self.heap.idle_space_start
|| self.heap.idle_space_end <= self.heap.active_space_start
);
self.heap.flip();
self.heap.initialize_worklist();
self.process_roots()?;
self.phase = CopyingCollectionPhase::ProcessWorklist;
Ok(GcProgress::Continue)
}
fn process_worklist_increment(&mut self) -> Result<GcProgress> {
log::trace!("Begin processing worklist increment");
let trace_infos = mem::take(&mut self.heap.trace_infos);
let mut count = 0;
let has_more = loop {
if count >= OBJECTS_PER_INCREMENT {
break true;
}
match self.heap.worklist_pop()? {
Some(gc_ref) => {
debug_assert!(self.heap.is_in_active_space(gc_ref.heap_index()?.get()));
self.heap.scan(&gc_ref, &trace_infos)?;
count += 1;
}
None => break false,
}
};
self.heap.trace_infos = trace_infos;
if !has_more {
self.phase = CopyingCollectionPhase::SweepExternRefs;
}
log::trace!("End processing worklist increment (has_more={has_more})");
Ok(GcProgress::Continue)
}
fn sweep_extern_refs(&mut self) -> Result<()> {
log::trace!("Begin sweeping `externref`s");
let mut link = self.heap.idle_extern_ref_set_head.take();
while let Some(externref) = link {
let gc_ref = externref.as_gc_ref();
debug_assert!(self.heap.is_in_idle_space(gc_ref.heap_index()?.get()));
let header = self.heap.index(copying_ref(gc_ref))?;
if !header.copied() {
let typed: &TypedGcRef<VMCopyingExternRef> = gc_ref.as_typed_unchecked();
let host_data_id = self.heap.index(typed)?.host_data;
self.host_data_table.dealloc(host_data_id)?;
}
link = self
.heap
.index::<VMCopyingExternRef>(gc_ref.as_typed_unchecked())?
.next_extern_ref
.as_ref()
.map(|e| e.unchecked_copy());
}
log::trace!("End sweeping `externref`s");
Ok(())
}
fn finish_collection(&mut self) -> Result<GcProgress> {
log::trace!("Copying collection: sweeping externrefs");
assert!(self.heap.active_space_start <= self.heap.bump_ptr());
assert!(self.heap.bump_ptr() <= self.heap.active_space_end());
assert!(self.heap.idle_space_start <= self.heap.idle_space_end);
assert!(
self.heap.active_space_end() <= self.heap.idle_space_start
|| self.heap.idle_space_end <= self.heap.active_space_start
);
self.sweep_extern_refs()?;
self.heap.resize_semi_spaces();
debug_assert_eq!(
self.heap.active_space_end() - self.heap.active_space_start,
self.heap.idle_space_end - self.heap.idle_space_start,
"the active and idle spaces should be the same size"
);
if cfg!(gc_zeal) {
let idle_start = usize::try_from(self.heap.idle_space_start).unwrap();
let idle_end = usize::try_from(self.heap.idle_space_end).unwrap();
self.heap.heap_slice_mut()[idle_start..idle_end].fill(POISON);
}
log::trace!("End copying collection");
self.phase = CopyingCollectionPhase::Done;
Ok(GcProgress::Complete)
}
}
impl GarbageCollection<'_> for CopyingCollection<'_> {
fn collect_increment(&mut self) -> Result<GcProgress> {
match self.phase {
CopyingCollectionPhase::ProcessRoots => self.begin_collection(),
CopyingCollectionPhase::ProcessWorklist => self.process_worklist_increment(),
CopyingCollectionPhase::SweepExternRefs => self.finish_collection(),
CopyingCollectionPhase::Done => Ok(GcProgress::Complete),
}
}
}
impl Drop for CopyingCollection<'_> {
fn drop(&mut self) {
match self.phase {
CopyingCollectionPhase::ProcessWorklist | CopyingCollectionPhase::SweepExternRefs => {
if let Err(e) = self.collect() {
log::error!("error finishing an interrupted copying collection: {e:?}");
}
}
CopyingCollectionPhase::ProcessRoots | CopyingCollectionPhase::Done => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use wasmtime_environ::{HostPtr, PtrSize};
#[test]
fn vm_copying_header_size_align() {
assert_eq!(
wasmtime_environ::copying::HEADER_SIZE as usize,
core::mem::size_of::<VMCopyingHeader>(),
);
assert!(
core::mem::align_of::<VMCopyingHeader>() <= wasmtime_environ::copying::ALIGN as usize,
);
}
#[test]
fn vm_copying_array_header_length_offset() {
assert_eq!(
wasmtime_environ::copying::ARRAY_LENGTH_OFFSET,
u32::try_from(core::mem::offset_of!(VMCopyingArrayHeader, length)).unwrap(),
);
}
#[test]
fn vm_copying_header_object_size_offset() {
assert_eq!(
wasmtime_environ::VM_GC_HEADER_SIZE,
u32::try_from(core::mem::offset_of!(VMCopyingHeader, object_size)).unwrap(),
);
}
#[test]
fn vm_copying_forwarding_ref_offset() {
assert_eq!(
wasmtime_environ::copying::FORWARDING_REF_OFFSET as usize,
core::mem::offset_of!(VMCopyingHeaderAndForwardingRef, forwarding_ref),
);
}
#[test]
fn vm_copying_header_and_forwarding_ref_size() {
assert!(
wasmtime_environ::copying::FORWARDING_REF_OFFSET as usize
+ core::mem::size_of::<Option<VMGcRef>>()
<= wasmtime_environ::copying::MIN_OBJECT_SIZE as usize,
);
}
#[test]
fn vm_copying_heap_data_bump_ptr_offset() {
assert_eq!(
HostPtr.vmcopying_heap_data_bump_ptr() as usize,
core::mem::offset_of!(VMCopyingHeapDataInner, bump_ptr),
);
}
#[test]
fn vm_copying_heap_data_active_space_end_offset() {
assert_eq!(
HostPtr.vmcopying_heap_data_active_space_end() as usize,
core::mem::offset_of!(VMCopyingHeapDataInner, active_space_end),
);
}
#[test]
fn vm_copying_heap_data_size() {
assert_eq!(
HostPtr.size_of_vmcopying_heap_data() as usize,
core::mem::size_of::<VMCopyingHeapData>(),
);
assert_eq!(
HostPtr.size_of_vmcopying_heap_data() as usize,
core::mem::size_of::<VMCopyingHeapDataInner>(),
);
}
#[test]
fn vm_copying_heap_data_align() {
assert_eq!(
HostPtr.align_of_vmcopying_heap_data() as usize,
core::mem::align_of::<VMCopyingHeapData>(),
);
assert_eq!(
HostPtr.align_of_vmcopying_heap_data() as usize,
core::mem::align_of::<VMCopyingHeapDataInner>(),
);
}
}