From 7c3a19b3bdf3eb538c335eda77a2ab10a2ba288b Mon Sep 17 00:00:00 2001 From: nuskey8 Date: Sat, 29 Aug 2026 16:44:58 +0900 Subject: [PATCH 1/6] optimize: write_u8 --- zerompk/src/write.rs | 29 +++++++++++++++++++++++++++++ zerompk/src/write_macro.rs | 9 --------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/zerompk/src/write.rs b/zerompk/src/write.rs index c2daea9..cadcabc 100644 --- a/zerompk/src/write.rs +++ b/zerompk/src/write.rs @@ -123,6 +123,16 @@ impl<'a> SliceWriter<'a> { } impl<'a> Write for SliceWriter<'a> { + #[inline(always)] + fn write_u8(&mut self, value: u8) -> Result<()> { + if value <= POS_FIXINT_END { + *self.take_array::<1>()? = [value]; + } else { + *self.take_array::<2>()? = [UINT8_MARKER, value]; + } + Ok(()) + } + impl_write_methods! { write = |writer, data| { match data { @@ -213,6 +223,16 @@ impl VecWriter { } impl Write for VecWriter { + #[inline(always)] + fn write_u8(&mut self, value: u8) -> Result<()> { + if value <= POS_FIXINT_END { + self.buffer.push(value); + } else { + self.buffer.extend_from_slice(&[UINT8_MARKER, value]); + } + Ok(()) + } + impl_write_methods! { write = |writer, data| { if let [byte] = data { @@ -274,6 +294,15 @@ impl IOWriter { #[cfg(feature = "std")] impl Write for IOWriter { + #[inline(always)] + fn write_u8(&mut self, value: u8) -> Result<()> { + if value <= POS_FIXINT_END { + self.write_all(&[value]) + } else { + self.write_all(&[UINT8_MARKER, value]) + } + } + impl_write_methods! { write = |writer, data| writer.write_all(data), write_parts = |writer, header, payload| { diff --git a/zerompk/src/write_macro.rs b/zerompk/src/write_macro.rs index 20031a7..73381c6 100644 --- a/zerompk/src/write_macro.rs +++ b/zerompk/src/write_macro.rs @@ -22,15 +22,6 @@ macro_rules! impl_write_methods { $write } - #[inline(always)] - fn write_u8(&mut self, value: u8) -> Result<()> { - let bytes = [UINT8_MARKER, value]; - let len = if value <= POS_FIXINT_END { 1 } else { 2 }; - let $writer = &mut *self; - let $data = if len == 1 { &bytes[1..] } else { &bytes }; - $write - } - #[inline(always)] fn write_u16(&mut self, value: u16) -> Result<()> { match value { From 78873602c30142218c60b1d096a254af4af31227 Mon Sep 17 00:00:00 2001 From: nuskey8 Date: Sat, 29 Aug 2026 16:50:50 +0900 Subject: [PATCH 2/6] optimize: string/binary header construction --- zerompk/src/write_macro.rs | 75 ++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/zerompk/src/write_macro.rs b/zerompk/src/write_macro.rs index 73381c6..60bddbd 100644 --- a/zerompk/src/write_macro.rs +++ b/zerompk/src/write_macro.rs @@ -339,57 +339,68 @@ macro_rules! impl_write_methods { #[inline(always)] fn write_string(&mut self, value: &str) -> Result<()> { let len = value.len(); - let mut header = [0; 5]; - let header_len = match len { + match len { 0..=31 => { - header[0] = FIXSTR_START | len as u8; - 1 + let header = [FIXSTR_START | len as u8]; + let $parts_writer = &mut *self; + let $header = header.as_slice(); + let $payload = value.as_bytes(); + $write_parts } 32..=255 => { - header[..2].copy_from_slice(&[STR8_MARKER, len as u8]); - 2 + let header = [STR8_MARKER, len as u8]; + let $parts_writer = &mut *self; + let $header = header.as_slice(); + let $payload = value.as_bytes(); + $write_parts } 256..=65535 => { - header[0] = STR16_MARKER; - header[1..3].copy_from_slice(&(len as u16).to_be_bytes()); - 3 + let [a, b] = (len as u16).to_be_bytes(); + let header = [STR16_MARKER, a, b]; + let $parts_writer = &mut *self; + let $header = header.as_slice(); + let $payload = value.as_bytes(); + $write_parts } _ => { - header[0] = STR32_MARKER; - header[1..].copy_from_slice(&(len as u32).to_be_bytes()); - 5 + let [a, b, c, d] = (len as u32).to_be_bytes(); + let header = [STR32_MARKER, a, b, c, d]; + let $parts_writer = &mut *self; + let $header = header.as_slice(); + let $payload = value.as_bytes(); + $write_parts } - }; - let $parts_writer = &mut *self; - let $header = &header[..header_len]; - let $payload = value.as_bytes(); - $write_parts + } } #[inline(always)] fn write_binary(&mut self, value: &[u8]) -> Result<()> { let len = value.len(); - let mut header = [0; 5]; - let header_len = match len { + match len { 0..=255 => { - header[..2].copy_from_slice(&[BIN8_MARKER, len as u8]); - 2 + let header = [BIN8_MARKER, len as u8]; + let $parts_writer = &mut *self; + let $header = header.as_slice(); + let $payload = value; + $write_parts } 256..=65535 => { - header[0] = BIN16_MARKER; - header[1..3].copy_from_slice(&(len as u16).to_be_bytes()); - 3 + let [a, b] = (len as u16).to_be_bytes(); + let header = [BIN16_MARKER, a, b]; + let $parts_writer = &mut *self; + let $header = header.as_slice(); + let $payload = value; + $write_parts } _ => { - header[0] = BIN32_MARKER; - header[1..].copy_from_slice(&(len as u32).to_be_bytes()); - 5 + let [a, b, c, d] = (len as u32).to_be_bytes(); + let header = [BIN32_MARKER, a, b, c, d]; + let $parts_writer = &mut *self; + let $header = header.as_slice(); + let $payload = value; + $write_parts } - }; - let $parts_writer = &mut *self; - let $header = &header[..header_len]; - let $payload = value; - $write_parts + } } #[inline(always)] From 0c92b8500457ddb874828be1a535110f503e40c6 Mon Sep 17 00:00:00 2001 From: nuskey8 Date: Sat, 29 Aug 2026 17:37:38 +0900 Subject: [PATCH 3/6] optimize: write_static_string() --- zerompk/src/write.rs | 23 +++++++++++++++++++++++ zerompk/tests/derive.rs | 18 ++++++++++++++++++ zerompk_derive/src/lib.rs | 23 ++++++++++++++++++++++- 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/zerompk/src/write.rs b/zerompk/src/write.rs index cadcabc..96c0068 100644 --- a/zerompk/src/write.rs +++ b/zerompk/src/write.rs @@ -67,6 +67,12 @@ pub trait Write { /// Writes a UTF-8 string. fn write_string(&mut self, s: &str) -> Result<()>; + /// Writes a static UTF-8 string literal. + #[inline(always)] + fn write_static_string(&mut self, value: &'static str, _encoded: &'static [u8]) -> Result<()> { + self.write_string(value) + } + /// Writes a binary blob. fn write_binary(&mut self, data: &[u8]) -> Result<()>; @@ -123,6 +129,12 @@ impl<'a> SliceWriter<'a> { } impl<'a> Write for SliceWriter<'a> { + #[inline(always)] + fn write_static_string(&mut self, _value: &'static str, encoded: &'static [u8]) -> Result<()> { + self.take_slice(encoded.len())?.copy_from_slice(encoded); + Ok(()) + } + #[inline(always)] fn write_u8(&mut self, value: u8) -> Result<()> { if value <= POS_FIXINT_END { @@ -223,6 +235,12 @@ impl VecWriter { } impl Write for VecWriter { + #[inline(always)] + fn write_static_string(&mut self, _value: &'static str, encoded: &'static [u8]) -> Result<()> { + self.buffer.extend_from_slice(encoded); + Ok(()) + } + #[inline(always)] fn write_u8(&mut self, value: u8) -> Result<()> { if value <= POS_FIXINT_END { @@ -294,6 +312,11 @@ impl IOWriter { #[cfg(feature = "std")] impl Write for IOWriter { + #[inline(always)] + fn write_static_string(&mut self, _value: &'static str, encoded: &'static [u8]) -> Result<()> { + self.write_all(encoded) + } + #[inline(always)] fn write_u8(&mut self, value: u8) -> Result<()> { if value <= POS_FIXINT_END { diff --git a/zerompk/tests/derive.rs b/zerompk/tests/derive.rs index b806d4a..e615338 100644 --- a/zerompk/tests/derive.rs +++ b/zerompk/tests/derive.rs @@ -52,6 +52,24 @@ struct LongMapKeyPoint { y: i32, } +#[derive(ToMessagePack, FromMessagePack, Debug, PartialEq)] +#[msgpack(map)] +struct Str8MapKey { + #[msgpack(key = "abcdefghijklmnopqrstuvwxyz123456")] + value: u8, +} + +#[test] +fn derive_map_preencodes_str8_key() { + let value = Str8MapKey { value: 42 }; + let data = zerompk::to_msgpack_vec(&value).unwrap(); + let mut expected = vec![0x81, 0xd9, 32]; + expected.extend_from_slice(b"abcdefghijklmnopqrstuvwxyz123456"); + expected.push(42); + assert_eq!(data, expected); + assert_eq!(zerompk::from_msgpack::(&data).unwrap(), value); +} + #[derive(ToMessagePack, FromMessagePack, Debug, PartialEq)] struct UnitStruct; diff --git a/zerompk_derive/src/lib.rs b/zerompk_derive/src/lib.rs index 728f058..b071f16 100644 --- a/zerompk_derive/src/lib.rs +++ b/zerompk_derive/src/lib.rs @@ -653,6 +653,26 @@ fn should_use_bin(ty: &Type, cfg: Option<&FieldConfig>) -> bool { cfg.and_then(|v| v.as_bytes).unwrap_or(true) } +fn encode_static_string(value: &LitStr) -> proc_macro2::Literal { + let string = value.value(); + let bytes = string.as_bytes(); + let mut encoded = Vec::with_capacity(bytes.len() + 5); + match bytes.len() { + 0..=31 => encoded.push(0xa0 | bytes.len() as u8), + 32..=255 => encoded.extend_from_slice(&[0xd9, bytes.len() as u8]), + 256..=65535 => { + encoded.push(0xda); + encoded.extend_from_slice(&(bytes.len() as u16).to_be_bytes()); + } + _ => { + encoded.push(0xdb); + encoded.extend_from_slice(&(bytes.len() as u32).to_be_bytes()); + } + } + encoded.extend_from_slice(bytes); + proc_macro2::Literal::byte_string(&encoded) +} + fn build_read_expr(ty: &Type, cfg: Option<&FieldConfig>) -> proc_macro2::TokenStream { if is_ref_str(ty) { quote! { @@ -1197,6 +1217,7 @@ fn expand_map_struct(data: &DataStruct, allow_unknown_fields: bool) -> Result>()?; let (field_indices, key_lits) = parse_named_map_keys(fields, &field_configs)?; + let encoded_key_lits: Vec<_> = key_lits.iter().map(encode_static_string).collect(); let count = field_indices.len(); let names: Vec<_> = field_indices .iter() @@ -1251,7 +1272,7 @@ fn expand_map_struct(data: &DataStruct, allow_unknown_fields: bool) -> Result Date: Sat, 29 Aug 2026 18:06:26 +0900 Subject: [PATCH 4/6] optimize: slice serialization --- zerompk/src/impl.rs | 52 +++--- zerompk/src/lib.rs | 14 ++ zerompk/src/write.rs | 371 ++++++++++++++++++++++++++++++++++++++ zerompk/tests/derive.rs | 68 +++++++ zerompk_derive/src/lib.rs | 119 ++++++++++-- 5 files changed, 584 insertions(+), 40 deletions(-) diff --git a/zerompk/src/impl.rs b/zerompk/src/impl.rs index d8f3fe5..1422679 100644 --- a/zerompk/src/impl.rs +++ b/zerompk/src/impl.rs @@ -9,7 +9,7 @@ use core::hash::Hash; // ------------------------------------------------------------------------------- macro_rules! impl_scalar { - ($ty:ty, $write_fn:ident, $read_fn:ident) => { + ($ty:ty, $write_fn:ident, $write_slice_fn:ident, $read_fn:ident) => { impl<'a> FromMessagePack<'a> for $ty { #[inline(always)] fn read>(reader: &mut R) -> crate::Result @@ -25,21 +25,26 @@ macro_rules! impl_scalar { fn write(&self, writer: &mut W) -> crate::Result<()> { writer.$write_fn(*self) } + + #[inline(always)] + fn write_slice(values: &[Self], writer: &mut W) -> crate::Result<()> { + writer.$write_slice_fn(values) + } } }; } -impl_scalar!(bool, write_boolean, read_boolean); -impl_scalar!(i8, write_i8, read_i8); -impl_scalar!(i16, write_i16, read_i16); -impl_scalar!(i32, write_i32, read_i32); -impl_scalar!(i64, write_i64, read_i64); -impl_scalar!(u8, write_u8, read_u8); -impl_scalar!(u16, write_u16, read_u16); -impl_scalar!(u32, write_u32, read_u32); -impl_scalar!(u64, write_u64, read_u64); -impl_scalar!(f32, write_f32, read_f32); -impl_scalar!(f64, write_f64, read_f64); +impl_scalar!(bool, write_boolean, write_boolean_slice, read_boolean); +impl_scalar!(i8, write_i8, write_i8_slice, read_i8); +impl_scalar!(i16, write_i16, write_i16_slice, read_i16); +impl_scalar!(i32, write_i32, write_i32_slice, read_i32); +impl_scalar!(i64, write_i64, write_i64_slice, read_i64); +impl_scalar!(u8, write_u8, write_u8_slice, read_u8); +impl_scalar!(u16, write_u16, write_u16_slice, read_u16); +impl_scalar!(u32, write_u32, write_u32_slice, read_u32); +impl_scalar!(u64, write_u64, write_u64_slice, read_u64); +impl_scalar!(f32, write_f32, write_f32_slice, read_f32); +impl_scalar!(f64, write_f64, write_f64_slice, read_f64); impl<'a> FromMessagePack<'a> for usize { #[inline(always)] @@ -180,10 +185,7 @@ impl ToMessagePack for [T] { #[inline(always)] fn write(&self, writer: &mut W) -> crate::Result<()> { writer.write_array_len(self.len())?; - for item in self { - item.write(writer)?; - } - Ok(()) + T::write_slice(self, writer) } } @@ -230,10 +232,7 @@ impl ToMessagePack for [T; N] { #[inline(always)] fn write(&self, writer: &mut W) -> crate::Result<()> { writer.write_array_len(N)?; - for item in self { - item.write(writer)?; - } - Ok(()) + T::write_slice(self, writer) } } @@ -394,11 +393,7 @@ impl<'a, T: FromMessagePack<'a>> FromMessagePack<'a> for alloc::vec::Vec { impl ToMessagePack for alloc::vec::Vec { #[inline(always)] fn write(&self, writer: &mut W) -> crate::Result<()> { - writer.write_array_len(self.len())?; - for item in self { - item.write(writer)?; - } - Ok(()) + self.as_slice().write(writer) } } @@ -421,10 +416,9 @@ impl<'a, T: FromMessagePack<'a>> FromMessagePack<'a> for alloc::collections::Vec impl ToMessagePack for alloc::collections::VecDeque { fn write(&self, writer: &mut W) -> crate::Result<()> { writer.write_array_len(self.len())?; - for item in self { - item.write(writer)?; - } - Ok(()) + let (front, back) = self.as_slices(); + T::write_slice(front, writer)?; + T::write_slice(back, writer) } } diff --git a/zerompk/src/lib.rs b/zerompk/src/lib.rs index 81b974b..85e4566 100644 --- a/zerompk/src/lib.rs +++ b/zerompk/src/lib.rs @@ -50,6 +50,20 @@ impl FromMessagePackOwned for T where T: for<'a> FromMessagePack<'a> {} pub trait ToMessagePack { /// Writes the MessagePack representation of this value into the provided writer. fn write(&self, writer: &mut W) -> Result<()>; + + /// Writes a contiguous slice of values. Primitive implementations override this + /// to let writers reserve capacity and encode the whole slice in one operation. + #[doc(hidden)] + #[inline(always)] + fn write_slice(values: &[Self], writer: &mut W) -> Result<()> + where + Self: Sized, + { + for value in values { + value.write(writer)?; + } + Ok(()) + } } /// Deserializes a value of type `T` from a MessagePack-encoded byte slice. diff --git a/zerompk/src/write.rs b/zerompk/src/write.rs index 96c0068..847bc41 100644 --- a/zerompk/src/write.rs +++ b/zerompk/src/write.rs @@ -6,6 +6,124 @@ use crate::{Error, Result, consts::*}; const MAX_CONTAINER_PREALLOC: usize = 4 * 1024; +#[inline(always)] +unsafe fn encode_unsigned_at(output: *mut u8, value: u64) -> usize { + unsafe { + if value <= POS_FIXINT_END as u64 { + output.write(value as u8); + 1 + } else if value <= u8::MAX as u64 { + output.write(UINT8_MARKER); + output.add(1).write(value as u8); + 2 + } else if value <= u16::MAX as u64 { + output.write(UINT16_MARKER); + output + .add(1) + .cast::() + .write_unaligned((value as u16).to_be()); + 3 + } else if value <= u32::MAX as u64 { + output.write(UINT32_MARKER); + output + .add(1) + .cast::() + .write_unaligned((value as u32).to_be()); + 5 + } else { + output.write(UINT64_MARKER); + output.add(1).cast::().write_unaligned(value.to_be()); + 9 + } + } +} + +#[inline(always)] +unsafe fn encode_signed_at(output: *mut u8, value: i64, width: usize) -> usize { + unsafe { + if (0..=127).contains(&value) || (-32..=-1).contains(&value) { + output.write(value as u8); + 1 + } else if width == 1 || (-128..=127).contains(&value) { + output.write(INT8_MARKER); + output.add(1).write(value as u8); + 2 + } else if width == 2 || (-32768..=32767).contains(&value) { + output.write(INT16_MARKER); + output + .add(1) + .cast::() + .write_unaligned((value as i16).to_be()); + 3 + } else if width == 4 || (-2147483648..=2147483647).contains(&value) { + output.write(INT32_MARKER); + output + .add(1) + .cast::() + .write_unaligned((value as i32).to_be()); + 5 + } else { + output.write(INT64_MARKER); + output.add(1).cast::().write_unaligned(value.to_be()); + 9 + } + } +} + +macro_rules! impl_unsigned_slice { + ($method:ident, $scalar:ident, $ty:ty, $max_len:expr) => { + #[inline(always)] + fn $method(&mut self, values: &[$ty]) -> Result<()> { + let Some(max_len) = values.len().checked_mul($max_len) else { + return Err(Error::BufferTooSmall); + }; + if max_len > self.buffer.len() - self.pos { + for &value in values { + self.$scalar(value)?; + } + return Ok(()); + } + + let output = unsafe { self.buffer.as_mut_ptr().add(self.pos) }; + let mut written = 0; + for &value in values { + // SAFETY: the fast path reserved the maximum encoded length + // for every remaining value. + written += unsafe { encode_unsigned_at(output.add(written), value as u64) }; + } + self.pos += written; + Ok(()) + } + }; +} + +macro_rules! impl_signed_slice { + ($method:ident, $scalar:ident, $ty:ty, $max_len:expr, $width:expr) => { + #[inline(always)] + fn $method(&mut self, values: &[$ty]) -> Result<()> { + let Some(max_len) = values.len().checked_mul($max_len) else { + return Err(Error::BufferTooSmall); + }; + if max_len > self.buffer.len() - self.pos { + for &value in values { + self.$scalar(value)?; + } + return Ok(()); + } + + let output = unsafe { self.buffer.as_mut_ptr().add(self.pos) }; + let mut written = 0; + for &value in values { + // SAFETY: the fast path reserved the maximum encoded length + // for every remaining value. + written += unsafe { encode_signed_at(output.add(written), value as i64, $width) }; + } + self.pos += written; + Ok(()) + } + }; +} + /// A trait for writing MessagePack-encoded data. /// /// ## Examples @@ -34,36 +152,168 @@ pub trait Write { /// Writes a boolean value. fn write_boolean(&mut self, b: bool) -> Result<()>; + /// Writes a slice of boolean values without an array header. + #[inline(always)] + fn write_boolean_slice(&mut self, values: &[bool]) -> Result<()> + where + Self: Sized, + { + for &value in values { + self.write_boolean(value)?; + } + Ok(()) + } + /// Writes an unsigned 8-bit integer. fn write_u8(&mut self, u: u8) -> Result<()>; + /// Writes a slice of unsigned 8-bit integers without an array header. + #[inline(always)] + fn write_u8_slice(&mut self, values: &[u8]) -> Result<()> + where + Self: Sized, + { + for &value in values { + self.write_u8(value)?; + } + Ok(()) + } + /// Writes an unsigned 16-bit integer. fn write_u16(&mut self, u: u16) -> Result<()>; + /// Writes a slice of unsigned 16-bit integers without an array header. + #[inline(always)] + fn write_u16_slice(&mut self, values: &[u16]) -> Result<()> + where + Self: Sized, + { + for &value in values { + self.write_u16(value)?; + } + Ok(()) + } + /// Writes an unsigned 32-bit integer. fn write_u32(&mut self, u: u32) -> Result<()>; + /// Writes a slice of unsigned 32-bit integers without an array header. + #[inline(always)] + fn write_u32_slice(&mut self, values: &[u32]) -> Result<()> + where + Self: Sized, + { + for &value in values { + self.write_u32(value)?; + } + Ok(()) + } + /// Writes an unsigned 64-bit integer. fn write_u64(&mut self, u: u64) -> Result<()>; + /// Writes a slice of unsigned 64-bit integers without an array header. + #[inline(always)] + fn write_u64_slice(&mut self, values: &[u64]) -> Result<()> + where + Self: Sized, + { + for &value in values { + self.write_u64(value)?; + } + Ok(()) + } + /// Writes a signed 8-bit integer. fn write_i8(&mut self, i: i8) -> Result<()>; + /// Writes a slice of signed 8-bit integers without an array header. + #[inline(always)] + fn write_i8_slice(&mut self, values: &[i8]) -> Result<()> + where + Self: Sized, + { + for &value in values { + self.write_i8(value)?; + } + Ok(()) + } + /// Writes a signed 16-bit integer. fn write_i16(&mut self, i: i16) -> Result<()>; + /// Writes a slice of signed 16-bit integers without an array header. + #[inline(always)] + fn write_i16_slice(&mut self, values: &[i16]) -> Result<()> + where + Self: Sized, + { + for &value in values { + self.write_i16(value)?; + } + Ok(()) + } + /// Writes a signed 32-bit integer. fn write_i32(&mut self, i: i32) -> Result<()>; + /// Writes a slice of signed 32-bit integers without an array header. + #[inline(always)] + fn write_i32_slice(&mut self, values: &[i32]) -> Result<()> + where + Self: Sized, + { + for &value in values { + self.write_i32(value)?; + } + Ok(()) + } + /// Writes a signed 64-bit integer. fn write_i64(&mut self, i: i64) -> Result<()>; + /// Writes a slice of signed 64-bit integers without an array header. + #[inline(always)] + fn write_i64_slice(&mut self, values: &[i64]) -> Result<()> + where + Self: Sized, + { + for &value in values { + self.write_i64(value)?; + } + Ok(()) + } + /// Writes a 32-bit floating-point number. fn write_f32(&mut self, f: f32) -> Result<()>; + /// Writes fixed-size 32-bit floating-point values without an array header. + #[inline(always)] + fn write_f32_slice(&mut self, values: &[f32]) -> Result<()> + where + Self: Sized, + { + for &value in values { + self.write_f32(value)?; + } + Ok(()) + } + /// Writes a 64-bit floating-point number. fn write_f64(&mut self, f: f64) -> Result<()>; + /// Writes a slice of 64-bit floating-point values without an array header. + #[inline(always)] + fn write_f64_slice(&mut self, values: &[f64]) -> Result<()> + where + Self: Sized, + { + for &value in values { + self.write_f64(value)?; + } + Ok(()) + } + /// Writes a UTF-8 string. fn write_string(&mut self, s: &str) -> Result<()>; @@ -135,6 +385,29 @@ impl<'a> Write for SliceWriter<'a> { Ok(()) } + #[inline(always)] + fn write_boolean_slice(&mut self, values: &[bool]) -> Result<()> { + let output = self.take_slice(values.len())?.as_mut_ptr(); + for (index, &value) in values.iter().enumerate() { + // SAFETY: `take_slice` reserved one byte for every input value. + unsafe { + output + .add(index) + .write(if value { TRUE_MARKER } else { FALSE_MARKER }); + } + } + Ok(()) + } + + impl_unsigned_slice!(write_u8_slice, write_u8, u8, 2); + impl_unsigned_slice!(write_u16_slice, write_u16, u16, 3); + impl_unsigned_slice!(write_u32_slice, write_u32, u32, 5); + impl_unsigned_slice!(write_u64_slice, write_u64, u64, 9); + impl_signed_slice!(write_i8_slice, write_i8, i8, 2, 1); + impl_signed_slice!(write_i16_slice, write_i16, i16, 3, 2); + impl_signed_slice!(write_i32_slice, write_i32, i32, 5, 4); + impl_signed_slice!(write_i64_slice, write_i64, i64, 9, 8); + #[inline(always)] fn write_u8(&mut self, value: u8) -> Result<()> { if value <= POS_FIXINT_END { @@ -145,6 +418,47 @@ impl<'a> Write for SliceWriter<'a> { Ok(()) } + #[inline(always)] + fn write_f32_slice(&mut self, values: &[f32]) -> Result<()> { + let len = match values.len().checked_mul(5) { + Some(len) => len, + None => return Err(Error::BufferTooSmall), + }; + let output = self.take_slice(len)?.as_mut_ptr(); + for (index, &value) in values.iter().enumerate() { + // SAFETY: `take_slice` reserved exactly five bytes per value. + // Each iteration writes its own marker byte and unaligned u32 payload. + unsafe { + let ptr = output.add(index * 5); + ptr.write(FLOAT32_MARKER); + ptr.add(1) + .cast::() + .write_unaligned(value.to_bits().to_be()); + } + } + Ok(()) + } + + #[inline(always)] + fn write_f64_slice(&mut self, values: &[f64]) -> Result<()> { + let len = match values.len().checked_mul(9) { + Some(len) => len, + None => return Err(Error::BufferTooSmall), + }; + let output = self.take_slice(len)?.as_mut_ptr(); + for (index, &value) in values.iter().enumerate() { + // SAFETY: `take_slice` reserved exactly nine bytes per value. + unsafe { + let ptr = output.add(index * 9); + ptr.write(FLOAT64_MARKER); + ptr.add(1) + .cast::() + .write_unaligned(value.to_bits().to_be()); + } + } + Ok(()) + } + impl_write_methods! { write = |writer, data| { match data { @@ -335,3 +649,60 @@ impl Write for IOWriter { write_container = |writer, header, _reserve| writer.write_all(header), } } + +#[cfg(test)] +mod tests { + use alloc::vec; + + use super::{SliceWriter, Write}; + + #[test] + fn primitive_slice_writers_match_scalar_encoding() { + macro_rules! check_slice { + ($slice:ident, $scalar:ident, $values:expr) => {{ + let values = $values; + let mut scalar_output = vec![0; values.len() * 10 + 1]; + let mut scalar_writer = SliceWriter::new(&mut scalar_output); + for &value in &values { + scalar_writer.$scalar(value).unwrap(); + } + let scalar_len = scalar_writer.position(); + let expected = scalar_output[..scalar_len].to_vec(); + + // Variable-width integers take their scalar fallback here. + let mut exact = vec![0; expected.len()]; + let mut exact_writer = SliceWriter::new(&mut exact); + exact_writer.$slice(&values).unwrap(); + assert_eq!(exact_writer.position(), expected.len()); + assert_eq!(exact, expected); + + // A roomy buffer exercises the single-check fast path. + let mut roomy = vec![0; values.len() * 10 + 1]; + let mut roomy_writer = SliceWriter::new(&mut roomy); + roomy_writer.$slice(&values).unwrap(); + assert_eq!(roomy_writer.position(), expected.len()); + assert_eq!(&roomy[..expected.len()], expected); + }}; + } + + check_slice!(write_boolean_slice, write_boolean, [false, true]); + check_slice!(write_u8_slice, write_u8, [0, 127, 128, u8::MAX]); + check_slice!(write_u16_slice, write_u16, [0, 128, 256, u16::MAX]); + check_slice!(write_u32_slice, write_u32, [0, 256, 65536, u32::MAX]); + check_slice!(write_u64_slice, write_u64, [0, 65536, 1 << 32, u64::MAX]); + check_slice!(write_i8_slice, write_i8, [i8::MIN, -32, 0, i8::MAX]); + check_slice!(write_i16_slice, write_i16, [i16::MIN, -128, 128, i16::MAX]); + check_slice!( + write_i32_slice, + write_i32, + [i32::MIN, -32769, 32768, i32::MAX] + ); + check_slice!( + write_i64_slice, + write_i64, + [i64::MIN, -2147483649, 2147483648, i64::MAX] + ); + check_slice!(write_f32_slice, write_f32, [f32::MIN, -0.0, f32::MAX]); + check_slice!(write_f64_slice, write_f64, [f64::MIN, -0.0, f64::MAX]); + } +} diff --git a/zerompk/tests/derive.rs b/zerompk/tests/derive.rs index e615338..e191c78 100644 --- a/zerompk/tests/derive.rs +++ b/zerompk/tests/derive.rs @@ -7,6 +7,74 @@ struct PointArray { y: i32, } +#[derive(ToMessagePack, FromMessagePack, Debug, PartialEq)] +#[msgpack(array)] +struct PrimitiveVectors { + flags: Vec, + u8s: Vec, + u16s: Vec, + u32s: Vec, + u64s: Vec, + i8s: Vec, + i16s: Vec, + i32s: Vec, + i64s: Vec, + f32s: Vec, + f64s: Vec, +} + +#[test] +fn derive_primitive_vectors_use_standard_array_encoding_with_exact_buffer() { + let value = PrimitiveVectors { + flags: vec![true, false, true], + u8s: vec![0, 127, 128, u8::MAX], + u16s: vec![0, 128, 256, u16::MAX], + u32s: vec![0, 256, 65_536, u32::MAX], + u64s: vec![0, 65_536, 1 << 32, u64::MAX], + i8s: vec![i8::MIN, -32, 0, i8::MAX], + i16s: vec![i16::MIN, -128, 128, i16::MAX], + i32s: vec![i32::MIN, -32_768, 32_768, i32::MAX], + i64s: vec![i64::MIN, i32::MIN as i64 - 1, i64::MAX], + f32s: vec![f32::MIN, -0.0, f32::MAX], + f64s: vec![f64::MIN, -0.0, f64::MAX], + }; + let mut roomy_output = [0; 512]; + let len = zerompk::to_msgpack(&value, &mut roomy_output).unwrap(); + let mut exact_output = vec![0; len]; + let exact_len = zerompk::to_msgpack(&value, &mut exact_output).unwrap(); + assert_eq!(exact_len, len); + assert_eq!(exact_output, roomy_output[..len]); + assert_eq!( + zerompk::from_msgpack::(&exact_output).unwrap(), + value + ); +} + +#[derive(ToMessagePack)] +#[msgpack(array)] +struct PrimitiveSequences<'a> { + slice: &'a [i32], + array: [f64; 3], + cow: std::borrow::Cow<'a, [u32]>, +} + +#[test] +fn derive_primitive_slices_arrays_and_cows_use_standard_array_encoding() { + let slice = [i32::MIN, -1, 0, i32::MAX]; + let value = PrimitiveSequences { + slice: &slice, + array: [f64::MIN, -0.0, f64::MAX], + cow: std::borrow::Cow::Borrowed(&[0, 128, u32::MAX]), + }; + let mut output = [0; 128]; + let len = zerompk::to_msgpack(&value, &mut output).unwrap(); + + let expected_value = (value.slice, value.array, value.cow.as_ref()); + let mut expected = [0; 128]; + let expected_len = zerompk::to_msgpack(&expected_value, &mut expected).unwrap(); + assert_eq!(&output[..len], &expected[..expected_len]); +} + #[derive(ToMessagePack, FromMessagePack, Debug, PartialEq)] struct DefaultReprPoint { x: i32, diff --git a/zerompk_derive/src/lib.rs b/zerompk_derive/src/lib.rs index b071f16..ad047cd 100644 --- a/zerompk_derive/src/lib.rs +++ b/zerompk_derive/src/lib.rs @@ -617,6 +617,10 @@ fn is_cow_u8_slice(ty: &Type) -> bool { } fn is_vec_u8(ty: &Type) -> bool { + is_vec_of(ty, "u8") +} + +fn is_vec_of(ty: &Type, element: &str) -> bool { let Type::Path(type_path) = ty else { return false; }; @@ -638,7 +642,7 @@ fn is_vec_u8(ty: &Type) -> bool { return false; }; - path.path.is_ident("u8") + path.path.is_ident(element) }) } @@ -646,6 +650,62 @@ fn is_bin_type(ty: &Type) -> bool { is_ref_u8_slice(ty) || is_cow_u8_slice(ty) || is_vec_u8(ty) } +fn primitive_slice_method(ty: &Type) -> Option { + let Type::Path(path) = ty else { + return None; + }; + if path.qself.is_some() { + return None; + } + + let method = match path.path.get_ident()?.to_string().as_str() { + "bool" => "write_boolean_slice", + "u8" => "write_u8_slice", + "u16" => "write_u16_slice", + "u32" => "write_u32_slice", + "u64" => "write_u64_slice", + "i8" => "write_i8_slice", + "i16" => "write_i16_slice", + "i32" => "write_i32_slice", + "i64" => "write_i64_slice", + "f32" => "write_f32_slice", + "f64" => "write_f64_slice", + _ => return None, + }; + Some(format_ident!("{method}")) +} + +fn sequence_element_type(ty: &Type) -> Option<&Type> { + match ty { + Type::Reference(reference) => match reference.elem.as_ref() { + Type::Slice(slice) => Some(slice.elem.as_ref()), + Type::Array(array) => Some(array.elem.as_ref()), + _ => None, + }, + Type::Slice(slice) => Some(slice.elem.as_ref()), + Type::Array(array) => Some(array.elem.as_ref()), + Type::Path(type_path) => { + let last = type_path.path.segments.last()?; + if last.ident != "Vec" && last.ident != "Cow" { + return None; + } + let PathArguments::AngleBracketed(args) = &last.arguments else { + return None; + }; + args.args.iter().find_map(|arg| match arg { + GenericArgument::Type(Type::Slice(slice)) => Some(slice.elem.as_ref()), + GenericArgument::Type(ty) if last.ident == "Vec" => Some(ty), + _ => None, + }) + } + _ => None, + } +} + +fn sequence_slice_method(ty: &Type) -> Option { + primitive_slice_method(sequence_element_type(ty)?) +} + fn should_use_bin(ty: &Type, cfg: Option<&FieldConfig>) -> bool { if !is_bin_type(ty) { return false; @@ -706,6 +766,11 @@ fn build_write_expr( quote! { writer.write_binary(::core::convert::AsRef::<[u8]>::as_ref(&#value))?; } + } else if let Some(method) = sequence_slice_method(ty) { + quote! { + writer.write_array_len(#value.len())?; + writer.#method(::core::convert::AsRef::<[_]>::as_ref(&#value))?; + } } else { quote! { #value.write(writer)?; @@ -1039,11 +1104,27 @@ fn expand_array_struct(data: &DataStruct) -> Result { }) .collect(); - let write = quote! { - writer.write_array_len(#array_len)?; - #( #slot_writes )* - Ok(()) - }; + let homogeneous_slice_method = + tys.first() + .and_then(primitive_slice_method) + .filter(|method| { + tys.iter() + .all(|ty| primitive_slice_method(ty).as_ref() == Some(method)) + }); + let write = + if let (true, Some(method)) = (is_dense_sequential, homogeneous_slice_method) { + quote! { + writer.write_array_len(#array_len)?; + writer.#method(&[#( self.#names ),*])?; + Ok(()) + } + } else { + quote! { + writer.write_array_len(#array_len)?; + #( #slot_writes )* + Ok(()) + } + }; let read = if is_dense_sequential { let direct_fields: Vec<_> = names @@ -1141,11 +1222,27 @@ fn expand_array_struct(data: &DataStruct) -> Result { }) .collect(); - let write = quote! { - writer.write_array_len(#array_len)?; - #( #slot_writes )* - Ok(()) - }; + let homogeneous_slice_method = + tys.first() + .and_then(primitive_slice_method) + .filter(|method| { + tys.iter() + .all(|ty| primitive_slice_method(ty).as_ref() == Some(method)) + }); + let write = + if let (true, Some(method)) = (is_dense_sequential, homogeneous_slice_method) { + quote! { + writer.write_array_len(#array_len)?; + writer.#method(&[#( self.#idx ),*])?; + Ok(()) + } + } else { + quote! { + writer.write_array_len(#array_len)?; + #( #slot_writes )* + Ok(()) + } + }; let ctor_values: Vec<_> = vars .iter() From 9c18d55938c16c898cc2e27a90ae92c4d084a721 Mon Sep 17 00:00:00 2001 From: nuskey8 Date: Sat, 29 Aug 2026 20:17:58 +0900 Subject: [PATCH 5/6] fix: use shorthand field initializers in generated code --- zerompk_derive/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zerompk_derive/src/lib.rs b/zerompk_derive/src/lib.rs index ad047cd..2974dd8 100644 --- a/zerompk_derive/src/lib.rs +++ b/zerompk_derive/src/lib.rs @@ -1099,7 +1099,7 @@ fn expand_array_struct(data: &DataStruct) -> Result { if field_configs[i].ignore { quote! { #name: <#ty as ::core::default::Default>::default() } } else { - quote! { #name: #name } + quote! { #name } } }) .collect(); @@ -1352,7 +1352,7 @@ fn expand_map_struct(data: &DataStruct, allow_unknown_fields: bool) -> Result::default() } } else { - quote! { #name: #name } + quote! { #name } } }) .collect(); @@ -1971,7 +1971,7 @@ fn build_enum_variant_payload( if field_configs[i].ignore { quote! { #n: <#ty as ::core::default::Default>::default() } } else { - quote! { #n: #n } + quote! { #n } } }) .collect(); @@ -2061,7 +2061,7 @@ fn build_enum_variant_payload( if field_configs[i].ignore { quote! { #n: <#ty as ::core::default::Default>::default() } } else { - quote! { #n: #n } + quote! { #n } } }) .collect(); From ed836c1984d2dfcaf06a47927fbe0b3d4675c876 Mon Sep 17 00:00:00 2001 From: nuskey8 Date: Sat, 29 Aug 2026 20:19:38 +0900 Subject: [PATCH 6/6] chore: update `write_slice` doc comment --- zerompk/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/zerompk/src/lib.rs b/zerompk/src/lib.rs index 85e4566..b2bcae1 100644 --- a/zerompk/src/lib.rs +++ b/zerompk/src/lib.rs @@ -51,9 +51,7 @@ pub trait ToMessagePack { /// Writes the MessagePack representation of this value into the provided writer. fn write(&self, writer: &mut W) -> Result<()>; - /// Writes a contiguous slice of values. Primitive implementations override this - /// to let writers reserve capacity and encode the whole slice in one operation. - #[doc(hidden)] + /// Writes the MessagePack representation of a slice of values into the provided writer. #[inline(always)] fn write_slice(values: &[Self], writer: &mut W) -> Result<()> where