Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/ros-z-cdr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ description = "CDR serialization for ros-z"
[dependencies]
serde = { workspace = true }
byteorder = { workspace = true }
bytemuck = { version = "1", features = ["derive", "extern_crate_alloc"] }
zenoh-buffers = { workspace = true }
thiserror = "1.0"

Expand Down
5 changes: 5 additions & 0 deletions crates/ros-z-cdr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
pub mod buffer;
pub mod deserializer;
pub mod error;
pub mod plain;
pub mod primitives;
pub mod serializer;
pub mod traits;
pub mod zbuf_writer;

use std::cell::RefCell;
Expand All @@ -36,8 +38,11 @@ pub use buffer::CdrBuffer;
pub use byteorder::{BigEndian, LittleEndian};
pub use deserializer::{CdrDeserializer, from_bytes, from_bytes_with};
pub use error::{Error, Result};
#[cfg(target_endian = "little")]
pub use plain::CdrPlain;
pub use primitives::{CdrReader, CdrWriter};
pub use serializer::{CdrSerializer, to_buffer, to_vec, to_vec_reuse};
pub use traits::{CdrDeserialize, CdrSerialize, CdrSerializedSize, cdr_to_vec};
pub use zbuf_writer::ZBufWriter;

/// Native endian type alias for the current platform.
Expand Down
65 changes: 65 additions & 0 deletions crates/ros-z-cdr/src/plain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! `CdrPlain` marker trait for types whose CDR wire layout equals their memory layout.
//!
//! When a type is `CdrPlain`, sequences of that type can be serialized and deserialized
//! with a single bulk memcpy instead of element-by-element encoding.
//!
//! # Safety invariants
//!
//! A type `T: CdrPlain` must satisfy **all** of:
//! 1. No padding bytes — `bytemuck::Pod` guarantees this at compile time.
//! 2. CDR wire layout == in-memory layout on little-endian hosts. For all ROS primitive
//! numeric types this holds: CDR encodes them in native byte order (LE) without
//! reordering fields or adding framing.
//! 3. Every possible bit pattern is a valid `T` — again `bytemuck::Pod`.
//!
//! This trait is only defined on little-endian targets because CDR uses little-endian
//! encoding for all primitive types. On a big-endian host the wire bytes would need
//! byte-swapping per element, making the bulk-copy path incorrect.

/// Marker trait for types whose CDR serialized form is identical to their in-memory
/// representation on little-endian hosts.
///
/// # Safety
/// Implementors must guarantee that:
/// - The type has no padding bytes.
/// - The CDR wire layout of the type matches its in-memory layout (true for all ROS
/// numeric primitives on LE hosts).
/// - Every possible bit pattern is a valid value (i.e. the type is `bytemuck::Pod`).
///
/// The `bytemuck::Pod` bound is enforced at the usage sites (`write_pod_slice`,
/// `read_pod_slice`) rather than here so that blanket impls for `[T; N]` can be
/// expressed — `bytemuck::Pod` is only impl'd for fixed array sizes up to 64.
///
/// This trait should only be implemented by codegen for generated message types, or
/// manually for well-known primitive types defined in this crate.
#[cfg(target_endian = "little")]
pub unsafe trait CdrPlain: Copy + 'static {}

// ── Primitive impls ──────────────────────────────────────────────────────────
// bool is excluded: bytemuck::Pod is not impl'd for bool (only 0/1 are valid).
// char is excluded: Rust char is 4-byte Unicode; CDR wchar is 2 bytes.

#[cfg(target_endian = "little")]
unsafe impl CdrPlain for f32 {}
#[cfg(target_endian = "little")]
unsafe impl CdrPlain for f64 {}
#[cfg(target_endian = "little")]
unsafe impl CdrPlain for i8 {}
#[cfg(target_endian = "little")]
unsafe impl CdrPlain for u8 {}
#[cfg(target_endian = "little")]
unsafe impl CdrPlain for i16 {}
#[cfg(target_endian = "little")]
unsafe impl CdrPlain for u16 {}
#[cfg(target_endian = "little")]
unsafe impl CdrPlain for i32 {}
#[cfg(target_endian = "little")]
unsafe impl CdrPlain for u32 {}
#[cfg(target_endian = "little")]
unsafe impl CdrPlain for i64 {}
#[cfg(target_endian = "little")]
unsafe impl CdrPlain for u64 {}

// Fixed arrays of plain types are themselves plain.
#[cfg(target_endian = "little")]
unsafe impl<T: CdrPlain, const N: usize> CdrPlain for [T; N] {}
41 changes: 41 additions & 0 deletions crates/ros-z-cdr/src/primitives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
//! by the serde-based serializer/deserializer and can also be used directly
//! for schema-driven (dynamic) message handling.

#[cfg(target_endian = "little")]
use bytemuck;
use byteorder::{ByteOrder, ReadBytesExt};
use std::marker::PhantomData;

Expand Down Expand Up @@ -159,6 +161,20 @@ impl<'a, BO: ByteOrder, B: CdrBuffer> CdrWriter<'a, BO, B> {
pub fn write_sequence_length(&mut self, len: usize) {
self.write_u32(len as u32);
}

/// Bulk-write a slice of plain (POD) values as raw bytes.
///
/// The caller must write the sequence length prefix separately before calling this.
/// Alignment is handled internally based on `T`'s alignment requirement.
///
/// Only available on little-endian hosts where CDR wire layout == memory layout.
#[cfg(target_endian = "little")]
#[inline]
pub fn write_pod_slice<T: crate::plain::CdrPlain + bytemuck::Pod>(&mut self, slice: &[T]) {
debug_assert!(!slice.is_empty());
self.align(std::mem::align_of::<T>());
self.buffer.extend_from_slice(bytemuck::cast_slice(slice));
}
}

/// Low-level CDR reader with alignment handling.
Expand Down Expand Up @@ -356,6 +372,31 @@ impl<'a, BO: ByteOrder> CdrReader<'a, BO> {
let len = self.read_u32()? as usize;
self.read_bytes(len)
}

/// Bulk-read `count` plain (POD) values as a zero-copy borrowed slice.
///
/// The caller must have already read the sequence length prefix.
/// Alignment is handled internally based on `T`'s alignment requirement.
///
/// Only available on little-endian hosts where CDR wire layout == memory layout.
#[cfg(target_endian = "little")]
#[inline]
pub fn read_pod_slice<T: crate::plain::CdrPlain + bytemuck::Pod>(
&mut self,
count: usize,
) -> Result<Vec<T>> {
if count == 0 {
return Ok(vec![]);
}
self.align(std::mem::align_of::<T>())?;
let byte_count = count
.checked_mul(std::mem::size_of::<T>())
.ok_or(Error::UnexpectedEof)?;
let bytes = self.read_bytes(byte_count)?;
// `pod_collect_to_vec` handles misaligned input buffers safely (copies into
// a freshly aligned allocation). `cast_slice` would panic on misaligned network data.
Ok(bytemuck::pod_collect_to_vec(bytes))
}
}

#[cfg(test)]
Expand Down
Loading
Loading