use crate::gc::heap::{OldSpace, SemiSpace};
use crate::gc::scavenger::RememberedSet;
use crate::objects::heap_object::HeapObject;
use crate::objects::value::JsValue;
pub struct WriteBarrier<'h> {
old_space: &'h OldSpace,
semi_space: &'h SemiSpace,
remembered_set: &'h mut RememberedSet,
}
impl<'h> WriteBarrier<'h> {
pub fn new(
old_space: &'h OldSpace,
semi_space: &'h SemiSpace,
remembered_set: &'h mut RememberedSet,
) -> Self {
Self {
old_space,
semi_space,
remembered_set,
}
}
pub unsafe fn record(&mut self, host: *mut HeapObject, _slot: *const JsValue, value: &JsValue) {
let JsValue::Object(value_ptr) = value else {
return;
};
if value_ptr.is_null() {
return;
}
if !self.old_space.contains(host as *mut u8) {
return;
}
if !self.semi_space.is_in_from_space(*value_ptr) {
return;
}
self.remembered_set.insert(host);
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn jit_write_barrier(host: *mut HeapObject, value: *const JsValue) {
if host.is_null() || value.is_null() {
return;
}
let value_ref = unsafe { &*value };
let JsValue::Object(value_ptr) = value_ref else {
return;
};
if value_ptr.is_null() {
return;
}
THREAD_LOCAL_BARRIER_BUFFER.with(|buf| {
buf.borrow_mut().push(host);
});
}
thread_local! {
static THREAD_LOCAL_BARRIER_BUFFER: std::cell::RefCell<Vec<*mut HeapObject>> =
const { std::cell::RefCell::new(Vec::new()) };
}
pub fn flush_jit_barrier_buffer(remembered_set: &mut RememberedSet) {
THREAD_LOCAL_BARRIER_BUFFER.with(|buf| {
let mut b = buf.borrow_mut();
for host in b.drain(..) {
remembered_set.insert(host);
}
});
}
pub fn jit_barrier_buffer_len() -> usize {
THREAD_LOCAL_BARRIER_BUFFER.with(|buf| buf.borrow().len())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gc::heap::{OldSpace, SemiSpace};
use crate::gc::scavenger::{RememberedSet, Scavenger};
use crate::objects::heap_object::HeapObject;
use std::alloc::Layout;
fn alloc_young(semi: &mut SemiSpace) -> *mut HeapObject {
let layout = Layout::new::<HeapObject>();
let raw = semi.bump_alloc(layout).expect("young from-space has space");
unsafe { std::ptr::write_bytes(raw, 0, layout.size()) };
let obj = raw as *mut HeapObject;
unsafe { (*obj).init_alloc_size(layout.size() as u32) };
obj
}
fn alloc_old(old: &mut OldSpace) -> *mut HeapObject {
let layout = Layout::new::<HeapObject>();
let raw = old.bump_alloc(layout).expect("old-space has space");
unsafe { std::ptr::write_bytes(raw, 0, layout.size()) };
let obj = raw as *mut HeapObject;
unsafe { (*obj).init_alloc_size(layout.size() as u32) };
obj
}
#[test]
fn test_barrier_records_old_to_young_edge() {
let mut semi = SemiSpace::new(4096);
let mut old = OldSpace::new(65536);
let mut rs = RememberedSet::new();
let old_obj = alloc_old(&mut old);
let young_obj = alloc_young(&mut semi);
let value = JsValue::Object(young_obj);
unsafe {
WriteBarrier::new(&old, &semi, &mut rs).record(old_obj, std::ptr::null(), &value);
}
assert_eq!(
rs.len(),
1,
"old→young edge must be recorded in the remembered set"
);
assert!(
rs.iter().any(|p| p == old_obj),
"the old-space host must be in the remembered set"
);
}
#[test]
fn test_barrier_skips_young_to_young_edge() {
let mut semi = SemiSpace::new(4096);
let old = OldSpace::new(65536);
let mut rs = RememberedSet::new();
let young_host = alloc_young(&mut semi);
let young_value_obj = alloc_young(&mut semi);
let value = JsValue::Object(young_value_obj);
unsafe {
WriteBarrier::new(&old, &semi, &mut rs).record(young_host, std::ptr::null(), &value);
}
assert!(
rs.is_empty(),
"young→young store must not populate the remembered set"
);
}
#[test]
fn test_barrier_skips_old_to_old_edge() {
let semi = SemiSpace::new(4096);
let mut old = OldSpace::new(65536);
let mut rs = RememberedSet::new();
let old_host = alloc_old(&mut old);
let old_value_obj = alloc_old(&mut old);
let value = JsValue::Object(old_value_obj);
unsafe {
WriteBarrier::new(&old, &semi, &mut rs).record(old_host, std::ptr::null(), &value);
}
assert!(
rs.is_empty(),
"old→old store must not populate the remembered set"
);
}
#[test]
fn test_barrier_skips_primitive_values() {
let semi = SemiSpace::new(4096);
let mut old = OldSpace::new(65536);
let mut rs = RememberedSet::new();
let old_obj = alloc_old(&mut old);
for value in [
JsValue::Smi(42),
JsValue::Undefined,
JsValue::Null,
JsValue::Boolean(true),
JsValue::HeapNumber(3.14),
JsValue::String("hello".to_string().into()),
] {
unsafe {
WriteBarrier::new(&old, &semi, &mut rs).record(old_obj, std::ptr::null(), &value);
}
}
assert!(
rs.is_empty(),
"primitive value stores must not populate the remembered set"
);
}
#[test]
fn test_barrier_skips_null_object_pointer() {
let semi = SemiSpace::new(4096);
let mut old = OldSpace::new(65536);
let mut rs = RememberedSet::new();
let old_obj = alloc_old(&mut old);
let value = JsValue::Object(std::ptr::null_mut());
unsafe {
WriteBarrier::new(&old, &semi, &mut rs).record(old_obj, std::ptr::null(), &value);
}
assert!(
rs.is_empty(),
"null Object pointer must not populate the remembered set"
);
}
#[test]
fn test_barrier_duplicate_records_are_idempotent() {
let mut semi = SemiSpace::new(4096);
let mut old = OldSpace::new(65536);
let mut rs = RememberedSet::new();
let old_obj = alloc_old(&mut old);
let young_obj = alloc_young(&mut semi);
let value = JsValue::Object(young_obj);
for _ in 0..3 {
unsafe {
WriteBarrier::new(&old, &semi, &mut rs).record(old_obj, std::ptr::null(), &value);
}
}
assert_eq!(
rs.len(),
1,
"duplicate records for the same host must be deduplicated"
);
}
#[test]
fn test_store_young_ref_in_old_object_scavenge_verify_survival() {
let mut semi = SemiSpace::new(4096);
let mut old = OldSpace::new(65536);
let mut rs = RememberedSet::new();
let old_obj = alloc_old(&mut old);
let young_obj = alloc_young(&mut semi);
let original_young_addr = young_obj as usize;
let value = JsValue::Object(young_obj);
unsafe {
WriteBarrier::new(&old, &semi, &mut rs).record(old_obj, std::ptr::null(), &value);
}
assert_eq!(
rs.len(),
1,
"write barrier must record the old-space host in the remembered set"
);
let mut root: *mut HeapObject = young_obj;
let mut roots = [&raw mut root as *mut *mut HeapObject];
unsafe {
Scavenger::new(&mut semi, &mut old).scavenge(&mut roots, &mut rs);
}
let new_young_addr = root as usize;
assert_ne!(
new_young_addr, original_young_addr,
"young object must have been evacuated to the new from-space"
);
assert!(
semi.used() > 0,
"new from-space must contain the evacuated young object"
);
assert!(
rs.is_empty(),
"remembered set must be cleared after a scavenge cycle"
);
}
#[test]
fn test_jit_barrier_records_object_value() {
use super::{jit_barrier_buffer_len, jit_write_barrier};
let mut host = HeapObject::new_null();
let value = JsValue::Object(&raw mut host);
unsafe { jit_write_barrier(&raw mut host, &value) };
assert!(
jit_barrier_buffer_len() >= 1,
"JIT barrier must record object-valued stores"
);
}
#[test]
fn test_jit_barrier_skips_primitive() {
use super::{flush_jit_barrier_buffer, jit_barrier_buffer_len, jit_write_barrier};
let mut rs = RememberedSet::new();
flush_jit_barrier_buffer(&mut rs);
let mut host = HeapObject::new_null();
let value = JsValue::Smi(42);
unsafe { jit_write_barrier(&raw mut host, &value) };
assert_eq!(
jit_barrier_buffer_len(),
0,
"JIT barrier must skip primitive values"
);
}
#[test]
fn test_flush_jit_barrier_buffer() {
use super::{flush_jit_barrier_buffer, jit_barrier_buffer_len, jit_write_barrier};
let mut rs = RememberedSet::new();
flush_jit_barrier_buffer(&mut rs);
let mut host = HeapObject::new_null();
let value = JsValue::Object(&raw mut host);
unsafe { jit_write_barrier(&raw mut host, &value) };
assert!(jit_barrier_buffer_len() >= 1);
flush_jit_barrier_buffer(&mut rs);
assert_eq!(
jit_barrier_buffer_len(),
0,
"buffer must be empty after flush"
);
assert_eq!(rs.len(), 1, "flushed host must appear in remembered set");
}
}