1 //! The deferred reference-counting (DRC) collector.
2 //!
3 //! Warning: this ref-counting collector does not have a tracing cycle
4 //! collector, and therefore cannot collect cycles between GC objects!
5 //!
6 //! For host VM code, we use plain reference counting, where cloning increments
7 //! the reference count, and dropping decrements it. We can avoid many of the
8 //! on-stack increment/decrement operations that typically plague the
9 //! performance of reference counting via Rust's ownership and borrowing system.
10 //! Moving a `VMGcRef` avoids mutating its reference count, and borrowing it
11 //! either avoids the reference count increment or delays it until if/when the
12 //! `VMGcRef` is cloned.
13 //!
14 //! When passing a `VMGcRef` into compiled Wasm code, we don't want to do
15 //! reference count mutations for every compiled `local.{get,set}`, nor for
16 //! every function call. Therefore, we use a variation of **deferred reference
17 //! counting**, where we only mutate reference counts when storing `VMGcRef`s
18 //! somewhere that outlives the Wasm activation: into a global or
19 //! table. Simultaneously, we over-approximate the set of `VMGcRef`s that are
20 //! inside Wasm function activations. Periodically, we walk the stack at GC safe
21 //! points, and use stack map information to precisely identify the set of
22 //! `VMGcRef`s inside Wasm activations. Then we take the difference between this
23 //! precise set and our over-approximation, and decrement the reference count
24 //! for each of the `VMGcRef`s that are in our over-approximation but not in the
25 //! precise set. Finally, the over-approximation is reset to the precise set.
26 //!
27 //! An intrusive, singly-linked list in the object header implements the
28 //! over-approximated set of `VMGcRef`s referenced by Wasm activations. Calling
29 //! a Wasm function and passing it a `VMGcRef` inserts the `VMGcRef` into that
30 //! list if it is not already present, and the compiled Wasm function logically
31 //! "borrows" the `VMGcRef` from the list. Similarly, `global.get` and
32 //! `table.get` operations logically clone the gotten `VMGcRef` into that list
33 //! and then "borrow" the reference out of the list.
34 //!
35 //! When a `VMGcRef` is returned to host code from a Wasm function, the host
36 //! increments the reference count (because the reference is logically
37 //! "borrowed" from the list and the reference count from
38 //! the table will be dropped at the next GC).
39 //!
40 //! The precise set of stack roots is implemented with a mark bit in the object
41 //! header. See the `trace` and `sweep` methods for more details.
42 //!
43 //! For more general information on deferred reference counting, see *An
44 //! Examination of Deferred Reference Counting and Cycle Detection* by Quinane:
45 //! <https://openresearch-repository.anu.edu.au/bitstream/1885/42030/2/hon-thesis.pdf>
46
47 use super::VMArrayRef;
48 use super::free_list::FreeList;
49 use crate::hash_map::HashMap;
50 use crate::hash_set::HashSet;
51 use crate::runtime::vm::{
52 ExternRefHostDataId, ExternRefHostDataTable, GarbageCollection, GcHeap, GcHeapObject,
53 GcProgress, GcRootsIter, GcRuntime, TypedGcRef, VMExternRef, VMGcHeader, VMGcRef,
54 };
55 use crate::vm::VMMemoryDefinition;
56 use crate::{Engine, EngineWeak, prelude::*};
57 use core::sync::atomic::AtomicUsize;
58 use core::{
59 alloc::Layout,
60 any::Any,
61 mem,
62 ops::{Deref, DerefMut},
63 ptr::NonNull,
64 };
65 use wasmtime_environ::drc::{ARRAY_LENGTH_OFFSET, DrcTypeLayouts};
66 use wasmtime_environ::{
67 GcArrayLayout, GcLayout, GcStructLayout, GcTypeLayouts, POISON, VMGcKind, VMSharedTypeIndex,
68 gc_assert,
69 };
70
71 #[expect(clippy::cast_possible_truncation, reason = "known to not overflow")]
72 const GC_REF_ARRAY_ELEMS_OFFSET: u32 = ARRAY_LENGTH_OFFSET + (mem::size_of::<u32>() as u32);
73
74 /// The deferred reference-counting (DRC) collector.
75 ///
76 /// This reference-counting collector does not have a cycle collector, and so it
77 /// will not be able to reclaim garbage cycles.
78 ///
79 /// This is not a moving collector; it doesn't have a nursery or do any
80 /// compaction.
81 #[derive(Default)]
82 pub struct DrcCollector {
83 layouts: DrcTypeLayouts,
84 }
85
86 unsafe impl GcRuntime for DrcCollector {
layouts(&self) -> &dyn GcTypeLayouts87 fn layouts(&self) -> &dyn GcTypeLayouts {
88 &self.layouts
89 }
90
new_gc_heap(&self, engine: &Engine) -> Result<Box<dyn GcHeap>>91 fn new_gc_heap(&self, engine: &Engine) -> Result<Box<dyn GcHeap>> {
92 let heap = DrcHeap::new(engine)?;
93 Ok(Box::new(heap) as _)
94 }
95 }
96
97 /// How to trace a GC object.
98 enum TraceInfo {
99 /// How to trace an array.
100 Array {
101 /// Whether this array type's elements are GC references, and need
102 /// tracing.
103 gc_ref_elems: bool,
104 },
105
106 /// How to trace a struct.
107 Struct {
108 /// The offsets of each GC reference field that needs tracing in
109 /// instances of this struct type.
110 gc_ref_offsets: Box<[u32]>,
111 },
112 }
113
114 /// A deferred reference-counting (DRC) heap.
115 struct DrcHeap {
116 engine: EngineWeak,
117
118 /// For every type that we have allocated in this heap, how do we trace it?
119 trace_infos: HashMap<VMSharedTypeIndex, TraceInfo>,
120
121 /// Count of how many no-gc scopes we are currently within.
122 no_gc_count: u64,
123
124 /// The head of the over-approximated-stack-roots list.
125 ///
126 /// Note that this is exposed directly to compiled Wasm code through the
127 /// vmctx, so must not move.
128 over_approximated_stack_roots: Box<Option<VMGcRef>>,
129
130 /// The storage for the GC heap itself.
131 memory: Option<crate::vm::Memory>,
132
133 /// The cached `VMMemoryDefinition` for `self.memory` so that we don't have
134 /// to make indirect calls through a `dyn RuntimeLinearMemory` object.
135 ///
136 /// Must be updated and kept in sync with `self.memory`, cleared when the
137 /// memory is taken and updated when the memory is replaced.
138 vmmemory: Option<VMMemoryDefinition>,
139
140 /// A free list describing which ranges of the heap are available for use.
141 free_list: Option<FreeList>,
142
143 /// An explicit stack to avoid recursion when deallocating one object needs
144 /// to dec-ref another object, which can then be deallocated and dec-refs
145 /// yet another object, etc...
146 ///
147 /// We store this stack here to reuse the storage and avoid repeated
148 /// allocations.
149 ///
150 /// Note that the `Option` is perhaps technically unnecessary (we could
151 /// remove the `Option` and, when we take the stack out of `self`, leave
152 /// behind an empty vec instead of `None`) but we keep it because it will
153 /// help us catch unexpected re-entry, similar to how a `RefCell` would.
154 dec_ref_stack: Option<Vec<VMGcRef>>,
155 }
156
157 impl DrcHeap {
158 /// Construct a new, default DRC heap.
new(engine: &Engine) -> Result<Self>159 fn new(engine: &Engine) -> Result<Self> {
160 log::trace!("allocating new DRC heap");
161 Ok(Self {
162 engine: engine.weak(),
163 trace_infos: HashMap::with_capacity(1),
164 no_gc_count: 0,
165 over_approximated_stack_roots: Box::new(None),
166 memory: None,
167 vmmemory: None,
168 free_list: None,
169 dec_ref_stack: Some(Vec::with_capacity(1)),
170 })
171 }
172
engine(&self) -> Engine173 fn engine(&self) -> Engine {
174 self.engine.upgrade().unwrap()
175 }
176
dealloc(&mut self, gc_ref: VMGcRef)177 fn dealloc(&mut self, gc_ref: VMGcRef) {
178 let drc_ref = drc_ref(&gc_ref);
179 let size = self.index(drc_ref).object_size();
180 let layout = FreeList::layout(size);
181 let index = gc_ref.as_heap_index().unwrap();
182
183 // Poison the freed memory so that any stale access is detectable.
184 if cfg!(gc_zeal) {
185 let index = usize::try_from(index.get()).unwrap();
186 self.heap_slice_mut()[index..][..layout.size()].fill(POISON);
187 }
188
189 self.free_list.as_mut().unwrap().dealloc(index, layout);
190 }
191
192 /// Increment the ref count for the associated object.
inc_ref(&mut self, gc_ref: &VMGcRef)193 fn inc_ref(&mut self, gc_ref: &VMGcRef) {
194 if gc_ref.is_i31() {
195 return;
196 }
197
198 let drc_ref = drc_ref(gc_ref);
199 let header = self.index_mut(&drc_ref);
200 debug_assert_ne!(
201 header.ref_count, 0,
202 "{:#p} is supposedly live; should have nonzero ref count",
203 *gc_ref
204 );
205 header.ref_count += 1;
206 log::trace!("increment {:#p} ref count -> {}", *gc_ref, header.ref_count);
207 }
208
209 /// Decrement the ref count for the associated object.
210 ///
211 /// Returns `true` if the ref count reached zero and the object should be
212 /// deallocated.
dec_ref(&mut self, gc_ref: &VMGcRef) -> bool213 fn dec_ref(&mut self, gc_ref: &VMGcRef) -> bool {
214 if gc_ref.is_i31() {
215 return false;
216 }
217
218 let drc_ref = drc_ref(gc_ref);
219 let header = self.index_mut(drc_ref);
220 debug_assert_ne!(
221 header.ref_count, 0,
222 "{:#p} is supposedly live; should have nonzero ref count",
223 *gc_ref
224 );
225 header.ref_count -= 1;
226 log::trace!("decrement {:#p} ref count -> {}", *gc_ref, header.ref_count);
227 header.ref_count == 0
228 }
229
230 /// Decrement the ref count for the associated object.
231 ///
232 /// If the ref count reached zero, then deallocate the object and remove its
233 /// associated entry from the `host_data_table` if necessary.
234 ///
235 /// This uses an explicit stack, rather than recursion, for the scenario
236 /// where dropping one object means that the ref count for another object
237 /// that it referenced reaches zero.
dec_ref_and_maybe_dealloc( &mut self, host_data_table: &mut ExternRefHostDataTable, gc_ref: &VMGcRef, )238 fn dec_ref_and_maybe_dealloc(
239 &mut self,
240 host_data_table: &mut ExternRefHostDataTable,
241 gc_ref: &VMGcRef,
242 ) {
243 let mut stack = self.dec_ref_stack.take().unwrap();
244 debug_assert!(stack.is_empty());
245 stack.push(gc_ref.unchecked_copy());
246
247 while let Some(gc_ref) = stack.pop() {
248 if self.dec_ref(&gc_ref) {
249 // The object's reference count reached zero.
250 //
251 // Enqueue any other objects it references for dec-ref'ing.
252 self.trace_gc_ref(&gc_ref, &mut stack);
253
254 // If this object was an `externref`, remove its associated
255 // entry from the host-data table.
256 if let Some(externref) = gc_ref.as_typed::<VMDrcExternRef>(self) {
257 let host_data_id = self.index(externref).host_data;
258 host_data_table.dealloc(host_data_id);
259 }
260
261 // Deallocate this GC object!
262 self.dealloc(gc_ref.unchecked_copy());
263 }
264 }
265
266 debug_assert!(stack.is_empty());
267 debug_assert!(self.dec_ref_stack.is_none());
268 self.dec_ref_stack = Some(stack);
269 }
270
271 /// Ensure that we have tracing information for the given type.
ensure_trace_info(&mut self, ty: VMSharedTypeIndex)272 fn ensure_trace_info(&mut self, ty: VMSharedTypeIndex) {
273 if self.trace_infos.contains_key(&ty) {
274 return;
275 }
276
277 self.insert_new_trace_info(ty);
278 }
279
insert_new_trace_info(&mut self, ty: VMSharedTypeIndex)280 fn insert_new_trace_info(&mut self, ty: VMSharedTypeIndex) {
281 debug_assert!(!self.trace_infos.contains_key(&ty));
282
283 let engine = self.engine();
284 let gc_layout = engine
285 .signatures()
286 .layout(ty)
287 .unwrap_or_else(|| panic!("should have a GC layout for {ty:?}"));
288
289 let info = match gc_layout {
290 GcLayout::Array(l) => {
291 if l.elems_are_gc_refs {
292 debug_assert_eq!(l.elem_offset(0), GC_REF_ARRAY_ELEMS_OFFSET,);
293 }
294 TraceInfo::Array {
295 gc_ref_elems: l.elems_are_gc_refs,
296 }
297 }
298 GcLayout::Struct(l) => TraceInfo::Struct {
299 gc_ref_offsets: l
300 .fields
301 .iter()
302 .filter_map(|f| if f.is_gc_ref { Some(f.offset) } else { None })
303 .collect(),
304 },
305 };
306
307 let old_entry = self.trace_infos.insert(ty, info);
308 debug_assert!(old_entry.is_none());
309 }
310
311 /// Enumerate all of the given `VMGcRef`'s outgoing edges.
trace_gc_ref(&self, gc_ref: &VMGcRef, stack: &mut Vec<VMGcRef>)312 fn trace_gc_ref(&self, gc_ref: &VMGcRef, stack: &mut Vec<VMGcRef>) {
313 debug_assert!(!gc_ref.is_i31());
314
315 let header = self.header(gc_ref);
316 let Some(ty) = header.ty() else {
317 debug_assert!(header.kind().matches(VMGcKind::ExternRef));
318 return;
319 };
320
321 match self
322 .trace_infos
323 .get(&ty)
324 .expect("should have inserted trace info for every GC type allocated in this heap")
325 {
326 TraceInfo::Struct { gc_ref_offsets } => {
327 stack.reserve(gc_ref_offsets.len());
328 let data = self.gc_object_data(gc_ref);
329 for offset in gc_ref_offsets {
330 let raw = data.read_u32(*offset);
331 if let Some(gc_ref) = VMGcRef::from_raw_u32(raw)
332 && !gc_ref.is_i31()
333 {
334 debug_assert!(
335 {
336 let header = self.header(&gc_ref);
337 let kind = header.kind().as_u32();
338 VMGcKind::try_from_u32(kind).is_some()
339 },
340 "trace_gc_ref: struct field at offset {offset} references object \
341 with invalid `VMGcKind`",
342 );
343
344 stack.push(gc_ref);
345 }
346 }
347 }
348
349 TraceInfo::Array { gc_ref_elems } => {
350 if !*gc_ref_elems {
351 return;
352 }
353
354 let data = self.gc_object_data(gc_ref);
355 let len = self.array_len(gc_ref.as_arrayref_unchecked());
356 stack.reserve(usize::try_from(len).unwrap());
357 for i in 0..len {
358 let elem_offset = GC_REF_ARRAY_ELEMS_OFFSET
359 + i * u32::try_from(mem::size_of::<u32>()).unwrap();
360 let raw = data.read_u32(elem_offset);
361 if let Some(gc_ref) = VMGcRef::from_raw_u32(raw)
362 && !gc_ref.is_i31()
363 {
364 debug_assert!(
365 {
366 let header = self.header(&gc_ref);
367 let kind = header.kind().as_u32();
368 VMGcKind::try_from_u32(kind).is_some()
369 },
370 "trace_gc_ref: array element at index {i} references object \
371 with invalid `VMGcKind`",
372 );
373
374 stack.push(gc_ref);
375 }
376 }
377 }
378 }
379 }
380
381 /// Iterate over the over-approximated-stack-roots list.
iter_over_approximated_stack_roots(&self) -> impl Iterator<Item = VMGcRef> + '_382 fn iter_over_approximated_stack_roots(&self) -> impl Iterator<Item = VMGcRef> + '_ {
383 let mut link = (*self.over_approximated_stack_roots)
384 .as_ref()
385 .map(|r| r.unchecked_copy());
386
387 core::iter::from_fn(move || {
388 let r = link.as_ref()?.unchecked_copy();
389 link = self.index(drc_ref(&r)).next_over_approximated_stack_root();
390 Some(r)
391 })
392 }
393
394 /// Assert the integrity of the over-approximated stack roots list.
assert_over_approximated_stack_roots_integrity(&self)395 fn assert_over_approximated_stack_roots_integrity(&self) {
396 if !cfg!(gc_zeal) {
397 return;
398 }
399
400 let mut visited = HashSet::new();
401 for gc_ref in self.iter_over_approximated_stack_roots() {
402 let idx = gc_ref.as_heap_index().unwrap().get();
403
404 // Each entry must have a valid `VMGcKind`.
405 let header = self.header(&gc_ref);
406 let kind = header.kind().as_u32();
407 assert!(
408 VMGcKind::try_from_u32(kind).is_some(),
409 "over-approx list: entry at heap index {idx} has invalid VMGcKind {kind:#034b}",
410 );
411
412 // Each entry must have its in-list bit set.
413 let drc_header = self.index(drc_ref(&gc_ref));
414 assert!(
415 drc_header.is_in_over_approximated_stack_roots(),
416 "over-approx list: entry at heap index {idx} does not have in-list bit set",
417 );
418
419 // Each entry must have a nonzero ref count.
420 assert_ne!(
421 drc_header.ref_count, 0,
422 "over-approx list: entry at heap index {idx} has zero ref count",
423 );
424
425 // No cycles or duplicates.
426 assert!(
427 visited.insert(idx),
428 "over-approx list: cycle or duplicate detected at heap index {idx}",
429 );
430 }
431 }
432
433 /// Assert that every free block in the free list is filled with the poison
434 /// pattern.
assert_free_blocks_are_poisoned(&self)435 fn assert_free_blocks_are_poisoned(&self) {
436 if !cfg!(gc_zeal) {
437 return;
438 }
439
440 let free_list = self.free_list.as_ref().unwrap();
441 for (index, len) in free_list.iter_free_blocks() {
442 let start = usize::try_from(index).unwrap();
443 let size = usize::try_from(len).unwrap();
444 let slice = &self.heap_slice()[start..][..size];
445 assert!(
446 slice.iter().all(|&b| b == POISON),
447 "free block at heap index {start} (size {size}) is not fully poisoned",
448 );
449 }
450 }
451
trace(&mut self, roots: &mut GcRootsIter<'_>)452 fn trace(&mut self, roots: &mut GcRootsIter<'_>) {
453 // The `over_approx_set` is used for `debug_assert!`s checking that
454 // every reference we read out from the stack via stack maps is actually
455 // in the table. If that weren't true, than either we forgot to insert a
456 // reference in the table when passing it into Wasm (a bug) or we are
457 // reading invalid references from the stack (another bug).
458 let mut over_approx_set: DebugOnly<HashSet<_>> = Default::default();
459 if cfg!(debug_assertions) {
460 over_approx_set.extend(self.iter_over_approximated_stack_roots());
461 }
462
463 for root in roots {
464 if !root.is_on_wasm_stack() {
465 // We only trace on-Wasm-stack GC roots. These are the
466 // GC references that we do deferred ref counting for
467 // and that get inserted into our activations
468 // table. Other GC roots are managed purely with naive
469 // ref counting.
470 continue;
471 }
472
473 let gc_ref = root.get();
474
475 if gc_ref.is_i31() {
476 continue;
477 }
478
479 log::trace!("Found GC reference on the stack: {gc_ref:#p}");
480
481 debug_assert!(
482 over_approx_set.contains(&gc_ref),
483 "every on-stack gc ref inside a Wasm frame should \
484 have be in our over-approximated stack roots set, \
485 but {gc_ref:#p} is not in the set",
486 );
487 debug_assert!(
488 self.index(drc_ref(&gc_ref))
489 .is_in_over_approximated_stack_roots(),
490 "every on-stack gc ref inside a Wasm frame should have \
491 its in-the-over-approximated-stack-roots-list bit set",
492 );
493 debug_assert_ne!(
494 self.index_mut(drc_ref(&gc_ref)).ref_count,
495 0,
496 "{gc_ref:#p} is on the Wasm stack and therefore should be held \
497 alive by the over-approximated-stack-roots set; should have \
498 nonzero ref count",
499 );
500
501 self.index_mut(drc_ref(&gc_ref)).set_marked();
502 }
503 }
504
505 #[inline(never)]
506 #[cold]
log_gc_ref_set(prefix: &str, items: impl Iterator<Item = VMGcRef>)507 fn log_gc_ref_set(prefix: &str, items: impl Iterator<Item = VMGcRef>) {
508 assert!(log::log_enabled!(log::Level::Trace));
509 let mut set = "{".to_string();
510 let mut any = false;
511 for gc_ref in items {
512 any = true;
513 set += &format!("\n {gc_ref:#p},");
514 }
515 if any {
516 set.push('\n');
517 }
518 set.push('}');
519 log::trace!("{prefix}: {set}");
520 }
521
522 /// Sweep the bump allocation table after we've discovered our precise stack
523 /// roots.
sweep(&mut self, host_data_table: &mut ExternRefHostDataTable)524 fn sweep(&mut self, host_data_table: &mut ExternRefHostDataTable) {
525 if log::log_enabled!(log::Level::Trace) {
526 Self::log_gc_ref_set(
527 "over-approximated-stack-roots set before sweeping",
528 self.iter_over_approximated_stack_roots(),
529 );
530 }
531
532 // Logically, we are taking the difference between
533 // over-approximated-stack-roots set and the precise-stack-roots set,
534 // decrementing the ref count for each object in that difference
535 // (because they are no longer live on the stack), and then resetting
536 // the over-approximated-stack-roots set to the precise set. In our
537 // actual implementation, the over-approximated-stack-roots set is
538 // implemented as an intrusive, singly-linked list in the object
539 // headers, and the precise-stack-roots set is implemented via the mark
540 // bits in the object headers. Therefore, we walk the
541 // over-approximated-stack-roots list, checking whether each object has
542 // its mark bit set.
543 //
544 // * If the mark bit is set, then it is in the precise-stack-roots set
545 // and is still on the stack, so we keep it in the
546 // over-approximated-stack-roots list and do not modify its ref count.
547 //
548 // * If the mark bit is not set, then it is not in the
549 // precise-stack-roots set and is no longer on the stack, so we remove
550 // it from the over-approximated-stack-roots set and decrement its ref
551 // count.
552 //
553 // We also clear the mark bits as we do this traversal.
554 //
555 // Finally, note that decrementing ref counts may run `Drop`
556 // implementations, which may run arbitrary user code. However, because
557 // of our `&mut` borrow on this heap (which ultimately comes from a
558 // `&mut Store`) we're guaranteed that nothing will reentrantly touch
559 // this heap or run Wasm code in this store.
560 log::trace!("Begin sweeping");
561
562 // The `VMGcRef` of the previous object in the
563 // over-approximated-stack-roots list, if any.
564 let mut prev = None;
565
566 // The `VMGcRef` of the next object in the over-approximated-stack-roots
567 // list, if any.
568 let mut next = (*self.over_approximated_stack_roots)
569 .as_ref()
570 .map(|r| r.unchecked_copy());
571
572 while let Some(gc_ref) = next {
573 log::trace!("sweeping gc ref: {gc_ref:#p}");
574
575 let header = self.index_mut(drc_ref(&gc_ref));
576 debug_assert!(header.is_in_over_approximated_stack_roots());
577
578 if header.clear_marked() {
579 // This GC ref was marked, meaning it is still on the stack, so
580 // keep it in the over-approximated-stack-roots list and move on
581 // to the next object in the list.
582 log::trace!(
583 " -> {gc_ref:#p} is marked, leaving it in the over-approximated-\
584 stack-roots list"
585 );
586 next = header.next_over_approximated_stack_root();
587 prev = Some(gc_ref);
588 continue;
589 }
590
591 // This GC ref was not marked, meaning it is no longer on the stack,
592 // so remove it from the over-approximated-stack-roots list and
593 // decrement its reference count.
594 log::trace!(
595 " -> {gc_ref:#p} is not marked, removing it from over-approximated-\
596 stack-roots list and decrementing its ref count"
597 );
598 next = header.next_over_approximated_stack_root();
599 let prev_next = header.next_over_approximated_stack_root();
600 header.set_in_over_approximated_stack_roots_bit(false);
601 match &prev {
602 None => *self.over_approximated_stack_roots = prev_next,
603 Some(prev) => self
604 .index_mut(drc_ref(prev))
605 .set_next_over_approximated_stack_root(prev_next),
606 }
607 self.dec_ref_and_maybe_dealloc(host_data_table, &gc_ref);
608 }
609
610 log::trace!("Done sweeping");
611
612 if log::log_enabled!(log::Level::Trace) {
613 Self::log_gc_ref_set(
614 "over-approximated-stack-roots set after sweeping",
615 self.iter_over_approximated_stack_roots(),
616 );
617 }
618 }
619 }
620
621 /// Convert the given GC reference as a typed GC reference pointing to a
622 /// `VMDrcHeader`.
drc_ref(gc_ref: &VMGcRef) -> &TypedGcRef<VMDrcHeader>623 fn drc_ref(gc_ref: &VMGcRef) -> &TypedGcRef<VMDrcHeader> {
624 debug_assert!(!gc_ref.is_i31());
625 gc_ref.as_typed_unchecked()
626 }
627
628 /// Convert a generic `externref` to a typed reference to our concrete
629 /// `externref` type.
externref_to_drc(externref: &VMExternRef) -> &TypedGcRef<VMDrcExternRef>630 fn externref_to_drc(externref: &VMExternRef) -> &TypedGcRef<VMDrcExternRef> {
631 let gc_ref = externref.as_gc_ref();
632 debug_assert!(!gc_ref.is_i31());
633 gc_ref.as_typed_unchecked()
634 }
635
636 /// The common header for all objects in the DRC collector.
637 ///
638 /// This adds a ref count on top collector-agnostic `VMGcHeader`.
639 ///
640 /// This is accessed by JIT code.
641 #[repr(C)]
642 struct VMDrcHeader {
643 header: VMGcHeader,
644 ref_count: u64,
645 next_over_approximated_stack_root: Option<VMGcRef>,
646 object_size: u32,
647 }
648
649 unsafe impl GcHeapObject for VMDrcHeader {
650 #[inline]
is(_header: &VMGcHeader) -> bool651 fn is(_header: &VMGcHeader) -> bool {
652 // All DRC objects have a DRC header.
653 true
654 }
655 }
656
657 impl VMDrcHeader {
658 /// The size of this header's object.
659 #[inline]
object_size(&self) -> usize660 fn object_size(&self) -> usize {
661 usize::try_from(self.object_size).unwrap()
662 }
663
664 /// Is this object in the over-approximated stack roots list?
665 #[inline]
is_in_over_approximated_stack_roots(&self) -> bool666 fn is_in_over_approximated_stack_roots(&self) -> bool {
667 self.header.reserved_u26() & wasmtime_environ::drc::HEADER_IN_OVER_APPROX_LIST_BIT != 0
668 }
669
670 /// Set whether this object is in the over-approximated stack roots list.
671 #[inline]
set_in_over_approximated_stack_roots_bit(&mut self, bit: bool)672 fn set_in_over_approximated_stack_roots_bit(&mut self, bit: bool) {
673 let reserved = self.header.reserved_u26();
674 let new_reserved = if bit {
675 reserved | wasmtime_environ::drc::HEADER_IN_OVER_APPROX_LIST_BIT
676 } else {
677 reserved & !wasmtime_environ::drc::HEADER_IN_OVER_APPROX_LIST_BIT
678 };
679 self.header.set_reserved_u26(new_reserved);
680 }
681
682 /// Get the next object after this one in the over-approximated-stack-roots
683 /// list, if any.
684 #[inline]
next_over_approximated_stack_root(&self) -> Option<VMGcRef>685 fn next_over_approximated_stack_root(&self) -> Option<VMGcRef> {
686 debug_assert!(self.is_in_over_approximated_stack_roots());
687 self.next_over_approximated_stack_root
688 .as_ref()
689 .map(|r| r.unchecked_copy())
690 }
691
692 /// Set the next object after this one in the over-approximated-stack-roots
693 /// list.
694 #[inline]
set_next_over_approximated_stack_root(&mut self, next: Option<VMGcRef>)695 fn set_next_over_approximated_stack_root(&mut self, next: Option<VMGcRef>) {
696 debug_assert!(self.is_in_over_approximated_stack_roots());
697 self.next_over_approximated_stack_root = next;
698 }
699
700 /// Is this object marked?
701 #[inline]
is_marked(&self) -> bool702 fn is_marked(&self) -> bool {
703 self.header.reserved_u26() & wasmtime_environ::drc::HEADER_MARK_BIT != 0
704 }
705
706 /// Mark this object.
707 ///
708 /// Returns `true` if this object was newly marked (i.e. `is_marked()` would
709 /// have returned `false` before this call was made).
710 #[inline]
set_marked(&mut self)711 fn set_marked(&mut self) {
712 let reserved = self.header.reserved_u26();
713 self.header
714 .set_reserved_u26(reserved | wasmtime_environ::drc::HEADER_MARK_BIT);
715 }
716
717 /// Clear the mark bit for this object.
718 ///
719 /// Returns `true` if this object was marked before the mark bit was
720 /// cleared.
721 #[inline]
clear_marked(&mut self) -> bool722 fn clear_marked(&mut self) -> bool {
723 if self.is_marked() {
724 let reserved = self.header.reserved_u26();
725 self.header
726 .set_reserved_u26(reserved & !wasmtime_environ::drc::HEADER_MARK_BIT);
727 debug_assert!(!self.is_marked());
728 true
729 } else {
730 false
731 }
732 }
733 }
734
735 /// The common header for all arrays in the DRC collector.
736 #[repr(C)]
737 struct VMDrcArrayHeader {
738 header: VMDrcHeader,
739 length: u32,
740 }
741
742 unsafe impl GcHeapObject for VMDrcArrayHeader {
743 #[inline]
is(header: &VMGcHeader) -> bool744 fn is(header: &VMGcHeader) -> bool {
745 header.kind() == VMGcKind::ArrayRef
746 }
747 }
748
749 /// The representation of an `externref` in the DRC collector.
750 #[repr(C)]
751 struct VMDrcExternRef {
752 header: VMDrcHeader,
753 host_data: ExternRefHostDataId,
754 }
755
756 unsafe impl GcHeapObject for VMDrcExternRef {
757 #[inline]
is(header: &VMGcHeader) -> bool758 fn is(header: &VMGcHeader) -> bool {
759 header.kind() == VMGcKind::ExternRef
760 }
761 }
762
763 unsafe impl GcHeap for DrcHeap {
is_attached(&self) -> bool764 fn is_attached(&self) -> bool {
765 debug_assert_eq!(self.memory.is_some(), self.free_list.is_some());
766 debug_assert_eq!(self.memory.is_some(), self.vmmemory.is_some());
767 self.memory.is_some()
768 }
769
attach(&mut self, memory: crate::vm::Memory)770 fn attach(&mut self, memory: crate::vm::Memory) {
771 assert!(!self.is_attached());
772 assert!(!memory.is_shared_memory());
773 debug_assert!(self.over_approximated_stack_roots.is_none());
774 let len = memory.vmmemory().current_length();
775 self.free_list = Some(FreeList::new(len));
776 self.vmmemory = Some(memory.vmmemory());
777 self.memory = Some(memory);
778
779 // Poison the entire heap so any access to uninitialized memory is
780 // detectable.
781 if cfg!(gc_zeal) {
782 self.heap_slice_mut().fill(POISON);
783 }
784 }
785
detach(&mut self) -> crate::vm::Memory786 fn detach(&mut self) -> crate::vm::Memory {
787 assert!(self.is_attached());
788
789 let DrcHeap {
790 engine: _,
791 no_gc_count,
792 over_approximated_stack_roots,
793 free_list,
794 dec_ref_stack,
795 memory,
796 vmmemory,
797
798 // NB: we will only ever be reused with the same engine, so no need
799 // to clear out our tracing info just to fill it back in with the
800 // same exact stuff.
801 trace_infos: _,
802 } = self;
803
804 *no_gc_count = 0;
805 **over_approximated_stack_roots = None;
806 *free_list = None;
807 *vmmemory = None;
808 debug_assert!(dec_ref_stack.as_ref().is_some_and(|s| s.is_empty()));
809
810 memory.take().unwrap()
811 }
812
as_any(&self) -> &dyn Any813 fn as_any(&self) -> &dyn Any {
814 self as _
815 }
816
as_any_mut(&mut self) -> &mut dyn Any817 fn as_any_mut(&mut self) -> &mut dyn Any {
818 self as _
819 }
820
enter_no_gc_scope(&mut self)821 fn enter_no_gc_scope(&mut self) {
822 self.no_gc_count += 1;
823 }
824
exit_no_gc_scope(&mut self)825 fn exit_no_gc_scope(&mut self) {
826 self.no_gc_count -= 1;
827 }
828
clone_gc_ref(&mut self, gc_ref: &VMGcRef) -> VMGcRef829 fn clone_gc_ref(&mut self, gc_ref: &VMGcRef) -> VMGcRef {
830 self.inc_ref(gc_ref);
831 gc_ref.unchecked_copy()
832 }
833
write_gc_ref( &mut self, host_data_table: &mut ExternRefHostDataTable, destination: &mut Option<VMGcRef>, source: Option<&VMGcRef>, )834 fn write_gc_ref(
835 &mut self,
836 host_data_table: &mut ExternRefHostDataTable,
837 destination: &mut Option<VMGcRef>,
838 source: Option<&VMGcRef>,
839 ) {
840 // Increment the ref count of the object being written into the slot.
841 if let Some(src) = source {
842 self.inc_ref(src);
843 }
844
845 // Decrement the ref count of the value being overwritten and, if
846 // necessary, deallocate the GC object.
847 if let Some(dest) = destination {
848 self.dec_ref_and_maybe_dealloc(host_data_table, dest);
849 }
850
851 // Do the actual write.
852 *destination = source.map(|s| s.unchecked_copy());
853 }
854
expose_gc_ref_to_wasm(&mut self, gc_ref: VMGcRef)855 fn expose_gc_ref_to_wasm(&mut self, gc_ref: VMGcRef) {
856 let header = self.index_mut(drc_ref(&gc_ref));
857 if header.is_in_over_approximated_stack_roots() {
858 // Already in the over-approximated-stack-roots list, nothing more
859 // to do here.
860 return;
861 }
862
863 // Push this object onto the head of the over-approximated-stack-roots
864 // list.
865 header.set_in_over_approximated_stack_roots_bit(true);
866 let next = (*self.over_approximated_stack_roots)
867 .as_ref()
868 .map(|r| r.unchecked_copy());
869 self.index_mut(drc_ref(&gc_ref))
870 .set_next_over_approximated_stack_root(next);
871 *self.over_approximated_stack_roots = Some(gc_ref);
872 }
873
alloc_externref( &mut self, host_data: ExternRefHostDataId, ) -> Result<Result<VMExternRef, u64>>874 fn alloc_externref(
875 &mut self,
876 host_data: ExternRefHostDataId,
877 ) -> Result<Result<VMExternRef, u64>> {
878 let gc_ref =
879 match self.alloc_raw(VMGcHeader::externref(), Layout::new::<VMDrcExternRef>())? {
880 Err(n) => return Ok(Err(n)),
881 Ok(gc_ref) => gc_ref,
882 };
883 self.index_mut::<VMDrcExternRef>(gc_ref.as_typed_unchecked())
884 .host_data = host_data;
885 Ok(Ok(gc_ref.into_externref_unchecked()))
886 }
887
externref_host_data(&self, externref: &VMExternRef) -> ExternRefHostDataId888 fn externref_host_data(&self, externref: &VMExternRef) -> ExternRefHostDataId {
889 let typed_ref = externref_to_drc(externref);
890 self.index(typed_ref).host_data
891 }
892
header(&self, gc_ref: &VMGcRef) -> &VMGcHeader893 fn header(&self, gc_ref: &VMGcRef) -> &VMGcHeader {
894 let header: &VMGcHeader = self.index(gc_ref.as_typed_unchecked());
895
896 debug_assert!(
897 VMGcKind::try_from_u32(header.kind().as_u32()).is_some(),
898 "header: invalid VMGcKind {:#010x} at gc_ref {gc_ref:#p}",
899 header.kind().as_u32(),
900 );
901
902 header
903 }
904
header_mut(&mut self, gc_ref: &VMGcRef) -> &mut VMGcHeader905 fn header_mut(&mut self, gc_ref: &VMGcRef) -> &mut VMGcHeader {
906 let header: &mut VMGcHeader = self.index_mut(gc_ref.as_typed_unchecked());
907
908 debug_assert!(
909 VMGcKind::try_from_u32(header.kind().as_u32()).is_some(),
910 "header_mut: invalid VMGcKind {:#010x} at gc_ref {gc_ref:#p}",
911 header.kind().as_u32(),
912 );
913
914 header
915 }
916
object_size(&self, gc_ref: &VMGcRef) -> usize917 fn object_size(&self, gc_ref: &VMGcRef) -> usize {
918 self.index(drc_ref(gc_ref)).object_size()
919 }
920
alloc_raw(&mut self, header: VMGcHeader, layout: Layout) -> Result<Result<VMGcRef, u64>>921 fn alloc_raw(&mut self, header: VMGcHeader, layout: Layout) -> Result<Result<VMGcRef, u64>> {
922 debug_assert!(layout.size() >= core::mem::size_of::<VMDrcHeader>());
923 debug_assert!(layout.align() >= core::mem::align_of::<VMDrcHeader>());
924 debug_assert_eq!(header.reserved_u26(), 0);
925
926 // We must have trace info for every GC type that we allocate in this
927 // heap. The only kinds of GC objects we allocate that do not have an
928 // associated `VMSharedTypeIndex` are `externref`s, and they don't have
929 // any GC edges.
930 if let Some(ty) = header.ty() {
931 self.ensure_trace_info(ty);
932 } else {
933 debug_assert_eq!(header.kind(), VMGcKind::ExternRef);
934 }
935
936 let object_size = u32::try_from(layout.size()).unwrap();
937
938 let gc_ref = match self.free_list.as_mut().unwrap().alloc(layout)? {
939 None => return Ok(Err(u64::try_from(layout.size()).unwrap())),
940 Some(index) => VMGcRef::from_heap_index(index).unwrap(),
941 };
942
943 // Assert that the newly-allocated memory is still filled with the
944 // poison pattern, and hasn't been corrupted since deallocation (or
945 // initial heap creation).
946 if cfg!(gc_zeal) {
947 let start = usize::try_from(gc_ref.as_heap_index().unwrap().get()).unwrap();
948 let slice = &self.heap_slice()[start..][..layout.size()];
949 gc_assert!(
950 slice.iter().all(|&b| b == POISON),
951 "newly allocated GC object at index {start} is not fully poisoned; \
952 freed memory was corrupted",
953 );
954 }
955
956 *self.index_mut(drc_ref(&gc_ref)) = VMDrcHeader {
957 header,
958 ref_count: 1,
959 next_over_approximated_stack_root: None,
960 object_size,
961 };
962 log::trace!("new object: increment {gc_ref:#p} ref count -> 1");
963 Ok(Ok(gc_ref))
964 }
965
alloc_uninit_struct_or_exn( &mut self, ty: VMSharedTypeIndex, layout: &GcStructLayout, ) -> Result<Result<VMGcRef, u64>>966 fn alloc_uninit_struct_or_exn(
967 &mut self,
968 ty: VMSharedTypeIndex,
969 layout: &GcStructLayout,
970 ) -> Result<Result<VMGcRef, u64>> {
971 let kind = if layout.is_exception {
972 VMGcKind::ExnRef
973 } else {
974 VMGcKind::StructRef
975 };
976 let gc_ref =
977 match self.alloc_raw(VMGcHeader::from_kind_and_index(kind, ty), layout.layout())? {
978 Err(n) => return Ok(Err(n)),
979 Ok(gc_ref) => gc_ref,
980 };
981
982 Ok(Ok(gc_ref))
983 }
984
dealloc_uninit_struct_or_exn(&mut self, gcref: VMGcRef)985 fn dealloc_uninit_struct_or_exn(&mut self, gcref: VMGcRef) {
986 self.dealloc(gcref);
987 }
988
alloc_uninit_array( &mut self, ty: VMSharedTypeIndex, length: u32, layout: &GcArrayLayout, ) -> Result<Result<VMArrayRef, u64>>989 fn alloc_uninit_array(
990 &mut self,
991 ty: VMSharedTypeIndex,
992 length: u32,
993 layout: &GcArrayLayout,
994 ) -> Result<Result<VMArrayRef, u64>> {
995 let gc_ref = match self.alloc_raw(
996 VMGcHeader::from_kind_and_index(VMGcKind::ArrayRef, ty),
997 layout.layout(length),
998 )? {
999 Err(n) => return Ok(Err(n)),
1000 Ok(gc_ref) => gc_ref,
1001 };
1002
1003 self.index_mut(gc_ref.as_typed_unchecked::<VMDrcArrayHeader>())
1004 .length = length;
1005
1006 Ok(Ok(gc_ref.into_arrayref_unchecked()))
1007 }
1008
dealloc_uninit_array(&mut self, arrayref: VMArrayRef)1009 fn dealloc_uninit_array(&mut self, arrayref: VMArrayRef) {
1010 self.dealloc(arrayref.into())
1011 }
1012
array_len(&self, arrayref: &VMArrayRef) -> u321013 fn array_len(&self, arrayref: &VMArrayRef) -> u32 {
1014 debug_assert!(arrayref.as_gc_ref().is_typed::<VMDrcArrayHeader>(self));
1015 self.index::<VMDrcArrayHeader>(arrayref.as_gc_ref().as_typed_unchecked())
1016 .length
1017 }
1018
gc<'a>( &'a mut self, roots: GcRootsIter<'a>, host_data_table: &'a mut ExternRefHostDataTable, ) -> Box<dyn GarbageCollection<'a> + 'a>1019 fn gc<'a>(
1020 &'a mut self,
1021 roots: GcRootsIter<'a>,
1022 host_data_table: &'a mut ExternRefHostDataTable,
1023 ) -> Box<dyn GarbageCollection<'a> + 'a> {
1024 assert_eq!(self.no_gc_count, 0, "Cannot GC inside a no-GC scope!");
1025 Box::new(DrcCollection {
1026 roots,
1027 host_data_table,
1028 heap: self,
1029 phase: DrcCollectionPhase::Trace,
1030 })
1031 }
1032
vmctx_gc_heap_data(&self) -> NonNull<u8>1033 unsafe fn vmctx_gc_heap_data(&self) -> NonNull<u8> {
1034 let ptr: NonNull<Option<VMGcRef>> = NonNull::from(&*self.over_approximated_stack_roots);
1035 ptr.cast()
1036 }
1037
take_memory(&mut self) -> crate::vm::Memory1038 fn take_memory(&mut self) -> crate::vm::Memory {
1039 debug_assert!(self.is_attached());
1040 self.vmmemory.take();
1041 self.memory.take().unwrap()
1042 }
1043
replace_memory(&mut self, memory: crate::vm::Memory, delta_bytes_grown: u64)1044 unsafe fn replace_memory(&mut self, memory: crate::vm::Memory, delta_bytes_grown: u64) {
1045 debug_assert!(self.memory.is_none());
1046 debug_assert!(!memory.is_shared_memory());
1047 self.vmmemory = Some(memory.vmmemory());
1048 self.memory = Some(memory);
1049
1050 // Poison the newly-grown region so stale accesses are detectable.
1051 if cfg!(gc_zeal) {
1052 let old_cap = self.free_list.as_ref().unwrap().current_capacity();
1053 let new_bytes = usize::try_from(delta_bytes_grown).unwrap();
1054 let slice = self.heap_slice_mut();
1055 if old_cap + new_bytes <= slice.len() {
1056 slice[old_cap..old_cap + new_bytes].fill(POISON);
1057 }
1058 }
1059
1060 self.free_list
1061 .as_mut()
1062 .unwrap()
1063 .add_capacity(usize::try_from(delta_bytes_grown).unwrap())
1064 }
1065
1066 #[inline]
vmmemory(&self) -> VMMemoryDefinition1067 fn vmmemory(&self) -> VMMemoryDefinition {
1068 debug_assert!(self.is_attached());
1069 debug_assert!(!self.memory.as_ref().unwrap().is_shared_memory());
1070 let vmmemory = self.vmmemory.as_ref().unwrap();
1071 VMMemoryDefinition {
1072 base: vmmemory.base,
1073 current_length: AtomicUsize::new(vmmemory.current_length()),
1074 }
1075 }
1076 }
1077
1078 struct DrcCollection<'a> {
1079 roots: GcRootsIter<'a>,
1080 host_data_table: &'a mut ExternRefHostDataTable,
1081 heap: &'a mut DrcHeap,
1082 phase: DrcCollectionPhase,
1083 }
1084
1085 enum DrcCollectionPhase {
1086 Trace,
1087 Sweep,
1088 Done,
1089 }
1090
1091 impl<'a> GarbageCollection<'a> for DrcCollection<'a> {
collect_increment(&mut self) -> GcProgress1092 fn collect_increment(&mut self) -> GcProgress {
1093 match self.phase {
1094 DrcCollectionPhase::Trace => {
1095 log::trace!("Begin DRC trace");
1096
1097 self.heap.assert_over_approximated_stack_roots_integrity();
1098 self.heap.assert_free_blocks_are_poisoned();
1099
1100 self.heap.trace(&mut self.roots);
1101
1102 self.heap.assert_over_approximated_stack_roots_integrity();
1103 self.heap.assert_free_blocks_are_poisoned();
1104
1105 log::trace!("End DRC trace");
1106 self.phase = DrcCollectionPhase::Sweep;
1107 GcProgress::Continue
1108 }
1109 DrcCollectionPhase::Sweep => {
1110 log::trace!("Begin DRC sweep");
1111
1112 self.heap.assert_over_approximated_stack_roots_integrity();
1113 self.heap.assert_free_blocks_are_poisoned();
1114
1115 self.heap.sweep(self.host_data_table);
1116
1117 self.heap.assert_over_approximated_stack_roots_integrity();
1118 self.heap.assert_free_blocks_are_poisoned();
1119
1120 log::trace!("End DRC sweep");
1121 self.phase = DrcCollectionPhase::Done;
1122 GcProgress::Complete
1123 }
1124 DrcCollectionPhase::Done => GcProgress::Complete,
1125 }
1126 }
1127 }
1128
1129 #[derive(Debug, Default)]
1130 struct DebugOnly<T> {
1131 inner: T,
1132 }
1133
1134 impl<T> Deref for DebugOnly<T> {
1135 type Target = T;
1136
deref(&self) -> &T1137 fn deref(&self) -> &T {
1138 if cfg!(debug_assertions) {
1139 &self.inner
1140 } else {
1141 panic!(
1142 "only deref `DebugOnly` when `cfg(debug_assertions)` or \
1143 inside a `debug_assert!(..)`"
1144 )
1145 }
1146 }
1147 }
1148
1149 impl<T> DerefMut for DebugOnly<T> {
deref_mut(&mut self) -> &mut T1150 fn deref_mut(&mut self) -> &mut T {
1151 if cfg!(debug_assertions) {
1152 &mut self.inner
1153 } else {
1154 panic!(
1155 "only deref `DebugOnly` when `cfg(debug_assertions)` or \
1156 inside a `debug_assert!(..)`"
1157 )
1158 }
1159 }
1160 }
1161
1162 #[cfg(test)]
1163 mod tests {
1164 use super::*;
1165 use wasmtime_environ::HostPtr;
1166
1167 #[test]
vm_drc_header_size_align()1168 fn vm_drc_header_size_align() {
1169 assert_eq!(
1170 (wasmtime_environ::drc::HEADER_SIZE as usize),
1171 core::mem::size_of::<VMDrcHeader>()
1172 );
1173 assert_eq!(
1174 (wasmtime_environ::drc::HEADER_ALIGN as usize),
1175 core::mem::align_of::<VMDrcHeader>()
1176 );
1177 }
1178
1179 #[test]
vm_drc_array_header_length_offset()1180 fn vm_drc_array_header_length_offset() {
1181 assert_eq!(
1182 wasmtime_environ::drc::ARRAY_LENGTH_OFFSET,
1183 u32::try_from(core::mem::offset_of!(VMDrcArrayHeader, length)).unwrap(),
1184 );
1185 }
1186
1187 #[test]
ref_count_is_at_correct_offset()1188 fn ref_count_is_at_correct_offset() {
1189 let extern_data = VMDrcHeader {
1190 header: VMGcHeader::externref(),
1191 ref_count: 0,
1192 next_over_approximated_stack_root: None,
1193 object_size: 0,
1194 };
1195
1196 let extern_data_ptr = &extern_data as *const _;
1197 let ref_count_ptr = &extern_data.ref_count as *const _;
1198
1199 let actual_offset = (ref_count_ptr as usize) - (extern_data_ptr as usize);
1200
1201 let offsets = wasmtime_environ::VMOffsets::from(wasmtime_environ::VMOffsetsFields {
1202 ptr: HostPtr,
1203 num_imported_functions: 0,
1204 num_imported_tables: 0,
1205 num_imported_memories: 0,
1206 num_imported_globals: 0,
1207 num_imported_tags: 0,
1208 num_defined_tables: 0,
1209 num_defined_memories: 0,
1210 num_owned_memories: 0,
1211 num_defined_globals: 0,
1212 num_defined_tags: 0,
1213 num_escaped_funcs: 0,
1214 });
1215
1216 assert_eq!(
1217 offsets.vm_drc_header_ref_count(),
1218 u32::try_from(actual_offset).unwrap(),
1219 );
1220 }
1221 }
1222