subetha_core/marshal.rs
1//! The `Marshal` trait - the type-system contract for "this value can
2//! cross an address-space boundary byte-identically."
3//!
4//! `Marshal` is strictly stronger than `Send`. A `Send` value can
5//! travel between threads inside one process, where pointers and
6//! references mean the same thing in both threads. A `Marshal` value
7//! can travel between *processes* (or be serialised to disk and read
8//! back), where pointers into the originating process's heap, file
9//! descriptors, and any other resource handle that means different
10//! things in different address spaces are forbidden.
11//!
12//! # The contract
13//!
14//! - [`Marshal::PAYLOAD_BYTES`] is the exact byte width of the
15//! marshalled form.
16//! - [`Marshal::marshal`] writes exactly `PAYLOAD_BYTES` into a
17//! caller-supplied buffer.
18//! - [`Marshal::unmarshal`] reads exactly `PAYLOAD_BYTES` and
19//! reconstructs a value byte-identical to the original.
20//! - Round-tripping: `unmarshal(&buf)` after `marshal(&v, &mut buf)`
21//! produces a value indistinguishable from `v` for every value `v`.
22//!
23//! # Why `unsafe`
24//!
25//! Correctness depends on every reachable byte of the value being
26//! position-independent across address spaces. The compiler cannot
27//! check this for arbitrary user types - a type with an inner
28//! `Box<u8>` plus a manual `marshal` impl that copies the box's
29//! *raw bytes* compiles cleanly and crashes at runtime. The trait is
30//! therefore `unsafe` to implement; the implementer asserts the
31//! contract holds.
32//!
33//! # Auto-impls
34//!
35//! Safe blanket impls are provided for the primitive integer and
36//! floating-point types, `bool`, `()`, and `[T; N]` where `T:
37//! Marshal`. These cover the common case (move a `u64` job ID, an
38//! `[u8; 48]` argument blob, a `(u32, u32)` pair) without requiring
39//! any unsafe code at the call site.
40//!
41//! # Connection to `pass_registry`
42//!
43//! `subetha_cxc::pass_registry` solves the same problem (closures
44//! that need to execute in another process) at the *runtime* layer,
45//! by registering closure handlers by integer ID. `Marshal` is the
46//! *compile-time* counterpart: a closure whose captured environment
47//! reduces to a `Marshal` payload can be shipped across processes by
48//! marshalling the payload and looking up the handler by ID. Both
49//! layers cooperate to make cross-process execution byte-safe.
50
51use core::fmt;
52
53/// Error returned when [`Marshal::unmarshal`] cannot reconstruct a
54/// value from the source buffer.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum MarshalError {
57 /// The source buffer is shorter than `PAYLOAD_BYTES`.
58 ShortBuffer { expected: usize, got: usize },
59 /// The source bytes do not encode a valid value of this type
60 /// (e.g. a `bool` byte that is neither 0 nor 1).
61 InvalidEncoding,
62}
63
64impl fmt::Display for MarshalError {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 match self {
67 Self::ShortBuffer { expected, got } => {
68 write!(f, "Marshal source buffer too short: expected {expected} bytes, got {got}")
69 }
70 Self::InvalidEncoding => write!(f, "Marshal source bytes do not encode a valid value"),
71 }
72 }
73}
74
75impl std::error::Error for MarshalError {}
76
77/// Type-system contract for "this value can be flattened into a
78/// fixed-size byte payload and reconstructed byte-identically in
79/// another address space."
80///
81/// See the [module docs](self) for the full contract.
82///
83/// # Safety
84///
85/// Implementer asserts that:
86/// - [`marshal`](Self::marshal) writes exactly `PAYLOAD_BYTES` into
87/// the destination buffer.
88/// - [`unmarshal`](Self::unmarshal) reads exactly `PAYLOAD_BYTES`
89/// from the source buffer.
90/// - Round-tripping is byte-identical and value-identical for every
91/// valid value of the type.
92/// - The marshalled bytes contain NO pointers, references, file
93/// descriptors, or other handles that mean different things in
94/// different address spaces.
95pub unsafe trait Marshal: Sized {
96 /// Exact byte width of the marshalled form.
97 const PAYLOAD_BYTES: usize;
98
99 /// Write the marshalled form of `self` into `dst`.
100 ///
101 /// `dst.len()` must be at least `PAYLOAD_BYTES`; implementations
102 /// write to `dst[..PAYLOAD_BYTES]` and leave any remaining bytes
103 /// unmodified. Panics on a short buffer.
104 fn marshal(&self, dst: &mut [u8]);
105
106 /// Read the marshalled form from `src` and reconstruct the value.
107 ///
108 /// `src.len()` must be at least `PAYLOAD_BYTES`; implementations
109 /// read from `src[..PAYLOAD_BYTES]`. Returns
110 /// [`MarshalError::ShortBuffer`] on a short buffer and
111 /// [`MarshalError::InvalidEncoding`] when the bytes do not encode
112 /// a valid value (e.g. an out-of-range discriminant).
113 fn unmarshal(src: &[u8]) -> Result<Self, MarshalError>;
114}
115
116// ---------------------------------------------------------------
117// Primitive impls. Each is sound because the type's bytes are
118// position-independent: an integer's bit pattern means the same
119// thing in every address space.
120// ---------------------------------------------------------------
121
122macro_rules! impl_marshal_for_primitive {
123 ($t:ty, $bytes:expr) => {
124 // SAFETY: $t has no internal pointers or handles; its raw
125 // bytes are position-independent across address spaces.
126 // Little-endian encoding is canonical and stable.
127 unsafe impl Marshal for $t {
128 const PAYLOAD_BYTES: usize = $bytes;
129
130 fn marshal(&self, dst: &mut [u8]) {
131 let bytes = self.to_le_bytes();
132 dst[..$bytes].copy_from_slice(&bytes);
133 }
134
135 fn unmarshal(src: &[u8]) -> Result<Self, MarshalError> {
136 if src.len() < $bytes {
137 return Err(MarshalError::ShortBuffer {
138 expected: $bytes,
139 got: src.len(),
140 });
141 }
142 let mut buf = [0u8; $bytes];
143 buf.copy_from_slice(&src[..$bytes]);
144 Ok(<$t>::from_le_bytes(buf))
145 }
146 }
147 };
148}
149
150impl_marshal_for_primitive!(u8, 1);
151impl_marshal_for_primitive!(u16, 2);
152impl_marshal_for_primitive!(u32, 4);
153impl_marshal_for_primitive!(u64, 8);
154impl_marshal_for_primitive!(u128, 16);
155impl_marshal_for_primitive!(i8, 1);
156impl_marshal_for_primitive!(i16, 2);
157impl_marshal_for_primitive!(i32, 4);
158impl_marshal_for_primitive!(i64, 8);
159impl_marshal_for_primitive!(i128, 16);
160impl_marshal_for_primitive!(f32, 4);
161impl_marshal_for_primitive!(f64, 8);
162
163// SAFETY: bool's two valid bit patterns are 0 and 1; the encoding
164// rejects any other byte.
165unsafe impl Marshal for bool {
166 const PAYLOAD_BYTES: usize = 1;
167 fn marshal(&self, dst: &mut [u8]) {
168 dst[0] = u8::from(*self);
169 }
170 fn unmarshal(src: &[u8]) -> Result<Self, MarshalError> {
171 if src.is_empty() {
172 return Err(MarshalError::ShortBuffer { expected: 1, got: 0 });
173 }
174 match src[0] {
175 0 => Ok(false),
176 1 => Ok(true),
177 _ => Err(MarshalError::InvalidEncoding),
178 }
179 }
180}
181
182// SAFETY: () has no bytes.
183unsafe impl Marshal for () {
184 const PAYLOAD_BYTES: usize = 0;
185 fn marshal(&self, _dst: &mut [u8]) {}
186 fn unmarshal(_src: &[u8]) -> Result<Self, MarshalError> { Ok(()) }
187}
188
189// SAFETY: an array of Marshal is Marshal: the concatenation of each
190// element's bytes is position-independent if every element is.
191unsafe impl<T: Marshal + Copy + Default, const N: usize> Marshal for [T; N] {
192 const PAYLOAD_BYTES: usize = T::PAYLOAD_BYTES * N;
193 fn marshal(&self, dst: &mut [u8]) {
194 for (i, item) in self.iter().enumerate() {
195 let off = i * T::PAYLOAD_BYTES;
196 item.marshal(&mut dst[off..off + T::PAYLOAD_BYTES]);
197 }
198 }
199 fn unmarshal(src: &[u8]) -> Result<Self, MarshalError> {
200 let need = T::PAYLOAD_BYTES * N;
201 if src.len() < need {
202 return Err(MarshalError::ShortBuffer { expected: need, got: src.len() });
203 }
204 let mut out = [T::default(); N];
205 for (i, slot) in out.iter_mut().enumerate() {
206 let off = i * T::PAYLOAD_BYTES;
207 *slot = T::unmarshal(&src[off..off + T::PAYLOAD_BYTES])?;
208 }
209 Ok(out)
210 }
211}
212
213// SAFETY: tuple of two Marshal values is Marshal by component-wise
214// concatenation; same argument as the array impl.
215unsafe impl<A: Marshal, B: Marshal> Marshal for (A, B) {
216 const PAYLOAD_BYTES: usize = A::PAYLOAD_BYTES + B::PAYLOAD_BYTES;
217 fn marshal(&self, dst: &mut [u8]) {
218 self.0.marshal(&mut dst[..A::PAYLOAD_BYTES]);
219 self.1.marshal(&mut dst[A::PAYLOAD_BYTES..A::PAYLOAD_BYTES + B::PAYLOAD_BYTES]);
220 }
221 fn unmarshal(src: &[u8]) -> Result<Self, MarshalError> {
222 let need = A::PAYLOAD_BYTES + B::PAYLOAD_BYTES;
223 if src.len() < need {
224 return Err(MarshalError::ShortBuffer { expected: need, got: src.len() });
225 }
226 let a = A::unmarshal(&src[..A::PAYLOAD_BYTES])?;
227 let b = B::unmarshal(&src[A::PAYLOAD_BYTES..need])?;
228 Ok((a, b))
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 fn round_trip<T: Marshal + PartialEq + std::fmt::Debug>(v: T) {
237 let mut buf = vec![0u8; T::PAYLOAD_BYTES];
238 v.marshal(&mut buf);
239 let back = T::unmarshal(&buf).unwrap();
240 assert_eq!(v, back);
241 }
242
243 #[test] fn u8_round_trip() { round_trip(0u8); round_trip(255u8); }
244 #[test] fn u32_round_trip() { round_trip(0u32); round_trip(u32::MAX); round_trip(0xDEAD_BEEFu32); }
245 #[test] fn u64_round_trip() { round_trip(0u64); round_trip(u64::MAX); round_trip(0xCAFEBABE_DEADBEEFu64); }
246 #[test] fn i64_round_trip() { round_trip(i64::MIN); round_trip(0i64); round_trip(i64::MAX); }
247 #[test] fn f64_round_trip() { round_trip(0.0_f64); round_trip(-1.5_f64); round_trip(f64::INFINITY); }
248 #[test] fn bool_round_trip() { round_trip(true); round_trip(false); }
249 #[test] fn unit_round_trip() { round_trip(()); }
250
251 #[test]
252 fn array_round_trip() {
253 round_trip([1u8, 2, 3, 4]);
254 round_trip([0u64; 8]);
255 round_trip([0xDEAD_BEEF_CAFE_BABE_u64, 0x1234_5678_9ABC_DEF0]);
256 }
257
258 #[test]
259 fn tuple_round_trip() {
260 round_trip((42u32, 7u64));
261 round_trip((true, 99i32));
262 }
263
264 #[test]
265 fn nested_array_in_tuple() {
266 let v: (u32, [u8; 16]) = (0xCAFEBABE, [9; 16]);
267 round_trip(v);
268 }
269
270 #[test]
271 fn bool_rejects_invalid_byte() {
272 match bool::unmarshal(&[42u8]) {
273 Err(MarshalError::InvalidEncoding) => {}
274 other => panic!("expected InvalidEncoding, got {other:?}"),
275 }
276 }
277
278 #[test]
279 fn short_buffer_rejected() {
280 match u64::unmarshal(&[0u8; 3]) {
281 Err(MarshalError::ShortBuffer { expected: 8, got: 3 }) => {}
282 other => panic!("expected ShortBuffer{{expected:8,got:3}}, got {other:?}"),
283 }
284 }
285
286 #[test]
287 fn payload_bytes_constants_match_sizes() {
288 assert_eq!(u8::PAYLOAD_BYTES, 1);
289 assert_eq!(u32::PAYLOAD_BYTES, 4);
290 assert_eq!(u64::PAYLOAD_BYTES, 8);
291 assert_eq!(u128::PAYLOAD_BYTES, 16);
292 assert_eq!(<[u64; 8]>::PAYLOAD_BYTES, 64);
293 assert_eq!(<(u32, u64)>::PAYLOAD_BYTES, 12);
294 }
295}