Skip to content

Commit a02803b

Browse files
authored
Remove NULL ObjectReference (#1064)
We changed `ObjectReference` so that it is backed by `NonZeroUsize`, and can no longer represent a NULL reference. For cases where an `ObjectReferences` may or may not exist, `Option<ObjectReference>` should be used instead. This is an API-breaking change. - `ObjectReference::NULL` and `ObjectReference::is_null()` are removed. - `ObjectReference::from_raw_address()` now returns `Option<ObjectReference>`. The unsafe method `ObjectReference::from_raw_address_unchecked()` assumes the address is not zero, and returns `ObjectReference`. - API functions that may or may not return a valid `ObjectReference` now return `Option<ObjectReference>`. Examples include `Edge::load()` and `ReferenceGlue::get_referent()`. If a VM binding needs to expose `Option<ObjectReference>` to native (C/C++) programs, a convenient type `crate::util::api_util::NullableObjectReference` is provided. Fixes: #1043
1 parent 1b9cfe4 commit a02803b

31 files changed

+196
-187
lines changed

docs/userguide/src/tutorial/code/mygc_semispace/gc_work.rs

-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ impl<VM: VMBinding> ProcessEdgesWork for MyGCProcessEdges<VM> {
5656
}
5757

5858
fn trace_object(&mut self, object: ObjectReference) -> ObjectReference {
59-
debug_assert!(!object.is_null());
6059
let worker = self.worker();
6160
let queue = &mut self.base.nodes;
6261
if self.plan.tospace().in_space(object) {

docs/userguide/src/tutorial/mygc/ss/exercise_solution.md

-2
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,6 @@ In `gc_work.rs`:
149149
the tospace and fromspace:
150150
```rust
151151
fn trace_object(&mut self, object: ObjectReference) -> ObjectReference {
152-
debug_assert!(!object.is_null());
153-
154152
// Add this!
155153
else if self.plan().youngspace().in_space(object) {
156154
self.plan().youngspace.trace_object::<Self, TripleSpaceCopyContext<VM>>(

src/memory_manager.rs

-3
Original file line numberDiff line numberDiff line change
@@ -638,9 +638,6 @@ pub fn is_mmtk_object(addr: Address) -> bool {
638638
/// * `object`: The object reference to query.
639639
pub fn is_in_mmtk_spaces<VM: VMBinding>(object: ObjectReference) -> bool {
640640
use crate::mmtk::SFT_MAP;
641-
if object.is_null() {
642-
return false;
643-
}
644641
SFT_MAP
645642
.get_checked(object.to_address::<VM>())
646643
.is_in_space(object)

src/plan/generational/gc_work.rs

+3-5
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ impl<VM: VMBinding, P: GenerationalPlanExt<VM> + PlanTraceObject<VM>, const KIND
4343
}
4444

4545
fn trace_object(&mut self, object: ObjectReference) -> ObjectReference {
46-
debug_assert!(!object.is_null());
47-
4846
// We cannot borrow `self` twice in a call, so we extract `worker` as a local variable.
4947
let worker = self.worker();
5048
self.plan.trace_object_nursery::<VectorObjectQueue, KIND>(
@@ -55,10 +53,10 @@ impl<VM: VMBinding, P: GenerationalPlanExt<VM> + PlanTraceObject<VM>, const KIND
5553
}
5654

5755
fn process_edge(&mut self, slot: EdgeOf<Self>) {
58-
let object = slot.load();
59-
if object.is_null() {
56+
let Some(object) = slot.load() else {
57+
// Skip slots that are not holding an object reference.
6058
return;
61-
}
59+
};
6260
let new_object = self.trace_object(object);
6361
debug_assert!(!self.plan.is_object_in_nursery(new_object));
6462
// Note: If `object` is a mature object, `trace_object` will not call `space.trace_object`,

src/plan/global.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,7 @@ pub trait PlanTraceObject<VM: VMBinding> {
691691
///
692692
/// Arguments:
693693
/// * `trace`: the current transitive closure
694-
/// * `object`: the object to trace. This is a non-nullable object reference.
694+
/// * `object`: the object to trace.
695695
/// * `worker`: the GC worker that is tracing this object.
696696
fn trace_object<Q: ObjectQueue, const KIND: TraceKind>(
697697
&self,

src/plan/tracing.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ impl<'a, E: ProcessEdgesWork> EdgeVisitor<EdgeOf<E>> for ObjectsClosure<'a, E> {
117117
{
118118
use crate::vm::edge_shape::Edge;
119119
trace!(
120-
"(ObjectsClosure) Visit edge {:?} (pointing to {})",
120+
"(ObjectsClosure) Visit edge {:?} (pointing to {:?})",
121121
slot,
122122
slot.load()
123123
);

src/policy/copyspace.rs

-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,6 @@ impl<VM: VMBinding> CopySpace<VM> {
204204
worker: &mut GCWorker<VM>,
205205
) -> ObjectReference {
206206
trace!("copyspace.trace_object(, {:?}, {:?})", object, semantics,);
207-
debug_assert!(!object.is_null());
208207

209208
// If this is not from space, we do not need to trace it (the object has been copied to the tosapce)
210209
if !self.is_from_space() {

src/policy/immix/immixspace.rs

-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ impl<VM: VMBinding> crate::policy::gc_work::PolicyTraceObject<VM> for ImmixSpace
184184
copy: Option<CopySemantics>,
185185
worker: &mut GCWorker<VM>,
186186
) -> ObjectReference {
187-
debug_assert!(!object.is_null());
188187
if KIND == TRACE_KIND_TRANSITIVE_PIN {
189188
self.trace_object_without_moving(queue, object)
190189
} else if KIND == TRACE_KIND_DEFRAG {

src/policy/immortalspace.rs

-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,6 @@ impl<VM: VMBinding> ImmortalSpace<VM> {
187187
queue: &mut Q,
188188
object: ObjectReference,
189189
) -> ObjectReference {
190-
debug_assert!(!object.is_null());
191190
#[cfg(feature = "vo_bit")]
192191
debug_assert!(
193192
crate::util::metadata::vo_bit::is_vo_bit_set::<VM>(object),

src/policy/largeobjectspace.rs

-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,6 @@ impl<VM: VMBinding> LargeObjectSpace<VM> {
189189
queue: &mut Q,
190190
object: ObjectReference,
191191
) -> ObjectReference {
192-
debug_assert!(!object.is_null());
193192
#[cfg(feature = "vo_bit")]
194193
debug_assert!(
195194
crate::util::metadata::vo_bit::is_vo_bit_set::<VM>(object),

src/policy/markcompactspace.rs

+11-19
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,7 @@ impl<VM: VMBinding> SFT for MarkCompactSpace<VM> {
3737
}
3838

3939
fn get_forwarded_object(&self, object: ObjectReference) -> Option<ObjectReference> {
40-
let forwarding_pointer = Self::get_header_forwarding_pointer(object);
41-
if forwarding_pointer.is_null() {
42-
None
43-
} else {
44-
Some(forwarding_pointer)
45-
}
40+
Self::get_header_forwarding_pointer(object)
4641
}
4742

4843
fn is_live(&self, object: ObjectReference) -> bool {
@@ -130,7 +125,6 @@ impl<VM: VMBinding> crate::policy::gc_work::PolicyTraceObject<VM> for MarkCompac
130125
_copy: Option<CopySemantics>,
131126
_worker: &mut GCWorker<VM>,
132127
) -> ObjectReference {
133-
debug_assert!(!object.is_null());
134128
debug_assert!(
135129
KIND != TRACE_KIND_TRANSITIVE_PIN,
136130
"MarkCompact does not support transitive pin trace."
@@ -177,8 +171,9 @@ impl<VM: VMBinding> MarkCompactSpace<VM> {
177171
}
178172

179173
/// Get header forwarding pointer for an object
180-
fn get_header_forwarding_pointer(object: ObjectReference) -> ObjectReference {
181-
unsafe { Self::header_forwarding_pointer_address(object).load::<ObjectReference>() }
174+
fn get_header_forwarding_pointer(object: ObjectReference) -> Option<ObjectReference> {
175+
let addr = unsafe { Self::header_forwarding_pointer_address(object).load::<Address>() };
176+
ObjectReference::from_raw_address(addr)
182177
}
183178

184179
/// Store header forwarding pointer for an object
@@ -251,12 +246,8 @@ impl<VM: VMBinding> MarkCompactSpace<VM> {
251246
queue.enqueue(object);
252247
}
253248

254-
let result = Self::get_header_forwarding_pointer(object);
255-
debug_assert!(
256-
!result.is_null(),
257-
"Object {object} does not have a forwarding pointer"
258-
);
259-
result
249+
Self::get_header_forwarding_pointer(object)
250+
.unwrap_or_else(|| panic!("Object {object} does not have a forwarding pointer"))
260251
}
261252

262253
pub fn test_and_mark(object: ObjectReference) -> bool {
@@ -393,10 +384,9 @@ impl<VM: VMBinding> MarkCompactSpace<VM> {
393384
// clear the VO bit
394385
vo_bit::unset_vo_bit::<VM>(obj);
395386

396-
let forwarding_pointer = Self::get_header_forwarding_pointer(obj);
397-
398-
trace!("Compact {} to {}", obj, forwarding_pointer);
399-
if !forwarding_pointer.is_null() {
387+
let maybe_forwarding_pointer = Self::get_header_forwarding_pointer(obj);
388+
if let Some(forwarding_pointer) = maybe_forwarding_pointer {
389+
trace!("Compact {} to {}", obj, forwarding_pointer);
400390
let new_object = forwarding_pointer;
401391
Self::clear_header_forwarding_pointer(new_object);
402392

@@ -408,6 +398,8 @@ impl<VM: VMBinding> MarkCompactSpace<VM> {
408398
vo_bit::set_vo_bit::<VM>(new_object);
409399
to = new_object.to_object_start::<VM>() + copied_size;
410400
debug_assert_eq!(end_of_new_object, to);
401+
} else {
402+
trace!("Skipping dead object {}", obj);
411403
}
412404
}
413405
}

src/policy/marksweepspace/malloc_ms/global.rs

-2
Original file line numberDiff line numberDiff line change
@@ -400,8 +400,6 @@ impl<VM: VMBinding> MallocSpace<VM> {
400400
queue: &mut Q,
401401
object: ObjectReference,
402402
) -> ObjectReference {
403-
debug_assert!(!object.is_null());
404-
405403
assert!(
406404
self.in_space(object),
407405
"Cannot mark an object {} that was not alloced by malloc.",

src/policy/marksweepspace/native_ms/block.rs

+8-4
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,8 @@ impl Block {
280280
while cell + cell_size <= self.start() + Block::BYTES {
281281
// The invariants we checked earlier ensures that we can use cell and object reference interchangably
282282
// We may not really have an object in this cell, but if we do, this object reference is correct.
283-
let potential_object = ObjectReference::from_raw_address(cell);
283+
// About unsafe: We know `cell` is non-zero here.
284+
let potential_object = unsafe { ObjectReference::from_raw_address_unchecked(cell) };
284285

285286
if !VM::VMObjectModel::LOCAL_MARK_BIT_SPEC
286287
.is_marked::<VM>(potential_object, Ordering::SeqCst)
@@ -320,9 +321,12 @@ impl Block {
320321

321322
while cell + cell_size <= self.end() {
322323
// possible object ref
323-
let potential_object_ref = ObjectReference::from_raw_address(
324-
cursor + VM::VMObjectModel::OBJECT_REF_OFFSET_LOWER_BOUND,
325-
);
324+
let potential_object_ref = unsafe {
325+
// We know cursor plus an offset cannot be 0.
326+
ObjectReference::from_raw_address_unchecked(
327+
cursor + VM::VMObjectModel::OBJECT_REF_OFFSET_LOWER_BOUND,
328+
)
329+
};
326330
trace!(
327331
"{:?}: cell = {}, last cell in free list = {}, cursor = {}, potential object = {}",
328332
self,

src/policy/marksweepspace/native_ms/global.rs

-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,6 @@ impl<VM: VMBinding> MarkSweepSpace<VM> {
291291
queue: &mut Q,
292292
object: ObjectReference,
293293
) -> ObjectReference {
294-
debug_assert!(!object.is_null());
295294
debug_assert!(
296295
self.in_space(object),
297296
"Cannot mark an object {} that was not alloced by free list allocator.",

src/scheduler/gc_work.rs

+6-12
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,6 @@ impl<E: ProcessEdgesWork> ObjectTracer for ProcessEdgesWorkTracer<E> {
223223
/// Forward the `trace_object` call to the underlying `ProcessEdgesWork`,
224224
/// and flush as soon as the underlying buffer of `process_edges_work` is full.
225225
fn trace_object(&mut self, object: ObjectReference) -> ObjectReference {
226-
debug_assert!(!object.is_null());
227226
let result = self.process_edges_work.trace_object(object);
228227
self.flush_if_full();
229228
result
@@ -604,12 +603,11 @@ pub trait ProcessEdgesWork:
604603
/// Process an edge, including loading the object reference from the memory slot,
605604
/// trace the object and store back the new object reference if necessary.
606605
fn process_edge(&mut self, slot: EdgeOf<Self>) {
607-
let object = slot.load();
608-
if object.is_null() {
606+
let Some(object) = slot.load() else {
607+
// Skip slots that are not holding an object reference.
609608
return;
610-
}
609+
};
611610
let new_object = self.trace_object(object);
612-
debug_assert!(!new_object.is_null());
613611
if Self::OVERWRITE_REFERENCE && new_object != object {
614612
slot.store(new_object);
615613
}
@@ -667,8 +665,6 @@ impl<VM: VMBinding> ProcessEdgesWork for SFTProcessEdges<VM> {
667665
fn trace_object(&mut self, object: ObjectReference) -> ObjectReference {
668666
use crate::policy::sft::GCWorkerMutRef;
669667

670-
debug_assert!(!object.is_null());
671-
672668
// Erase <VM> type parameter
673669
let worker = GCWorkerMutRef::new(self.worker());
674670

@@ -925,20 +921,18 @@ impl<VM: VMBinding, P: PlanTraceObject<VM> + Plan<VM = VM>, const KIND: TraceKin
925921
}
926922

927923
fn trace_object(&mut self, object: ObjectReference) -> ObjectReference {
928-
debug_assert!(!object.is_null());
929924
// We cannot borrow `self` twice in a call, so we extract `worker` as a local variable.
930925
let worker = self.worker();
931926
self.plan
932927
.trace_object::<VectorObjectQueue, KIND>(&mut self.base.nodes, object, worker)
933928
}
934929

935930
fn process_edge(&mut self, slot: EdgeOf<Self>) {
936-
let object = slot.load();
937-
if object.is_null() {
931+
let Some(object) = slot.load() else {
932+
// Skip slots that are not holding an object reference.
938933
return;
939-
}
934+
};
940935
let new_object = self.trace_object(object);
941-
debug_assert!(!new_object.is_null());
942936
if P::may_move_objects::<KIND>() && new_object != object {
943937
slot.store(new_object);
944938
}

src/util/address.rs

+32-22
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use bytemuck::NoUninit;
33

44
use std::fmt;
55
use std::mem;
6+
use std::num::NonZeroUsize;
67
use std::ops::*;
78
use std::sync::atomic::Ordering;
89

@@ -477,30 +478,52 @@ use crate::vm::VMBinding;
477478
/// their layout. We now only allow a binding to define their semantics through a set of
478479
/// methods in [`crate::vm::ObjectModel`]. Major refactoring is needed in MMTk to allow
479480
/// the opaque `ObjectReference` type, and we haven't seen a use case for now.
481+
///
482+
/// Note that [`ObjectReference`] cannot be null. For the cases where a non-null object reference
483+
/// may or may not exist, (such as the result of [`crate::vm::edge_shape::Edge::load`])
484+
/// `Option<ObjectReference>` should be used. [`ObjectReference`] is backed by `NonZeroUsize`
485+
/// which cannot be zero, and it has the `#[repr(transparent)]` attribute. Thanks to [null pointer
486+
/// optimization (NPO)][NPO], `Option<ObjectReference>` has the same size as `NonZeroUsize` and
487+
/// `usize`. For the convenience of passing `Option<ObjectReference>` to and from native (C/C++)
488+
/// programs, mmtk-core provides [`crate::util::api_util::NullableObjectReference`].
489+
///
490+
/// [NPO]: https://doc.rust-lang.org/std/option/index.html#representation
480491
#[repr(transparent)]
481492
#[derive(Copy, Clone, Eq, Hash, PartialOrd, Ord, PartialEq, NoUninit)]
482-
pub struct ObjectReference(usize);
493+
pub struct ObjectReference(NonZeroUsize);
483494

484495
impl ObjectReference {
485-
/// The null object reference, represented as zero.
486-
pub const NULL: ObjectReference = ObjectReference(0);
487-
488496
/// Cast the object reference to its raw address. This method is mostly for the convinience of a binding.
489497
///
490498
/// MMTk should not make any assumption on the actual location of the address with the object reference.
491499
/// MMTk should not assume the address returned by this method is in our allocation. For the purposes of
492500
/// setting object metadata, MMTk should use [`crate::vm::ObjectModel::ref_to_address()`] or [`crate::vm::ObjectModel::ref_to_header()`].
493501
pub fn to_raw_address(self) -> Address {
494-
Address(self.0)
502+
Address(self.0.get())
495503
}
496504

497505
/// Cast a raw address to an object reference. This method is mostly for the convinience of a binding.
498506
/// This is how a binding creates `ObjectReference` instances.
499507
///
508+
/// If `addr` is 0, the result is `None`.
509+
///
500510
/// MMTk should not assume an arbitrary address can be turned into an object reference. MMTk can use [`crate::vm::ObjectModel::address_to_ref()`]
501511
/// to turn addresses that are from [`crate::vm::ObjectModel::ref_to_address()`] back to object.
502-
pub fn from_raw_address(addr: Address) -> ObjectReference {
503-
ObjectReference(addr.0)
512+
pub fn from_raw_address(addr: Address) -> Option<ObjectReference> {
513+
NonZeroUsize::new(addr.0).map(ObjectReference)
514+
}
515+
516+
/// Like `from_raw_address`, but assume `addr` is not zero. This can be used to elide a check
517+
/// against zero for performance-critical code.
518+
///
519+
/// # Safety
520+
///
521+
/// This method assumes `addr` is not zero. It should only be used in cases where we know at
522+
/// compile time that the input cannot be zero. For example, if we compute the address by
523+
/// adding a positive offset to a non-zero address, we know the result must not be zero.
524+
pub unsafe fn from_raw_address_unchecked(addr: Address) -> ObjectReference {
525+
debug_assert!(!addr.is_zero());
526+
ObjectReference(NonZeroUsize::new_unchecked(addr.0))
504527
}
505528

506529
/// Get the in-heap address from an object reference. This method is used by MMTk to get an in-heap address
@@ -541,28 +564,15 @@ impl ObjectReference {
541564
obj
542565
}
543566

544-
/// is this object reference null reference?
545-
pub fn is_null(self) -> bool {
546-
self.0 == 0
547-
}
548-
549567
/// Is the object reachable, determined by the policy?
550568
/// Note: Objects in ImmortalSpace may have `is_live = true` but are actually unreachable.
551569
pub fn is_reachable<VM: VMBinding>(self) -> bool {
552-
if self.is_null() {
553-
false
554-
} else {
555-
unsafe { SFT_MAP.get_unchecked(self.to_address::<VM>()) }.is_reachable(self)
556-
}
570+
unsafe { SFT_MAP.get_unchecked(self.to_address::<VM>()) }.is_reachable(self)
557571
}
558572

559573
/// Is the object live, determined by the policy?
560574
pub fn is_live<VM: VMBinding>(self) -> bool {
561-
if self.0 == 0 {
562-
false
563-
} else {
564-
unsafe { SFT_MAP.get_unchecked(self.to_address::<VM>()) }.is_live(self)
565-
}
575+
unsafe { SFT_MAP.get_unchecked(self.to_address::<VM>()) }.is_live(self)
566576
}
567577

568578
/// Can the object be moved?

src/util/alloc/free_list_allocator.rs

+7-5
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,8 @@ impl<VM: VMBinding> FreeListAllocator<VM> {
370370

371371
#[cfg(feature = "malloc_native_mimalloc")]
372372
fn free(&self, addr: Address) {
373+
assert!(!addr.is_zero(), "Attempted to free zero address.");
374+
373375
use crate::util::ObjectReference;
374376
let block = Block::from_unaligned_address(addr);
375377
let block_tls = block.load_tls();
@@ -402,11 +404,11 @@ impl<VM: VMBinding> FreeListAllocator<VM> {
402404
}
403405

404406
// unset allocation bit
405-
unsafe {
406-
crate::util::metadata::vo_bit::unset_vo_bit_unsafe::<VM>(
407-
ObjectReference::from_raw_address(addr),
408-
)
409-
};
407+
// Note: We cannot use `unset_vo_bit_unsafe` because two threads may attempt to free
408+
// objects at adjacent addresses, and they may share the same byte in the VO bit metadata.
409+
crate::util::metadata::vo_bit::unset_vo_bit::<VM>(unsafe {
410+
ObjectReference::from_raw_address_unchecked(addr)
411+
})
410412
}
411413

412414
fn store_block_tls(&self, block: Block) {

0 commit comments

Comments
 (0)