Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add function for computing the postcard serialized size of a value. #86

Merged
merged 8 commits into from
Jan 26, 2023

Conversation

dignifiedquire
Copy link
Contributor

Closes #46.

Based on #66.

@netlify
Copy link

netlify bot commented Jan 25, 2023

Deploy Preview for cute-starship-2d9c9b canceled.

Name Link
🔨 Latest commit 9573f9f
🔍 Latest deploy log https://app.netlify.com/sites/cute-starship-2d9c9b/deploys/63d19e9514f9750009c1657c

@dignifiedquire
Copy link
Contributor Author

@jamesmunns I am wondering if the Sizer serializer could make use of the MAX_SIZE compile time information if it is available. Not quite sure how that could be integrated.

@jamesmunns
Copy link
Owner

Hey @dignifiedquire, thanks for putting this together! I have some thoughts, let me know what you think:

I think we could avoid all of the code duplication in src/ser/size.rs by instead using a Serialization flavor. Specifically, you could probably instead duplicate

postcard/src/ser/flavors.rs

Lines 134 to 209 in 7d07930

/// The `Slice` flavor is a storage flavor, storing the serialized (or otherwise modified) bytes into a plain
/// `[u8]` slice. The `Slice` flavor resolves into a sub-slice of the original slice buffer.
pub struct Slice<'a> {
start: *mut u8,
cursor: *mut u8,
end: *mut u8,
_pl: PhantomData<&'a [u8]>,
}
impl<'a> Slice<'a> {
/// Create a new `Slice` flavor from a given backing buffer
pub fn new(buf: &'a mut [u8]) -> Self {
Slice {
start: buf.as_mut_ptr(),
cursor: buf.as_mut_ptr(),
end: unsafe { buf.as_mut_ptr().add(buf.len()) },
_pl: PhantomData,
}
}
}
impl<'a> Flavor for Slice<'a> {
type Output = &'a mut [u8];
#[inline(always)]
fn try_push(&mut self, b: u8) -> Result<()> {
if self.cursor == self.end {
Err(Error::SerializeBufferFull)
} else {
unsafe {
self.cursor.write(b);
self.cursor = self.cursor.add(1);
}
Ok(())
}
}
#[inline(always)]
fn try_extend(&mut self, b: &[u8]) -> Result<()> {
let remain = (self.end as usize) - (self.cursor as usize);
let blen = b.len();
if blen > remain {
Err(Error::SerializeBufferFull)
} else {
unsafe {
core::ptr::copy_nonoverlapping(b.as_ptr(), self.cursor, blen);
self.cursor = self.cursor.add(blen);
}
Ok(())
}
}
fn finalize(self) -> Result<Self::Output> {
let used = (self.cursor as usize) - (self.start as usize);
let sli = unsafe { core::slice::from_raw_parts_mut(self.start, used) };
Ok(sli)
}
}
impl<'a> Index<usize> for Slice<'a> {
type Output = u8;
fn index(&self, idx: usize) -> &u8 {
let len = (self.end as usize) - (self.start as usize);
assert!(idx < len);
unsafe { &*self.start.add(idx) }
}
}
impl<'a> IndexMut<usize> for Slice<'a> {
fn index_mut(&mut self, idx: usize) -> &mut u8 {
let len = (self.end as usize) - (self.start as usize);
assert!(idx < len);
unsafe { &mut *self.start.add(idx) }
}
}
, but don't actually write to the buffer, just increment a counter.

I think this will be more maintainable than remembering to keep the main serializer and the size serializer in sync, as flavors have a much simpler API, and I think cover what is needed here.

This MAY not play nice with things like the Cobs flavor, which expect to be able to "reach back" to serialized data for filling in placeholder values, though it looks like the interaction is "write only".

If you're not sure how to do this, lemme know, and I can either help you out, or take this on at a later date.

I don't think the current implementation would work with say, "to_slice" and "to_slice_cobs", because they will have different "on the wire" sizes. You really only can accurately determine the size with the "right" serializer flavor taken into account. Again - I think this should be handled by using a counting serializer, instead of the flavor being Cobs<Slice<'a>> you'd have Cobs<Counting>.

I am wondering if the Sizer serializer could make use of the MAX_SIZE compile time information if it is available.

So, I touch on this in #46 (comment), but there are really two things going on here:

  • What is the MAXIMUM size this could serialize to (statically knowable, with hints in some cases)
  • What is the EXACT size THIS VALUE will serialize to (not statically knowable)

The latter will have a larger runtime cost to handle, but using something like u32::MAX_SIZE will give you 5 bytes, while serializing 42u32 will only take one byte. I'd prefer to make the distinction clearly in the API:

  • If you want the max size, instantly, and are okay with that being >= the actual size, use MAX_SIZE.
  • If you want the actual size, and are willing to work for it, use the size API.

I think at least most embedded folks will end up using MAX_SIZE + to_slice to make sure there is enough size to serialize, and determine the actual size "for free" while serializing.

@dignifiedquire
Copy link
Contributor Author

I think at least most embedded folks will end up using MAX_SIZE + to_slice to make sure there is enough size to serialize, and determine the actual size "for free" while serializing.

The tricky piece for my usage here is that I would love to have the MAX_SIZE + "length of the slices in my structs", because they are not limited in size, but because they are embedded I can't use MAX_SIZE at all currently. I guess I could use the approach you suggested with max_size = 0 annotation, and then manually add the runtime knowledge.

@dignifiedquire
Copy link
Contributor Author

I think this will be more maintainable than remembering to keep the main serializer and the size serializer in sync, as flavors have a much simpler API, and I think cover what is needed here.

Yes very much, looks like this was pretty straightforward :)

@@ -410,3 +410,29 @@ where
self.flav.finalize()
}
}

/// The `Size` flavor is a measurement flavor, storing the serialized bytes length into a plain.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a missing thought here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@jamesmunns
Copy link
Owner

One minor comment, otherwise overall looks good! If you have time to add some doctest examples, it would be appreciated, but we can merge without if you're in a rush.

@dignifiedquire
Copy link
Contributor Author

will update tomorrow

@dignifiedquire
Copy link
Contributor Author

added a small doctest

Copy link
Owner

@jamesmunns jamesmunns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks so much!

@jamesmunns jamesmunns merged commit 08f18dc into jamesmunns:main Jan 26, 2023
@dignifiedquire dignifiedquire deleted the serialized-size branch January 26, 2023 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

pre-compute size of serialized data
3 participants