use bytemuck::{Pod, Zeroable};
use super::{DumpError, types::*};
const OBJECT_STARTS_MAGIC: [u8; 16] = *b"NEOOBJSTARTS\0\0\0\0";
const OBJECT_STARTS_FORMAT_VERSION: u32 = 1;
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
struct ObjectStartsHeader {
magic: [u8; 16],
version: u32,
header_size: u32,
object_count: u64,
}
const HEADER_SIZE: usize = std::mem::size_of::<ObjectStartsHeader>();
pub(crate) fn build_object_starts(heap: &DumpTaggedHeap) -> Result<Vec<u8>, DumpError> {
let count = heap.objects.len();
let mut bytes = vec![0u8; HEADER_SIZE];
for (i, obj) in heap.objects.iter().enumerate() {
write_object_span(&mut bytes, obj, heap, i);
}
let header = ObjectStartsHeader {
magic: OBJECT_STARTS_MAGIC,
version: OBJECT_STARTS_FORMAT_VERSION,
header_size: HEADER_SIZE as u32,
object_count: count as u64,
};
bytes[..HEADER_SIZE].copy_from_slice(bytemuck::bytes_of(&header));
Ok(bytes)
}
const SPAN_NONE: u8 = 0;
const SPAN_CONS: u8 = 1;
const SPAN_FLOAT: u8 = 2;
const SPAN_STRING: u8 = 3;
const SPAN_VECTORLIKE: u8 = 4;
const SPAN_UNMAPPED: u8 = 5;
fn write_object_span(out: &mut Vec<u8>, obj: &DumpHeapObject, heap: &DumpTaggedHeap, index: usize) {
match obj {
DumpHeapObject::Cons { .. } => {
if let Some(span) = heap.mapped_cons.get(index).and_then(|s| *s) {
out.push(SPAN_CONS);
write_u64(out, span.offset);
} else {
out.push(SPAN_NONE);
}
}
DumpHeapObject::Float(_) => {
if let Some(span) = heap.mapped_floats.get(index).and_then(|s| *s) {
out.push(SPAN_FLOAT);
write_u64(out, span.offset);
} else {
out.push(SPAN_NONE);
}
}
DumpHeapObject::Str { .. } => {
if let Some(span) = heap.mapped_strings.get(index).and_then(|s| *s) {
out.push(SPAN_STRING);
write_u64(out, span.offset);
write_u64(out, span.len);
} else {
out.push(SPAN_NONE);
}
}
DumpHeapObject::Vector(_)
| DumpHeapObject::Lambda(_)
| DumpHeapObject::Macro(_)
| DumpHeapObject::Record(_)
| DumpHeapObject::Marker(_)
| DumpHeapObject::Overlay(_) => {
let vl = heap.mapped_veclikes.get(index).and_then(|s| *s);
let sl = heap.mapped_slots.get(index).and_then(|s| *s);
if let Some(vl) = vl {
out.push(SPAN_VECTORLIKE);
write_u64(out, vl.offset);
write_u64(out, vl.len);
if let Some(sl) = sl {
out.push(1); write_u64(out, sl.offset);
write_u64(out, sl.len);
} else {
out.push(0); }
} else {
out.push(SPAN_NONE);
}
}
DumpHeapObject::HashTable(_)
| DumpHeapObject::ByteCode(_)
| DumpHeapObject::Subr { .. }
| DumpHeapObject::Buffer(_)
| DumpHeapObject::Window(_)
| DumpHeapObject::Frame(_)
| DumpHeapObject::Timer(_)
| DumpHeapObject::Free => {
out.push(SPAN_UNMAPPED);
}
}
}
pub(crate) struct LoadedSpans {
pub mapped_cons: Vec<Option<DumpConsSpan>>,
pub mapped_floats: Vec<Option<DumpFloatSpan>>,
pub mapped_strings: Vec<Option<DumpStringSpan>>,
pub mapped_veclikes: Vec<Option<DumpVecLikeSpan>>,
pub mapped_slots: Vec<Option<DumpSlotSpan>>,
}
pub(crate) fn load_object_starts(section: &[u8]) -> Result<LoadedSpans, DumpError> {
if section.len() < HEADER_SIZE {
return Err(DumpError::ImageFormatError(
"object-starts section too small for header".into(),
));
}
let header = *bytemuck::from_bytes::<ObjectStartsHeader>(§ion[..HEADER_SIZE]);
if header.magic != OBJECT_STARTS_MAGIC {
return Err(DumpError::ImageFormatError(
"object-starts magic mismatch".into(),
));
}
if header.version != OBJECT_STARTS_FORMAT_VERSION {
return Err(DumpError::ImageFormatError(format!(
"object-starts version mismatch: expected {}, got {}",
OBJECT_STARTS_FORMAT_VERSION, header.version,
)));
}
let count = header.object_count as usize;
let mut cursor = HEADER_SIZE;
let mut mapped_cons = vec![None; count];
let mut mapped_floats = vec![None; count];
let mut mapped_strings = vec![None; count];
let mut mapped_veclikes = vec![None; count];
let mut mapped_slots = vec![None; count];
for i in 0..count {
if cursor >= section.len() {
return Err(DumpError::ImageFormatError(
"object-starts section truncated".into(),
));
}
let tag = section[cursor];
cursor += 1;
match tag {
SPAN_NONE | SPAN_UNMAPPED => {}
SPAN_CONS => {
let offset = read_u64(section, &mut cursor)?;
mapped_cons[i] = Some(DumpConsSpan { offset });
}
SPAN_FLOAT => {
let offset = read_u64(section, &mut cursor)?;
mapped_floats[i] = Some(DumpFloatSpan { offset });
}
SPAN_STRING => {
let offset = read_u64(section, &mut cursor)?;
let len = read_u64(section, &mut cursor)?;
mapped_strings[i] = Some(DumpStringSpan { offset, len });
}
SPAN_VECTORLIKE => {
let vl_offset = read_u64(section, &mut cursor)?;
let vl_len = read_u64(section, &mut cursor)?;
mapped_veclikes[i] = Some(DumpVecLikeSpan {
offset: vl_offset,
len: vl_len,
});
if cursor >= section.len() {
return Err(DumpError::ImageFormatError(
"object-starts vectorlike slot flag truncated".into(),
));
}
let has_slots = section[cursor];
cursor += 1;
if has_slots != 0 {
let sl_offset = read_u64(section, &mut cursor)?;
let sl_len = read_u64(section, &mut cursor)?;
mapped_slots[i] = Some(DumpSlotSpan {
offset: sl_offset,
len: sl_len,
});
}
}
other => {
return Err(DumpError::ImageFormatError(format!(
"unknown object-starts span tag {other}"
)));
}
}
}
Ok(LoadedSpans {
mapped_cons,
mapped_floats,
mapped_strings,
mapped_veclikes,
mapped_slots,
})
}
fn write_u64(out: &mut Vec<u8>, value: u64) {
out.extend_from_slice(&value.to_le_bytes());
}
fn read_u64(data: &[u8], cursor: &mut usize) -> Result<u64, DumpError> {
if *cursor + 8 > data.len() {
return Err(DumpError::ImageFormatError(
"object-starts section truncated at u64".into(),
));
}
let val = u64::from_le_bytes(data[*cursor..*cursor + 8].try_into().unwrap());
*cursor += 8;
Ok(val)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn object_starts_round_trips() {
let heap = DumpTaggedHeap {
objects: vec![
DumpHeapObject::Cons {
car: DumpValue::Int(1),
cdr: DumpValue::Nil,
},
DumpHeapObject::Float(3.14),
DumpHeapObject::Free,
DumpHeapObject::Vector(vec![DumpValue::Nil, DumpValue::True]),
DumpHeapObject::Str {
data: DumpByteData::owned(b"hello".to_vec()),
size: 5,
size_byte: 5,
text_props: vec![],
},
],
mapped_cons: vec![Some(DumpConsSpan { offset: 0 }), None, None, None, None],
mapped_floats: vec![None, Some(DumpFloatSpan { offset: 32 }), None, None, None],
mapped_strings: vec![
None,
None,
None,
None,
Some(DumpStringSpan {
offset: 48,
len: 16,
}),
],
mapped_veclikes: vec![
None,
None,
None,
Some(DumpVecLikeSpan {
offset: 64,
len: 24,
}),
None,
],
mapped_slots: vec![
None,
None,
None,
Some(DumpSlotSpan {
offset: 88,
len: 16,
}),
None,
],
};
let bytes = build_object_starts(&heap).unwrap();
let spans = load_object_starts(&bytes).unwrap();
assert_eq!(spans.mapped_cons.len(), 5);
assert_eq!(spans.mapped_cons[0], Some(DumpConsSpan { offset: 0 }));
assert!(spans.mapped_cons[1].is_none());
assert_eq!(spans.mapped_floats[1], Some(DumpFloatSpan { offset: 32 }));
assert_eq!(
spans.mapped_strings[4],
Some(DumpStringSpan {
offset: 48,
len: 16
})
);
assert_eq!(
spans.mapped_veclikes[3],
Some(DumpVecLikeSpan {
offset: 64,
len: 24
})
);
assert_eq!(
spans.mapped_slots[3],
Some(DumpSlotSpan {
offset: 88,
len: 16
})
);
assert!(spans.mapped_cons[2].is_none()); }
}