From 05530b2bc81035b65d3ffc04c3d9ad45b79d7c75 Mon Sep 17 00:00:00 2001 From: Adin Ackerman Date: Wed, 26 Aug 2026 18:00:40 -0700 Subject: [PATCH] serac: heapless vec --- Cargo.lock | 50 ++++++++- ci.sh | 4 +- serac/Cargo.toml | 7 +- serac/src/encoding/vanilla.rs | 3 + serac/src/encoding/vanilla/heapless.rs | 136 +++++++++++++++++++++++++ 5 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 serac/src/encoding/vanilla/heapless.rs diff --git a/Cargo.lock b/Cargo.lock index 4868841..65aceaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,6 +42,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cortex-m" version = "0.7.7" @@ -114,6 +120,12 @@ dependencies = [ "serac", ] +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + [[package]] name = "embedded-command-macros" version = "0.4.0" @@ -169,6 +181,34 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -297,16 +337,24 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serac" -version = "0.4.7" +version = "0.4.8" dependencies = [ "cortex-m", "cortex-m-rt", "defmt", "embedded-command-macros 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "fill-array", + "heapless", + "itertools", "panic-halt", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "syn" version = "1.0.109" diff --git a/ci.sh b/ci.sh index 4f86493..88bdc07 100755 --- a/ci.sh +++ b/ci.sh @@ -12,14 +12,14 @@ CRATES=("embedded-command-macros" "serac" "dispatch-bundle") for TARGET in "${TARGETS[@]}"; do rustup target add "$TARGET" for CRATE in "${CRATES[@]}"; do - cargo build -p "$CRATE" --target "$TARGET" + cargo build -p "$CRATE" --all-features --target "$TARGET" done done # tests for CRATE in "${CRATES[@]}"; do - cargo test -p "$CRATE" + cargo test -p "$CRATE" --all-features done # miri diff --git a/serac/Cargo.toml b/serac/Cargo.toml index 14285d4..625af5f 100644 --- a/serac/Cargo.toml +++ b/serac/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "serac" -version = "0.4.7" +version = "0.4.8" edition = "2024" description = "A static, modular, and light-weight serialization framework." license = "CC-BY-NC-SA-4.0" @@ -15,10 +15,15 @@ panic-halt = { version = "1.0.0", optional = true } cortex-m = { version = "0.7.7", optional = true } cortex-m-rt = { version = "0.7.3", optional = true } defmt = { version = "0.3.10", optional = true } +heapless = { version = "0.9.3", optional = true } + +[dev-dependencies] +itertools = { version = "0.15.0", default-features = false } [features] binary = ["dep:panic-halt", "dep:cortex-m", "dep:cortex-m-rt"] defmt = ["dep:defmt"] +heapless = ["dep:heapless"] [[bin]] name = "asm" diff --git a/serac/src/encoding/vanilla.rs b/serac/src/encoding/vanilla.rs index 2c594a4..1c87b1c 100644 --- a/serac/src/encoding/vanilla.rs +++ b/serac/src/encoding/vanilla.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "heapless")] +pub mod heapless; + use core::{marker::PhantomData, mem::MaybeUninit}; use fill_array::fill; diff --git a/serac/src/encoding/vanilla/heapless.rs b/serac/src/encoding/vanilla/heapless.rs new file mode 100644 index 0000000..600eded --- /dev/null +++ b/serac/src/encoding/vanilla/heapless.rs @@ -0,0 +1,136 @@ +//! Vanilla encoding implementations for types from the [`heapless`] crate. + +use heapless::Vec; + +use crate::{SerializeIter, Size, error}; + +pub type VecU8 = Vec; +pub type VecU16 = Vec; +pub type VecU32 = Vec; +pub type VecU64 = Vec; + +impl SerializeIter + for Vec +{ + fn ser<'a>( + &self, + dst: &mut crate::Buf< + impl Iterator::Word>, + >, + ) -> Result<(), crate::error::EndOfInput> + where + ::Word: 'a, + { + LenT::from_usize(self.len()).ser(dst)?; + + for e in self { + e.ser(dst)?; + } + + Ok(()) + } + + fn de<'a>( + src: &mut crate::Buf::Word>>, + ) -> Result + where + ::Word: 'a, + { + let mut vec = Vec::new(); + + let len = LenT::de(src)?; + + // invariant 0: encoded length must not exceed vec capacity + if len.into_usize() > N { + Err(error::Invalid)? + } + + for _ in 0..len.into_usize() { + // SAFETY: room is ensured by invariant 0 + unsafe { vec.push_unchecked(T::de(src)?) }; + } + + Ok(vec) + } +} + +// SAFETY: size of len + size of element * number of elements +unsafe impl Size for Vec { + const SIZE: usize = LenT::SIZE + T::SIZE * N; +} + +#[cfg(test)] +mod tests { + mod vec { + + use crate::{ + self as serac, SerializeIter, Size, buf, + encoding::vanilla::heapless::{VecU8, VecU32}, + }; + + #[test] + fn simple() { + let v = const { VecU8::::from_array([5, 4, 3, 2, 1, 0]) }; + + let mut buf = buf!(VecU8); + + v.serialize_iter(&mut buf).expect("vec should fit in buf"); + + let readback = VecU8::::deserialize_iter(&buf) + .expect("vec should deserialize successfully"); + + itertools::assert_equal(v, readback); + } + + #[test] + fn shorter() { + let v = const { VecU8::::from_array([5, 4, 3, 2, 1, 0]) }; + + let mut buf = buf!(VecU8); + + v.serialize_iter(&mut buf).expect("vec should fit in buf"); + + let readback = VecU8::::deserialize_iter(&buf) + .expect("vec should deserialize successfully"); + + itertools::assert_equal(v, readback); + } + + #[test] + fn too_short() { + let v = const { VecU8::::from_array([5, 4, 3, 2, 1, 0]) }; + + let mut buf = buf!(VecU8); + + v.serialize_iter(&mut buf).expect("vec should fit in buf"); + + let readback = VecU8::::deserialize_iter(&buf); + + assert!( + readback.is_err(), + "expected deserialization to fail since the vec has an insufficient capacity", + ); + } + + #[test] + fn buf_too_small() { + let v = const { VecU8::::from_array([5, 4, 3, 2, 1, 0]) }; + + let mut buf = buf!(VecU8); + + assert!( + v.serialize_iter(&mut buf).is_err(), + "expected serialization to fail since the buffer has an insufficient capacity", + ); + } + + #[test] + fn len_type() { + assert_eq!( + VecU32::<(), 0>::SIZE, + ::SIZE, + "expected empty vec with u32 length type to be the same size as u32", + ); + } + } +}