From ffbdb9ce397a6408d5a91cbdcbaf4e13b0c3ba0b Mon Sep 17 00:00:00 2001 From: metamuffin Date: Tue, 6 Jan 2026 14:56:20 +0100 Subject: Multi fields; object buffer constructor; unit tests --- common/object/src/buffer.rs | 50 +++++++++++++++++++++++++++++++++ common/object/src/lib.rs | 67 ++++++++++++++++++++++++++++++++------------- common/object/src/tests.rs | 39 ++++++++++++++++++++++++++ common/object/src/value.rs | 62 ++++++++++++++++++++++++++++++++--------- 4 files changed, 186 insertions(+), 32 deletions(-) create mode 100644 common/object/src/buffer.rs create mode 100644 common/object/src/tests.rs (limited to 'common/object') diff --git a/common/object/src/buffer.rs b/common/object/src/buffer.rs new file mode 100644 index 0000000..56b8caf --- /dev/null +++ b/common/object/src/buffer.rs @@ -0,0 +1,50 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin +*/ + +use crate::{Object, Tag, ValueStore}; + +pub struct ObjectBuffer(pub Vec); + +impl ObjectBuffer { + pub fn empty() -> Self { + Self(vec![0]) + } + pub fn as_object<'a>(&'a self) -> Object<'a> { + Object::load(&self.0).unwrap() + } + pub fn new(fields: &mut [(Tag, &dyn ValueStore)]) -> ObjectBuffer { + let mut tags = Vec::new(); + let mut offsets = Vec::new(); + let mut values = Vec::new(); + fields.sort_by_key(|(t, _)| t.0); + let mut temp = Vec::new(); + for (tag, val) in fields { + tags.push(tag.0); + if val.is_aligned() { + offsets.push((values.len() as u32) << 2); + val.store_aligned(&mut values); + } else { + temp.clear(); + val.store_unaligned(&mut temp); + let mut pad = 0; + while temp.len() % 4 != 0 { + pad += 1; + temp.push(0); + } + offsets.push(((values.len() as u32) << 2) | pad); + values.extend(bytemuck::cast_slice(&temp)); // ok bc. temp length is a whole number of dwords + } + } + ObjectBuffer( + [tags.len() as u32] + .into_iter() + .chain(tags) + .chain(offsets) + .chain(values) + .collect(), + ) + } +} diff --git a/common/object/src/lib.rs b/common/object/src/lib.rs index 9f9e0be..831dee7 100644 --- a/common/object/src/lib.rs +++ b/common/object/src/lib.rs @@ -4,7 +4,11 @@ Copyright (C) 2026 metamuffin */ +mod buffer; +#[cfg(test)] +mod tests; mod value; +pub use buffer::*; pub use value::*; use std::marker::PhantomData; @@ -14,17 +18,7 @@ use std::marker::PhantomData; pub struct Tag(pub u32); pub struct TypedTag(pub Tag, pub PhantomData); -pub struct ObjectBuffer(pub Vec); - -impl ObjectBuffer { - pub fn new() -> Self { - Self(vec![0]) - } - pub fn as_object<'a>(&'a self) -> Object<'a> { - Object::load(&self.0).unwrap() - } -} - +#[derive(Debug)] pub struct Object<'a> { tags: &'a [u32], offsets: &'a [u32], @@ -33,25 +27,28 @@ pub struct Object<'a> { impl<'a> Object<'a> { pub fn load(buf: &'a [u32]) -> Option { let nf = *buf.get(0)? as usize; + if buf.len() < 1 + nf * 2 { + return None; + } Some(Self { tags: &buf[1..1 + nf], offsets: &buf[1 + nf..1 + nf + nf], values: &buf[1 + nf + nf..], }) } - fn find_field(&self, tag: Tag) -> Option { + pub fn find_field(&self, tag: Tag) -> Option { // using partition as binary search for the first field (instead of regular binary_search that returns any) - let first = self.tags.partition_point(|&x| x >= tag.0); + let first = self.tags.partition_point(|&x| x < tag.0); self.tags .get(first) .is_some_and(|&x| x == tag.0) .then_some(first) } fn get_aligned(&self, index: usize) -> Option<&[u32]> { - let start_raw = *self.offsets.get(index)?; + let start_raw = self.offsets[index]; let end_raw = self .offsets - .get(index) + .get(index + 1) .copied() .unwrap_or((self.values.len() as u32) << 2); @@ -61,10 +58,10 @@ impl<'a> Object<'a> { Some(&self.values[start as usize..end as usize]) } fn get_unaligned(&self, index: usize) -> Option<&[u8]> { - let start_raw = *self.offsets.get(index)?; + let start_raw = self.offsets[index]; let end_raw = self .offsets - .get(index) + .get(index + 1) .copied() .unwrap_or((self.values.len() as u32) << 2); @@ -75,12 +72,44 @@ impl<'a> Object<'a> { let values_u8: &[u8] = bytemuck::cast_slice(self.values); Some(&values_u8[start as usize..end as usize]) } - pub fn get<'b: 'a, T: Value<'b>>(&'b self, tag: TypedTag) -> Option { - let index = self.find_field(tag.0)?; + #[inline] + pub fn get_typed<'b: 'a, T: Value<'b>>(&'b self, index: usize) -> Option { if T::ALIGNED { T::load_aligned(self.get_aligned(index)?) } else { T::load_unaligned(self.get_unaligned(index)?) } } + pub fn get<'b: 'a, T: Value<'b>>(&'b self, tag: TypedTag) -> Option { + self.get_typed(self.find_field(tag.0)?) + } + pub fn iter<'b: 'a, T>(&'b self, tag: TypedTag) -> FieldIter<'b, T> { + FieldIter { + object: self, + index: self.tags.partition_point(|&x| x < tag.0.0), + tag: tag.0.0, + ty: PhantomData, + } + } +} + +pub struct FieldIter<'a, T> { + object: &'a Object<'a>, + index: usize, + tag: u32, + ty: PhantomData, +} +impl<'a, T: Value<'a>> Iterator for FieldIter<'a, T> { + type Item = T; + fn next(&mut self) -> Option { + if self.index >= self.object.tags.len() { + return None; + } + if self.object.tags[self.index] != self.tag { + return None; + } + let val = self.object.get_typed(self.index); + self.index += 1; + val + } } diff --git a/common/object/src/tests.rs b/common/object/src/tests.rs new file mode 100644 index 0000000..35a29ba --- /dev/null +++ b/common/object/src/tests.rs @@ -0,0 +1,39 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin +*/ +use crate::{ObjectBuffer, Tag, TypedTag}; +use std::marker::PhantomData; + +const NAME: TypedTag<&str> = TypedTag(Tag(15), PhantomData); +const AGE: TypedTag = TypedTag(Tag(13), PhantomData); +const FRIEND: TypedTag<&str> = TypedTag(Tag(54321), PhantomData); + +fn test_object() -> ObjectBuffer { + ObjectBuffer::new(&mut [ + (NAME.0, &"Bob"), + (AGE.0, &35_u32), + (FRIEND.0, &"Alice"), + (FRIEND.0, &"Charlie"), + ]) +} + +#[test] +fn read_single_field() { + let bob = test_object(); + let bob = bob.as_object(); + assert_eq!(bob.get(NAME), Some("Bob")); + assert_eq!(bob.get(AGE), Some(35)); +} + +#[test] +fn read_multi_field() { + let bob = test_object(); + let bob = bob.as_object(); + + let mut friends = bob.iter(FRIEND); + assert_eq!(friends.next(), Some("Alice")); + assert_eq!(friends.next(), Some("Charlie")); + assert_eq!(friends.next(), None); +} diff --git a/common/object/src/value.rs b/common/object/src/value.rs index 3a8b7df..d77d53a 100644 --- a/common/object/src/value.rs +++ b/common/object/src/value.rs @@ -4,9 +4,9 @@ Copyright (C) 2026 metamuffin */ -use crate::Object; +use crate::{Object, ObjectBuffer}; -pub trait Value<'a>: Sized { +pub trait Value<'a>: ValueStore + Sized { const ALIGNED: bool; fn load_aligned(buf: &'a [u32]) -> Option { let _ = buf; @@ -16,7 +16,11 @@ pub trait Value<'a>: Sized { let _ = buf; None } - fn store(&self, buf: &mut Vec); +} +pub trait ValueStore { + fn is_aligned(&self) -> bool; + fn store_aligned(&self, _buf: &mut Vec) {} + fn store_unaligned(&self, _buf: &mut Vec) {} fn size(&self) -> usize; } impl<'a> Value<'a> for &'a str { @@ -24,7 +28,12 @@ impl<'a> Value<'a> for &'a str { fn load_unaligned(buf: &'a [u8]) -> Option { str::from_utf8(buf).ok() } - fn store(&self, buf: &mut Vec) { +} +impl ValueStore for &str { + fn is_aligned(&self) -> bool { + false + } + fn store_unaligned(&self, buf: &mut Vec) { buf.extend(self.as_bytes()); } fn size(&self) -> usize { @@ -32,12 +41,17 @@ impl<'a> Value<'a> for &'a str { } } impl Value<'_> for u32 { - const ALIGNED: bool = false; + const ALIGNED: bool = true; fn load_aligned(buf: &[u32]) -> Option { buf.get(0).copied() } - fn store(&self, buf: &mut Vec) { - buf.extend(self.to_ne_bytes()); +} +impl ValueStore for u32 { + fn is_aligned(&self) -> bool { + true + } + fn store_aligned(&self, buf: &mut Vec) { + buf.push(*self); } fn size(&self) -> usize { 4 @@ -50,8 +64,14 @@ impl Value<'_> for u64 { let lo = *buf.get(1)? as u64; Some(hi << 32 | lo) } - fn store(&self, buf: &mut Vec) { - buf.extend(self.to_ne_bytes()); +} +impl ValueStore for u64 { + fn is_aligned(&self) -> bool { + true + } + fn store_aligned(&self, buf: &mut Vec) { + buf.push((self >> 32) as u32); + buf.push(*self as u32); } fn size(&self) -> usize { 8 @@ -62,12 +82,28 @@ impl<'a> Value<'a> for Object<'a> { fn load_aligned(buf: &'a [u32]) -> Option { Self::load(buf) } - fn store(&self, buf: &mut Vec) { - buf.extend(self.tags.iter().copied().map(u32::to_ne_bytes).flatten()); - buf.extend(self.offsets.iter().copied().map(u32::to_ne_bytes).flatten()); - buf.extend(self.values.iter().copied().map(u32::to_ne_bytes).flatten()); +} +impl ValueStore for Object<'_> { + fn is_aligned(&self) -> bool { + true + } + fn store_aligned(&self, buf: &mut Vec) { + buf.extend(self.tags); + buf.extend(self.offsets); + buf.extend(self.values); } fn size(&self) -> usize { (self.tags.len() + self.offsets.len() + self.values.len()) * size_of::() } } +impl ValueStore for ObjectBuffer { + fn is_aligned(&self) -> bool { + true + } + fn store_aligned(&self, buf: &mut Vec) { + buf.extend(&self.0); + } + fn size(&self) -> usize { + self.0.len() * 4 + } +} -- cgit v1.3